pax_global_header00006660000000000000000000000064152707007230014515gustar00rootroot0000000000000052 comment=a34b6fa56948bebb539cfece22adf5d022cc3c19 opentelemetry-python-contrib-0.66b2/000077500000000000000000000000001527070072300175455ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/.changelog/000077500000000000000000000000001527070072300215525ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/.changelog/.gitignore000066400000000000000000000000141527070072300235350ustar00rootroot00000000000000!.gitignore opentelemetry-python-contrib-0.66b2/.codespellrc000066400000000000000000000002311527070072300220410ustar00rootroot00000000000000[codespell] # skipping auto generated folders skip = ./.tox,./.mypy_cache,./docs/_build,./target,*/LICENSE,./venv,*/cassettes ignore-words-list = aks,ot opentelemetry-python-contrib-0.66b2/.coveragerc000066400000000000000000000000471527070072300216670ustar00rootroot00000000000000[run] omit = */tests/* */gen/* opentelemetry-python-contrib-0.66b2/.git-blame-ignore-revs000066400000000000000000000002401527070072300236410ustar00rootroot00000000000000# Switch to SPDX license headers (#4533) 1f17cc1931389ae2a6b43ac3a45956aa1860588f # Update line length to 120 (#4933) 3a96d1cfc6e183e3ccb91bf7505718a2c83a09c8 opentelemetry-python-contrib-0.66b2/.github/000077500000000000000000000000001527070072300211055ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/.github/CODEOWNERS000066400000000000000000000007411527070072300225020ustar00rootroot00000000000000# This file is only used as a way to assign any change to the approvers team # except for a change in any of the instrumentations. The actual codeowners # of the instrumentations are in .github/component_owners.yml. # Assigns any change to the approvers team... * @open-telemetry/opentelemetry-python-contrib-approvers # ...except for changes in any instrumentation. /instrumentation # Learn about CODEOWNERS file format: # https://help.github.com/en/articles/about-code-owners opentelemetry-python-contrib-0.66b2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001527070072300232705ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/.github/ISSUE_TEMPLATE/bug_report.yaml000066400000000000000000000061321527070072300263260ustar00rootroot00000000000000--- name: Bug Report description: Create a report to help us improve labels: [bug] body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! Please make sure to fill out the entire form below, providing as much context as you can in order to help us triage and track down your bug as quickly as possible. Before filing a bug, please be sure you have searched through [existing bugs](https://github.com/open-telemetry/opentelemetry-python-contrib/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Abug) to see if your bug is already addressed. - type: textarea id: environment attributes: label: Describe your environment description: | Please describe any aspect of your environment relevant to the problem, including your Python version, [platform](https://docs.python.org/3/library/platform.html), version numbers of installed dependencies, information about your cloud hosting provider, etc. If you're reporting a problem with a specific version of a library in this repo, please check whether the problem has been fixed on main. value: | OS: (e.g, Ubuntu) Python version: (e.g., Python 3.10.0) Package version: (e.g., 0.46.0) - type: textarea attributes: label: What happened? description: Please provide as much detail as you reasonably can. validations: required: true - type: textarea attributes: label: Steps to Reproduce description: Provide a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) if possible and the needed steps to reproduce the problem. validations: required: true - type: textarea attributes: label: Expected Result description: What did you expect to see? validations: required: true - type: textarea attributes: label: Actual Result description: What did you see instead? validations: required: true - type: textarea id: additional-context attributes: label: Additional context description: Add any other context about the problem here. placeholder: Any additional information... - type: dropdown id: contribute attributes: label: Would you like to implement a fix? description: For guidance on how to get started, refer to the [contribution guide](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/CONTRIBUTING.md). options: - "No" - "Yes" - type: dropdown attributes: label: Tip description: This element is static, used to render a helpful sub-heading for end-users and community members to help prioritize issues. Please leave as is. options: - [React](https://github.blog/news-insights/product-news/add-reactions-to-pull-requests-issues-and-comments/) with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding `+1` or `me too`, to help us triage it. Learn more [here](https://opentelemetry.io/community/end-user/issue-participation/). default: 0 opentelemetry-python-contrib-0.66b2/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000002411527070072300252550ustar00rootroot00000000000000contact_links: - name: Slack url: https://cloud-native.slack.com/archives/C01PD4HUVBL about: Or the `#otel-python` channel in the CNCF Slack instance. opentelemetry-python-contrib-0.66b2/.github/ISSUE_TEMPLATE/feature_request.yaml000066400000000000000000000050351527070072300273620ustar00rootroot00000000000000--- name: Feature Request description: Suggest an idea for this project labels: [feature-request] body: - type: markdown attributes: value: | Before opening a feature request against this repo, consider whether the feature should/could be implemented in the [other OpenTelemetry client libraries](https://github.com/open-telemetry/). If so, please [open an issue on opentelemetry-specification](https://github.com/open-telemetry/opentelemetry-specification/issues/new) first. - type: textarea id: related-problem attributes: label: What problem do you want to solve? description: Is your feature request related to a problem? If so, provide a concise description of the problem. placeholder: Describe the problem and include relevant issue IDs validations: required: true - type: textarea id: solution attributes: label: Describe the solution you'd like description: What do you want to happen instead? What is the expected behavior? placeholder: I'd like to ... validations: required: true - type: textarea id: alternatives attributes: label: Describe alternatives you've considered description: Which alternative solutions or features have you considered? placeholder: Some potential solutions validations: required: false - type: textarea id: additional-context attributes: label: Additional Context description: Add any other context about the feature request here. placeholder: Some related requests in other projects or upstream spec proposals. validations: required: false - type: dropdown id: contribute attributes: label: Would you like to implement a fix? description: | For guidance on how to get started, refer to the [contribution guide](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/CONTRIBUTING.md). options: - "No" - "Yes" - type: dropdown attributes: label: Tip description: This element is static, used to render a helpful sub-heading for end-users and community members to help prioritize issues. Please leave as is. options: - [React](https://github.blog/news-insights/product-news/add-reactions-to-pull-requests-issues-and-comments/) with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding `+1` or `me too`, to help us triage it. Learn more [here](https://opentelemetry.io/community/end-user/issue-participation/). default: 0 opentelemetry-python-contrib-0.66b2/.github/component_owners.yml000066400000000000000000000021041527070072300252240ustar00rootroot00000000000000components: instrumentation/opentelemetry-instrumentation-aiohttp-client: - herin049 instrumentation/opentelemetry-instrumentation-aiokafka: - dimastbk instrumentation/opentelemetry-instrumentation-asyncclick: - jomcgi instrumentation/opentelemetry-instrumentation-asyncio: - bourbonkk instrumentation/opentelemetry-instrumentation-exceptions: - iblancasa instrumentation/opentelemetry-instrumentation-botocore: - lukeina2z - yiyuan-he instrumentation/opentelemetry-instrumentation-pymssql: - guillaumep instrumentation/opentelemetry-instrumentation-urllib: - shalevr instrumentation/opentelemetry-instrumentation-urllib3: - shalevr processor/opentelemetry-processor-baggage: - codeboten propagator/opentelemetry-propagator-aws-xray: - jj22ee resource/opentelemetry-resource-detector-azure: - JacksonWeber - rads-1996 sdk-extension/opentelemetry-sdk-extension-aws: - srprash - jj22ee util/opentelemetry-util-genai: - Cirilla-zmh - DylanRussell - keith-decker - lmolkova opentelemetry-python-contrib-0.66b2/.github/instructions/000077500000000000000000000000001527070072300236515ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/.github/instructions/instrumentation-genai.instructions.md000066400000000000000000000013201527070072300332560ustar00rootroot00000000000000--- applyTo: "instrumentation-genai/**" --- Review rules for PRs touching `instrumentation-genai/**`. GenAI instrumentations are no longer developed in this repository. They live in [opentelemetry-python-genai](https://github.com/open-telemetry/opentelemetry-python-genai), which is where new instrumentations, features, and bug fixes go. Everything left under `instrumentation-genai/` is deprecated, receives security patches only, and will be removed from this repository in the future. Reject PRs that add new instrumentations here, add features, or fix non-security bugs, and point the author to the `opentelemetry-python-genai` repo. See [instrumentation-genai/AGENTS.md](../../instrumentation-genai/AGENTS.md). opentelemetry-python-contrib-0.66b2/.github/instructions/instrumentation.instructions.md000066400000000000000000000055021527070072300322030ustar00rootroot00000000000000--- applyTo: "{instrumentation,instrumentation-genai}/**" --- Review rules for PRs touching `instrumentation/**` and `instrumentation-genai/**`. Flag violations with a link to the rule. ## 0. GenAI instrumentations maintained elsewhere GenAI instrumentations are developed and released from the [opentelemetry-python-genai](https://github.com/open-telemetry/opentelemetry-python-genai) repository. Everything left under `instrumentation-genai/` here is deprecated, receives security patches only, and will be removed from this repository in the future. Reject PRs that add features or fix non-security bugs in these packages and point the author to the `opentelemetry-python-genai` repo. See [instrumentation-genai/AGENTS.md](../../instrumentation-genai/AGENTS.md). ## 1. Reviewer mindset Review as long-term maintainer. For new instrumentations, consult upstream library docs and judge: - Does the library already emit its own telemetry, making this instrumentation redundant? - Is the library used widely enough to warrant a package in this repo? - Does it avoid unbounded in-memory accumulation or other side-effects? For changes to existing instrumentations: prefer back-compat. Break users only for a real reason; prefer opt-in or additive. Breaking changes need explicit justification in the PR. ## 2. Component ownership & maintenance commitment - New instrumentations must add an entry under the correct folder in [`component_owners.yml`](../component_owners.yml) in the same PR. Contributor must commit to long-term maintenance. See [Expectations from contributors](../../CONTRIBUTING.md#expectations-from-contributors) and the general [instrumentation checklist](../../CONTRIBUTING.md#guideline-for-instrumentations). ## 3. Semantic conventions - Attribute names must come from the semconv attribute modules, not hardcoded strings. Use the module matching the namespace under `opentelemetry.semconv` (e.g. `server_attributes`, `error_attributes`, `http_attributes`, `db_attributes`, …). - For attributes with a well-known value set in semconv, use the generated enum from the same modules instead of string literals. - If a signal is not in semconv, wait until semconv lands. ## 4. Exception handling - 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. ## 5. Tests - For every public API instrumented, cover sync/async variants when both exist. - Cover happy path and error scenarios. - 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}.txt` and `{oldest,latest}` `tox.ini` factors. See also [AGENTS.md](../../AGENTS.md) for general repo rules. opentelemetry-python-contrib-0.66b2/.github/instructions/util-genai.instructions.md000066400000000000000000000066721527070072300310070ustar00rootroot00000000000000--- applyTo: "util/opentelemetry-util-genai/**" --- Review rules for PRs touching `util/opentelemetry-util-genai/**`. Flag violations with a link to the rule. ## 0. Reviewer mindset Review as long-term maintainer. Every GenAI instrumentation in the repo depends on this package; churn breaks them. Default to back-compat changes. Every public addition is a long-term commitment — limit public API. ## 1. Semconv first No code without semconv. If a signal, attribute, or operation is not in the [GenAI semconv](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/), land the semconv change first. ## 2. Semantic conventions - Names (attributes, operations, spans) must match semconv exactly. - Use matching `opentelemetry.semconv` modules per namespace (`gen_ai`, `server`, `error`, …). Do not hardcode name strings if a constant exists. - Shared attributes must behave consistently across invocation types (same conditions, same defaults). If semconv marks an attribute for multiple invocations, set it on all. ## 3. API backwards compatibility - Do not remove or rename public objects. Deprecate first via a docstring note pointing to the replacement (not `@deprecated` — unreliable). - Private modules and module-private objects start with `_`. - Default to internal (`_`-prefixed) unless instrumentations need it public. ## 4. Invocation shape - `start_*()` factories must accept all sampling-relevant semconv attributes as parameters. Attributes also marked required by semconv must be required parameters (no default value). - `start_*()` factories must map 1:1 to distinct semconv operation types (inference, embeddings, tool execution, agent invocation, workflow invocation). Names must match the operation unambiguously — e.g., `create_agent` vs `invoke_agent` are distinct ops; `start_agent()` alone is ambiguous. - Each operation exposes both a factory (`start_inference(...)`) and a context-manager (`inference(...)`) form. - Never construct invocation types directly (`InferenceInvocation(...)`) — skips span creation, silent no-ops. Always use `handler.start_*()` or the context manager. ## 5. Exception handling - When catching exceptions from the underlying library to record telemetry, always re-raise the original exception unmodified. - Do not raise **new** exceptions in utils code. ## 6. Performance Keep the hot path tight: - Avoid per-invocation allocations; do not accumulate state unboundedly. - Skip content capture when content capture is disabled. - Skip setting span attributes when the span is not recording. - Still record attributes that feed metrics — metric recording is independent of span sampling. ## 7. DRY Do not copy-paste logic across invocation types. Extract shared helpers. ## 8. Tests - Every new operation type or attribute change needs tests asserting exact attribute names **and value types**, checked against semconv. - Cover success (`invocation.stop()`), failure (`invocation.fail(exc)`), and conditional attribute logic. - Prefer public API in tests. ## 9. Documentation - Docstrings for invocation types and span/event helpers must link to the corresponding semconv op. - When adding/changing attributes, update the docstring with what is set and when. ## 10. PR description - Cover which part of the GenAI semconv the change implements or follows (when applicable) and how instrumentations should consume it. See also [AGENTS.md](../../AGENTS.md) for general repo rules. opentelemetry-python-contrib-0.66b2/.github/labeler.yml000066400000000000000000000002731527070072300232400ustar00rootroot00000000000000gen-ai: - changed-files: - any-glob-to-any-file: - 'instrumentation-genai/**' - 'util/opentelemetry-util-genai/**' - 'docs/instrumentation-genai/**' opentelemetry-python-contrib-0.66b2/.github/pull_request_template.md000066400000000000000000000022311527070072300260440ustar00rootroot00000000000000# Description Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. Fixes # (issue) ## Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update # How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration - [ ] Test A # Does This PR Require a Core Repo Change? - [ ] Yes. - Link to PR: - [ ] No. # Checklist: See [contributing.md](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/CONTRIBUTING.md) for styleguide, changelog guidelines, and more. - [ ] Followed the style guidelines of this project - [ ] Changelogs have been updated - [ ] Unit tests have been added - [ ] Documentation has been updated opentelemetry-python-contrib-0.66b2/.github/scripts/000077500000000000000000000000001527070072300225745ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/.github/scripts/update-version-patch.sh000077500000000000000000000004661527070072300272030ustar00rootroot00000000000000#!/bin/bash -e sed -i "/\[stable\]/{n;s/version=.*/version=$1/}" eachdist.ini sed -i "/\[prerelease\]/{n;s/version=.*/version=$2/}" eachdist.ini ./scripts/eachdist.py update_patch_versions \ --stable_version=$1 \ --unstable_version=$2 \ --stable_version_prev=$3 \ --unstable_version_prev=$4 opentelemetry-python-contrib-0.66b2/.github/scripts/update-version.sh000077500000000000000000000003251527070072300261000ustar00rootroot00000000000000#!/bin/bash -e sed -i "/\[stable\]/{n;s/version=.*/version=$1/}" eachdist.ini sed -i "/\[prerelease\]/{n;s/version=.*/version=$2/}" eachdist.ini ./scripts/eachdist.py update_versions --versions stable,prereleaseopentelemetry-python-contrib-0.66b2/.github/scripts/use-cla-approved-github-bot.sh000077500000000000000000000001551527070072300303450ustar00rootroot00000000000000#!/bin/bash -e git config user.name otelbot git config user.email 197425009+otelbot@users.noreply.github.comopentelemetry-python-contrib-0.66b2/.github/workflows/000077500000000000000000000000001527070072300231425ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/.github/workflows/add-to-project.yml000066400000000000000000000020051527070072300264760ustar00rootroot00000000000000name: Add PR to project board on: pull_request_target: types: [opened, reopened, ready_for_review] permissions: contents: read jobs: add-to-project: name: add to project board runs-on: ubuntu-latest if: github.event.pull_request.draft == false steps: # NOTE: do NOT add an actions/checkout step here. This workflow uses # pull_request_target (which has access to secrets) but must never # execute code from the fork branch. See open-telemetry/opentelemetry-python#4955 for context. - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 id: otelbot-token with: client-id: ${{ vars.OTELBOT_PYTHON_CLIENT_ID }} private-key: ${{ secrets.OTELBOT_PYTHON_PRIVATE_KEY }} - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 with: project-url: https://github.com/orgs/open-telemetry/projects/88 github-token: ${{ steps.otelbot-token.outputs.token }} opentelemetry-python-contrib-0.66b2/.github/workflows/backport.yml000066400000000000000000000044041527070072300254740ustar00rootroot00000000000000name: Backport on: workflow_dispatch: inputs: number: description: "The pull request # to backport" required: true permissions: contents: read jobs: backport: permissions: contents: write # required for pushing branches pull-requests: write # required for creating and editing pull requests runs-on: ubuntu-latest steps: - run: | if [[ ! $GITHUB_REF_NAME =~ ^release/v[0-9]+\.[0-9]+\.x-0\.[0-9]+bx$ ]]; then echo this workflow should only be run against long-term release branches exit 1 fi - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: # history is needed to run git cherry-pick below fetch-depth: 0 - name: Use CLA approved github bot run: .github/scripts/use-cla-approved-github-bot.sh - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 id: otelbot-token with: client-id: ${{ vars.OTELBOT_CLIENT_ID }} private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} - name: Create pull request id: create_pr env: NUMBER: ${{ github.event.inputs.number }} # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} run: | commit=$(gh pr view $NUMBER --json mergeCommit --jq .mergeCommit.oid) title=$(gh pr view $NUMBER --json title --jq .title) branch="otelbot/backport-${NUMBER}-to-${GITHUB_REF_NAME//\//-}" git cherry-pick $commit git push origin HEAD:$branch pr_url=$(gh pr create --title "[$GITHUB_REF_NAME] $title" \ --body "Clean cherry-pick of #$NUMBER to the \`$GITHUB_REF_NAME\` branch." \ --head $branch \ --base $GITHUB_REF_NAME) echo "pr_url=$pr_url" >> $GITHUB_OUTPUT - name: Add backport label to PR if: steps.create_pr.outputs.pr_url != '' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh pr edit ${{ steps.create_pr.outputs.pr_url }} --add-label "backport" opentelemetry-python-contrib-0.66b2/.github/workflows/changelog.yml000066400000000000000000000070371527070072300256230ustar00rootroot00000000000000# This action requires that any PR targeting the main branch should add a # changelog fragment file in a .changelog/ directory. If a changelog entry # is not required, add the "Skip Changelog" label to disable this action. name: changelog on: pull_request: types: [opened, synchronize, reopened, labeled, unlabeled] branches: - main merge_group: permissions: contents: read jobs: changelog: runs-on: ubuntu-latest if: | github.event_name != 'merge_group' && !contains(github.event.pull_request.labels.*.name, 'Skip Changelog') && github.actor != 'otelbot[bot]' steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 - name: Fetch base branch run: git fetch origin ${{ github.base_ref }} --depth=1 - name: Ensure no direct changes to CHANGELOG.md run: | if [[ $(git diff --name-only FETCH_HEAD -- 'CHANGELOG.md' '**/CHANGELOG.md') ]] then echo "CHANGELOG.md files should not be directly modified." echo "Please add a changelog fragment file to the appropriate .changelog/ directory instead." echo "See CONTRIBUTING.md for details." echo "" echo "Or add the \"Skip Changelog\" label if this job should be skipped." false fi - name: Check changelog fragments env: PR_NUMBER: ${{ github.event.pull_request.number }} run: | # Check for any new fragment files in any .changelog/ directory fragments=$(git diff --diff-filter=A --name-only FETCH_HEAD -- '**/.changelog/*' '.changelog/*' | grep -v '.gitignore' || true) if [[ -z "$fragments" ]]; then echo "No changelog fragment found for this PR." echo "" echo "Add a file named .changelog/${PR_NUMBER}." echo "where is one of: added, changed, deprecated, removed, fixed" echo "" echo "For coordinated packages, add to the root .changelog/ directory." echo "For independently released packages, add to /.changelog/" echo "See CONTRIBUTING.md for details." echo "" echo "Or add the \"Skip Changelog\" label if this job should be skipped." exit 1 fi invalid=() while IFS= read -r f; do encoding=$(file --brief --mime-encoding "$f") if [[ "$encoding" != "us-ascii" && "$encoding" != "utf-8" ]]; then invalid+=("$f (must be UTF-8 encoded; detected $encoding)") fi base=$(basename "$f") if [[ ! "$base" =~ ^([0-9]+)\.(added|changed|deprecated|removed|fixed)$ ]]; then invalid+=("$f (expected .; type one of added, changed, deprecated, removed, fixed)") continue fi if [[ "${BASH_REMATCH[1]}" != "${PR_NUMBER}" ]]; then invalid+=("$f (PR number ${BASH_REMATCH[1]} does not match this PR's number ${PR_NUMBER})") fi done <<< "$fragments" if (( ${#invalid[@]} > 0 )); then echo "Invalid changelog fragment(s):" for msg in "${invalid[@]}"; do echo " $msg" done exit 1 fi echo "Found changelog fragment(s):" echo "$fragments" - name: Install towncrier run: pip install towncrier==25.8.0 - name: Preview changelog run: towncrier build --draft --version Unreleased opentelemetry-python-contrib-0.66b2/.github/workflows/check-links.yml000066400000000000000000000064011527070072300260610ustar00rootroot00000000000000name: check-links on: push: branches: [ main ] paths: - '**/*.md' - '**/*.rst' - '.github/workflows/check-links.yml' - '.github/workflows/check_links_config.json' pull_request: paths: - '**/*.md' - '**/*.rst' - '.github/workflows/check-links.yml' - '.github/workflows/check_links_config.json' merge_group: paths: - '**/*.md' - '**/*.rst' - '.github/workflows/check-links.yml' - '.github/workflows/check_links_config.json' permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true env: DIFF_RANGE: ${{ github.event_name == 'merge_group' && format('{0}...{1}', github.event.merge_group.base_sha, github.event.merge_group.head_sha) || format('origin/{0}...HEAD', github.base_ref) }} jobs: check-links: runs-on: ubuntu-latest if: ${{ github.actor != 'dependabot[bot]' && github.actor != 'otelbot[bot]' }} timeout-minutes: 15 steps: - name: Checkout Repo uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: fetch-depth: 0 - name: Get changed markdown files id: changed-files uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5 with: files: | **/*.md **/*.rst - name: Install markdown-link-check if: steps.changed-files.outputs.any_changed == 'true' run: npm install -g markdown-link-check@v3.12.2 - name: Check links on push to main if: steps.changed-files.outputs.any_changed == 'true' && github.event_name == 'push' run: | markdown-link-check \ --verbose \ --config .github/workflows/check_links_config.json \ ${{ steps.changed-files.outputs.all_changed_files }} \ || { echo "Check that anchor links are lowercase"; exit 1; } - name: Check new links if: steps.changed-files.outputs.any_changed == 'true' && (github.event_name == 'pull_request' || github.event_name == 'merge_group') run: | # Extract URLs only from added lines in the diff to avoid # rate limiting when checking all links in large files like # CHANGELOG.md. Only new/changed links are checked on PRs; # pushes to main still check all links in changed files. git diff "$DIFF_RANGE" -- \ ${{ steps.changed-files.outputs.all_changed_files }} \ | grep '^+' | grep -v '^+++' \ | grep -oP 'https?://[^\s\)\]\"'"'"'`>]+' \ | sort -u > /tmp/new_links.txt if [ ! -s /tmp/new_links.txt ]; then echo "No new links found in diff, skipping check" exit 0 fi echo "Checking $(wc -l < /tmp/new_links.txt) new links:" cat /tmp/new_links.txt # Write links as markdown so markdown-link-check can parse them awk '{print "- <" $0 ">"}' /tmp/new_links.txt > /tmp/new_links.md markdown-link-check \ --verbose \ --config .github/workflows/check_links_config.json \ /tmp/new_links.md \ || { echo "Check that anchor links are lowercase"; exit 1; } opentelemetry-python-contrib-0.66b2/.github/workflows/check_links_config.json000066400000000000000000000003601527070072300276360ustar00rootroot00000000000000{ "ignorePatterns": [ { "pattern": "http(s)?://\\d+\\.\\d+\\.\\d+\\.\\d+" }, { "pattern": "http(s)?://localhost" }, { "pattern": "http(s)?://example.com" } ], "aliveStatusCodes": [429, 200] } opentelemetry-python-contrib-0.66b2/.github/workflows/ci.yml000066400000000000000000000014321527070072300242600ustar00rootroot00000000000000# Do not edit this file. # This file is generated automatically by executing tox -e generate-workflows name: CI on: push: branches: - 'main' pull_request: merge_group: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: misc: uses: ./.github/workflows/misc.yml lint: uses: ./.github/workflows/lint.yml tests: uses: ./.github/workflows/test.yml check: if: always() needs: - misc - lint - tests runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35 # v1.3.0 with: jobs: ${{ toJSON(needs) }} opentelemetry-python-contrib-0.66b2/.github/workflows/codeql-analysis.yml000066400000000000000000000026341527070072300267620ustar00rootroot00000000000000name: CodeQL Analysis on: workflow_dispatch: schedule: # ┌───────────── minute (0 - 59) # │ ┌───────────── hour (0 - 23) # │ │ ┌───────────── day of the month (1 - 31) # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) # │ │ │ │ │ # │ │ │ │ │ # │ │ │ │ │ # * * * * * - cron: '30 1 * * *' permissions: contents: read jobs: CodeQL-Build: permissions: security-events: write # for github/codeql-action/analyze to upload SARIF results runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@6f5948dfacef28e207b48d0905cf90c03365536d # v3.37.9 with: languages: python - name: Autobuild uses: github/codeql-action/autobuild@6f5948dfacef28e207b48d0905cf90c03365536d # v3.37.9 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@6f5948dfacef28e207b48d0905cf90c03365536d # v3.37.9 opentelemetry-python-contrib-0.66b2/.github/workflows/component-owners.yml000066400000000000000000000014411527070072300272020ustar00rootroot00000000000000# This action assigns and requires approval from owners of components for # PRs that are open against those components. Components are defined as # individual paths within this repository. name: 'Component Owners' on: pull_request_target: permissions: contents: read jobs: run_self: runs-on: ubuntu-latest name: Auto Assign Owners permissions: contents: read # to read changed files issues: write # to read/write issue assignees pull-requests: write # to read/write PR reviewers # Don't fail tests if this workflow fails. Some pending issues: # - https://github.com/dyladan/component-owners/issues/8 continue-on-error: true steps: - uses: dyladan/component-owners@7ff2b343629407c4dbe1c28ae66f55f723543d2b # v0.2.0 opentelemetry-python-contrib-0.66b2/.github/workflows/core_contrib_test.yml000066400000000000000000004005441527070072300274030ustar00rootroot00000000000000# Do not edit this file. # This file is generated automatically by executing tox -e generate-workflows name: Core Contrib Test on: workflow_call: inputs: CORE_REPO_SHA: required: true type: string CONTRIB_REPO_SHA: required: true type: string permissions: contents: read env: CORE_REPO_SHA: ${{ inputs.CORE_REPO_SHA }} CONTRIB_REPO_SHA: ${{ inputs.CONTRIB_REPO_SHA }} PIP_EXISTS_ACTION: w CORE_REPO_API: ${{ github.workspace }}/opentelemetry-python/opentelemetry-api CORE_REPO_SDK: ${{ github.workspace }}/opentelemetry-python/opentelemetry-sdk CORE_REPO_SEMCONV: ${{ github.workspace }}/opentelemetry-python/opentelemetry-semantic-conventions CORE_REPO_TEST_UTILS: ${{ github.workspace }}/opentelemetry-python/tests/opentelemetry-test-utils jobs: py310-test-instrumentation-openai-v2-oldest: name: instrumentation-openai-v2-oldest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-openai-v2-oldest -- -ra py310-test-instrumentation-openai-v2-latest: name: instrumentation-openai-v2-latest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-openai-v2-latest -- -ra py310-test-instrumentation-openai_agents-v2-oldest: name: instrumentation-openai_agents-v2-oldest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-openai_agents-v2-oldest -- -ra py310-test-instrumentation-openai_agents-v2-latest: name: instrumentation-openai_agents-v2-latest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-openai_agents-v2-latest -- -ra py310-test-instrumentation-vertexai-oldest: name: instrumentation-vertexai-oldest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-vertexai-oldest -- -ra py310-test-instrumentation-vertexai-latest: name: instrumentation-vertexai-latest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-vertexai-latest -- -ra py310-test-resource-detector-containerid: name: resource-detector-containerid runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-resource-detector-containerid -- -ra py310-test-resource-detector-azure-0: name: resource-detector-azure-0 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-resource-detector-azure-0 -- -ra py310-test-resource-detector-azure-1: name: resource-detector-azure-1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-resource-detector-azure-1 -- -ra py310-test-resourcedetector-gcp-0: name: resourcedetector-gcp-0 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-resourcedetector-gcp-0 -- -ra py310-test-resourcedetector-gcp-1: name: resourcedetector-gcp-1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-resourcedetector-gcp-1 -- -ra py310-test-sdk-extension-aws-0: name: sdk-extension-aws-0 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-sdk-extension-aws-0 -- -ra py310-test-sdk-extension-aws-1: name: sdk-extension-aws-1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-sdk-extension-aws-1 -- -ra py310-test-distro: name: distro runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-distro -- -ra py310-test-opentelemetry-instrumentation-wrapt1: name: opentelemetry-instrumentation-wrapt1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-opentelemetry-instrumentation-wrapt1 -- -ra py310-test-opentelemetry-instrumentation-wrapt2: name: opentelemetry-instrumentation-wrapt2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-opentelemetry-instrumentation-wrapt2 -- -ra py310-test-instrumentation-aiohttp-client: name: instrumentation-aiohttp-client runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-aiohttp-client -- -ra py310-test-instrumentation-aiohttp-server: name: instrumentation-aiohttp-server runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-aiohttp-server -- -ra py310-test-instrumentation-aiopg-wrapt1: name: instrumentation-aiopg-wrapt1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-aiopg-wrapt1 -- -ra py310-test-instrumentation-aiopg-wrapt2: name: instrumentation-aiopg-wrapt2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-aiopg-wrapt2 -- -ra py310-test-instrumentation-aws-lambda: name: instrumentation-aws-lambda runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-aws-lambda -- -ra py310-test-instrumentation-botocore-0-wrapt1: name: instrumentation-botocore-0-wrapt1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-botocore-0-wrapt1 -- -ra py310-test-instrumentation-botocore-0-wrapt2: name: instrumentation-botocore-0-wrapt2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-botocore-0-wrapt2 -- -ra py310-test-instrumentation-botocore-1-wrapt1: name: instrumentation-botocore-1-wrapt1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-botocore-1-wrapt1 -- -ra py310-test-instrumentation-botocore-1-wrapt2: name: instrumentation-botocore-1-wrapt2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-botocore-1-wrapt2 -- -ra py310-test-instrumentation-botocore-2: name: instrumentation-botocore-2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-botocore-2 -- -ra py310-test-instrumentation-botocore-3: name: instrumentation-botocore-3 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-botocore-3 -- -ra py310-test-instrumentation-boto3sqs: name: instrumentation-boto3sqs runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-boto3sqs -- -ra py310-test-instrumentation-django-0: name: instrumentation-django-0 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-django-0 -- -ra py310-test-instrumentation-django-1: name: instrumentation-django-1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-django-1 -- -ra py310-test-instrumentation-django-2: name: instrumentation-django-2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-django-2 -- -ra py310-test-instrumentation-django-3: name: instrumentation-django-3 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-django-3 -- -ra py310-test-instrumentation-dbapi-wrapt1: name: instrumentation-dbapi-wrapt1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-dbapi-wrapt1 -- -ra py310-test-instrumentation-dbapi-wrapt2: name: instrumentation-dbapi-wrapt2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-dbapi-wrapt2 -- -ra py310-test-instrumentation-asyncclick: name: instrumentation-asyncclick runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-asyncclick -- -ra py310-test-instrumentation-click: name: instrumentation-click runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-click -- -ra py310-test-instrumentation-falcon-1: name: instrumentation-falcon-1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-falcon-1 -- -ra py310-test-instrumentation-falcon-2: name: instrumentation-falcon-2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-falcon-2 -- -ra py310-test-instrumentation-falcon-3: name: instrumentation-falcon-3 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-falcon-3 -- -ra py310-test-instrumentation-falcon-4: name: instrumentation-falcon-4 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-falcon-4 -- -ra py310-test-instrumentation-fastapi-oldest: name: instrumentation-fastapi-oldest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-fastapi-oldest -- -ra py310-test-instrumentation-fastapi-latest: name: instrumentation-fastapi-latest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-fastapi-latest -- -ra py310-test-instrumentation-flask-0: name: instrumentation-flask-0 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-flask-0 -- -ra py310-test-instrumentation-flask-1: name: instrumentation-flask-1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-flask-1 -- -ra py310-test-instrumentation-flask-2: name: instrumentation-flask-2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-flask-2 -- -ra py310-test-instrumentation-flask-3: name: instrumentation-flask-3 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-flask-3 -- -ra py310-test-instrumentation-urllib: name: instrumentation-urllib runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-urllib -- -ra py310-test-instrumentation-urllib3-0: name: instrumentation-urllib3-0 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-urllib3-0 -- -ra py310-test-instrumentation-urllib3-1: name: instrumentation-urllib3-1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-urllib3-1 -- -ra py310-test-instrumentation-requests: name: instrumentation-requests runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-requests -- -ra py310-test-instrumentation-starlette-oldest: name: instrumentation-starlette-oldest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-starlette-oldest -- -ra py310-test-instrumentation-starlette-latest: name: instrumentation-starlette-latest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-starlette-latest -- -ra py310-test-instrumentation-structlog-oldest: name: instrumentation-structlog-oldest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-structlog-oldest -- -ra py310-test-instrumentation-structlog-latest: name: instrumentation-structlog-latest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-structlog-latest -- -ra py310-test-instrumentation-jinja2: name: instrumentation-jinja2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-jinja2 -- -ra py310-test-instrumentation-logging: name: instrumentation-logging runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-logging -- -ra py310-test-instrumentation-exceptions: name: instrumentation-exceptions runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-exceptions -- -ra py310-test-exporter-richconsole: name: exporter-richconsole runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-exporter-richconsole -- -ra py310-test-exporter-prometheus-remote-write: name: exporter-prometheus-remote-write runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-exporter-prometheus-remote-write -- -ra py310-test-instrumentation-mysql-0: name: instrumentation-mysql-0 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-mysql-0 -- -ra py310-test-instrumentation-mysql-1: name: instrumentation-mysql-1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-mysql-1 -- -ra py310-test-instrumentation-mysql-2: name: instrumentation-mysql-2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-mysql-2 -- -ra py310-test-instrumentation-mysqlclient: name: instrumentation-mysqlclient runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-mysqlclient -- -ra py310-test-instrumentation-psycopg2: name: instrumentation-psycopg2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-psycopg2 -- -ra py310-test-instrumentation-psycopg2-binary: name: instrumentation-psycopg2-binary runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-psycopg2-binary -- -ra py310-test-instrumentation-psycopg: name: instrumentation-psycopg runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-psycopg -- -ra py310-test-instrumentation-pymemcache-0: name: instrumentation-pymemcache-0 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-pymemcache-0 -- -ra py310-test-instrumentation-pymemcache-1: name: instrumentation-pymemcache-1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-pymemcache-1 -- -ra py310-test-instrumentation-pymemcache-2: name: instrumentation-pymemcache-2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-pymemcache-2 -- -ra py310-test-instrumentation-pymemcache-3: name: instrumentation-pymemcache-3 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-pymemcache-3 -- -ra py310-test-instrumentation-pymemcache-4: name: instrumentation-pymemcache-4 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-pymemcache-4 -- -ra py310-test-instrumentation-pymongo: name: instrumentation-pymongo runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-pymongo -- -ra py310-test-instrumentation-pymysql: name: instrumentation-pymysql runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-pymysql -- -ra py310-test-instrumentation-pymssql: name: instrumentation-pymssql runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-pymssql -- -ra py310-test-instrumentation-pyramid: name: instrumentation-pyramid runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-pyramid -- -ra py310-test-instrumentation-asgi: name: instrumentation-asgi runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-asgi -- -ra py310-test-instrumentation-asyncpg-wrapt1: name: instrumentation-asyncpg-wrapt1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-asyncpg-wrapt1 -- -ra py310-test-instrumentation-asyncpg-wrapt2: name: instrumentation-asyncpg-wrapt2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-asyncpg-wrapt2 -- -ra py310-test-instrumentation-sqlite3: name: instrumentation-sqlite3 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-sqlite3 -- -ra py310-test-instrumentation-wsgi: name: instrumentation-wsgi runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-wsgi -- -ra py310-test-instrumentation-grpc-0-wrapt1: name: instrumentation-grpc-0-wrapt1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-grpc-0-wrapt1 -- -ra py310-test-instrumentation-grpc-0-wrapt2: name: instrumentation-grpc-0-wrapt2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-grpc-0-wrapt2 -- -ra py310-test-instrumentation-grpc-1-wrapt1: name: instrumentation-grpc-1-wrapt1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-grpc-1-wrapt1 -- -ra py310-test-instrumentation-grpc-1-wrapt2: name: instrumentation-grpc-1-wrapt2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-grpc-1-wrapt2 -- -ra py310-test-instrumentation-sqlalchemy-1: name: instrumentation-sqlalchemy-1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-sqlalchemy-1 -- -ra py310-test-instrumentation-sqlalchemy-2: name: instrumentation-sqlalchemy-2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-sqlalchemy-2 -- -ra py310-test-instrumentation-redis: name: instrumentation-redis runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-redis -- -ra py310-test-instrumentation-remoulade: name: instrumentation-remoulade runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-remoulade -- -ra py310-test-instrumentation-celery: name: instrumentation-celery runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-celery -- -ra py310-test-instrumentation-system-metrics: name: instrumentation-system-metrics runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-system-metrics -- -ra py310-test-instrumentation-threading: name: instrumentation-threading runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-threading -- -ra py310-test-instrumentation-tornado: name: instrumentation-tornado runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-tornado -- -ra py310-test-instrumentation-tortoiseorm: name: instrumentation-tortoiseorm runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-tortoiseorm -- -ra py310-test-instrumentation-httpx-0-wrapt1: name: instrumentation-httpx-0-wrapt1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-httpx-0-wrapt1 -- -ra py310-test-instrumentation-httpx-0-wrapt2: name: instrumentation-httpx-0-wrapt2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-httpx-0-wrapt2 -- -ra py310-test-instrumentation-httpx-1-wrapt1: name: instrumentation-httpx-1-wrapt1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-httpx-1-wrapt1 -- -ra py310-test-instrumentation-httpx-1-wrapt2: name: instrumentation-httpx-1-wrapt2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-httpx-1-wrapt2 -- -ra py310-test-instrumentation-httpx-2-wrapt1: name: instrumentation-httpx-2-wrapt1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-httpx-2-wrapt1 -- -ra py310-test-instrumentation-httpx-2-wrapt2: name: instrumentation-httpx-2-wrapt2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-httpx-2-wrapt2 -- -ra py310-test-instrumentation-httpx-3-wrapt1: name: instrumentation-httpx-3-wrapt1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-httpx-3-wrapt1 -- -ra py310-test-instrumentation-httpx-3-wrapt2: name: instrumentation-httpx-3-wrapt2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-httpx-3-wrapt2 -- -ra py310-test-util-http: name: util-http runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-util-http -- -ra py310-test-exporter-credential-provider-gcp: name: exporter-credential-provider-gcp runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-exporter-credential-provider-gcp -- -ra py310-test-util-genai: name: util-genai runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-util-genai -- -ra py310-test-propagator-aws-xray-0: name: propagator-aws-xray-0 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-propagator-aws-xray-0 -- -ra py310-test-propagator-aws-xray-1: name: propagator-aws-xray-1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-propagator-aws-xray-1 -- -ra py310-test-propagator-ot-trace: name: propagator-ot-trace runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-propagator-ot-trace -- -ra py310-test-instrumentation-sio-pika-0-wrapt1: name: instrumentation-sio-pika-0-wrapt1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-sio-pika-0-wrapt1 -- -ra py310-test-instrumentation-sio-pika-0-wrapt2: name: instrumentation-sio-pika-0-wrapt2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-sio-pika-0-wrapt2 -- -ra py310-test-instrumentation-sio-pika-1-wrapt1: name: instrumentation-sio-pika-1-wrapt1 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-sio-pika-1-wrapt1 -- -ra py310-test-instrumentation-sio-pika-1-wrapt2: name: instrumentation-sio-pika-1-wrapt2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-sio-pika-1-wrapt2 -- -ra py310-test-instrumentation-aio-pika-oldest: name: instrumentation-aio-pika-oldest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-aio-pika-oldest -- -ra py310-test-instrumentation-aio-pika-latest: name: instrumentation-aio-pika-latest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-aio-pika-latest -- -ra py310-test-instrumentation-aiokafka: name: instrumentation-aiokafka runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-aiokafka -- -ra py310-test-instrumentation-kafka-python-oldest: name: instrumentation-kafka-python-oldest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-kafka-python-oldest -- -ra py310-test-instrumentation-kafka-python-latest: name: instrumentation-kafka-python-latest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-kafka-python-latest -- -ra py310-test-instrumentation-confluent-kafka: name: instrumentation-confluent-kafka runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-confluent-kafka -- -ra py310-test-instrumentation-asyncio: name: instrumentation-asyncio runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-asyncio -- -ra py310-test-instrumentation-cassandra-driver: name: instrumentation-cassandra-driver runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-cassandra-driver -- -ra py310-test-instrumentation-cassandra-scylla: name: instrumentation-cassandra-scylla runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-cassandra-scylla -- -ra py310-test-processor-baggage: name: processor-baggage runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-processor-baggage -- -ra py310-test-opamp-client-latest: name: opamp-client-latest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-opamp-client-latest -- -ra py310-test-opamp-client-lowest: name: opamp-client-lowest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${{ env.CORE_REPO_SHA }} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-opamp-client-lowest -- -ra opentelemetry-python-contrib-0.66b2/.github/workflows/fossa.yml000066400000000000000000000006221527070072300250000ustar00rootroot00000000000000name: FOSSA scanning on: push: branches: - main permissions: contents: read jobs: fossa: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: fossas/fossa-action@93a52ecf7c3ac7eb40f5de77fd69b1a19524de94 # v1.5.0 with: api-key: ${{secrets.FOSSA_API_KEY}} team: OpenTelemetry opentelemetry-python-contrib-0.66b2/.github/workflows/generate_workflows.py000066400000000000000000000011711527070072300274230ustar00rootroot00000000000000from pathlib import Path from generate_workflows_lib import ( generate_ci_workflow, generate_contrib_workflow, generate_lint_workflow, generate_misc_workflow, generate_test_workflow, ) tox_ini_path = Path(__file__).parent.parent.parent.joinpath("tox.ini") workflows_directory_path = Path(__file__).parent generate_test_workflow(tox_ini_path, workflows_directory_path, "ubuntu-latest") generate_lint_workflow(tox_ini_path, workflows_directory_path) generate_misc_workflow(tox_ini_path, workflows_directory_path) generate_contrib_workflow(workflows_directory_path) generate_ci_workflow(workflows_directory_path) opentelemetry-python-contrib-0.66b2/.github/workflows/generate_workflows_lib/000077500000000000000000000000001527070072300276775ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/.github/workflows/generate_workflows_lib/hatch_build.py000066400000000000000000000010031527070072300325110ustar00rootroot00000000000000from pathlib import Path from hatchling.builders.hooks.plugin.interface import BuildHookInterface class CustomBuildHook(BuildHookInterface): def initialize(self, version, build_data): with open(Path(__file__).parent.parent.parent.parent.joinpath("tox.ini")) as tox_ini_file_0: with open( Path(__file__).parent.joinpath("src/generate_workflows_lib/tox.ini"), "w", ) as tox_ini_file_1: tox_ini_file_1.write(tox_ini_file_0.read()) opentelemetry-python-contrib-0.66b2/.github/workflows/generate_workflows_lib/pyproject.toml000066400000000000000000000017001527070072300326110ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "generate-workflows-lib" dynamic = ["version"] description = "A library to generate workflows" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Typing :: Typed", ] dependencies = ["Jinja2", "tox"] [tool.hatch.version] path = "src/generate_workflows_lib/version.py" [tool.hatch.build.targets.wheel.hooks.custom] opentelemetry-python-contrib-0.66b2/.github/workflows/generate_workflows_lib/src/000077500000000000000000000000001527070072300304665ustar00rootroot00000000000000generate_workflows_lib/000077500000000000000000000000001527070072300351445ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/.github/workflows/generate_workflows_lib/src.gitignore000066400000000000000000000000101527070072300371230ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/.github/workflows/generate_workflows_lib/src/generate_workflows_libtox.ini __init__.py000066400000000000000000000140761527070072300372650ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/.github/workflows/generate_workflows_lib/src/generate_workflows_libfrom collections import defaultdict from pathlib import Path from re import compile as re_compile from jinja2 import Environment, FileSystemLoader from tox.config.cli.parse import get_options from tox.config.sets import CoreConfigSet from tox.config.source.tox_ini import ToxIni from tox.session.state import State _tox_test_env_regex = re_compile( r"(?Ppy\w+)-test-" r"(?P[-\w]+\w)-?(?P\d+)?" ) _tox_lint_env_regex = re_compile(r"lint-(?P[-\w]+)") _tox_contrib_env_regex = re_compile(r"py310-test-(?P[-\w]+\w)-?(?P\d+)?") def get_tox_envs(tox_ini_path: Path) -> list: tox_ini = ToxIni(tox_ini_path) conf = State(get_options(), []).conf tox_section = next(tox_ini.sections()) core_config_set = CoreConfigSet(conf, tox_section, tox_ini_path.parent, tox_ini_path) ( core_config_set.loaders.extend( tox_ini.get_loaders( tox_section, base=[], override_map=defaultdict(list, {}), conf=core_config_set, ) ) ) return core_config_set.load("env_list") def get_test_job_datas(tox_envs: list, operating_systems: list) -> list: os_alias = {"ubuntu-latest": "Ubuntu", "windows-latest": "Windows"} python_version_alias = { "pypy3": "pypy-3.10", "py310": "3.10", "py311": "3.11", "py312": "3.12", "py313": "3.13", "py314": "3.14", } test_job_datas = [] for operating_system in operating_systems: for tox_env in tox_envs: tox_test_env_match = _tox_test_env_regex.match(tox_env) if tox_test_env_match is None: continue groups = tox_test_env_match.groupdict() aliased_python_version = python_version_alias[groups["python_version"]] tox_env = tox_test_env_match.string test_requirements = groups["test_requirements"] if test_requirements is None: test_requirements = " " else: test_requirements = f"-{test_requirements} " test_job_datas.append( { "name": f"{tox_env}_{operating_system}", "ui_name": ( f"{groups['name']}{test_requirements}{aliased_python_version} {os_alias[operating_system]}" ), "python_version": aliased_python_version, "tox_env": tox_env, "os": operating_system, } ) return test_job_datas def get_lint_job_datas(tox_envs: list) -> list: lint_job_datas = [] for tox_env in tox_envs: tox_lint_env_match = _tox_lint_env_regex.match(tox_env) if tox_lint_env_match is None: continue tox_env = tox_lint_env_match.string lint_job_datas.append( { "name": f"{tox_env}", "ui_name": f"{tox_lint_env_match.groupdict()['name']}", "tox_env": tox_env, } ) return lint_job_datas def get_contrib_job_datas(tox_envs: list) -> list: contrib_job_datas = [] for tox_env in tox_envs: tox_contrib_env_match = _tox_contrib_env_regex.match(tox_env) if tox_contrib_env_match is None: continue groups = tox_contrib_env_match.groupdict() tox_env = tox_contrib_env_match.string contrib_requirements = groups["contrib_requirements"] if contrib_requirements is None: contrib_requirements = " " else: contrib_requirements = f"-{contrib_requirements} " contrib_job_datas.append( { "ui_name": (f"{groups['name']}{contrib_requirements}"), "tox_env": tox_env, } ) return contrib_job_datas def get_misc_job_datas(tox_envs: list) -> list: misc_job_datas = [] _tox_benchmark_env_regex = re_compile(r"benchmark.+") for tox_env in tox_envs: if ( _tox_test_env_regex.match(tox_env) is not None or _tox_lint_env_regex.match(tox_env) is not None or _tox_contrib_env_regex.match(tox_env) is not None or _tox_benchmark_env_regex.match(tox_env) is not None ): continue misc_job_datas.append(tox_env) return misc_job_datas def _generate_workflow(job_datas: list, name: str, workflow_directory_path: Path) -> None: env = Environment(loader=FileSystemLoader(Path(__file__).parent)) with open(workflow_directory_path.joinpath(f"{name}.yml"), "w") as yml_file: yml_file.write( env.get_template(f"{name}.yml.j2").render( job_datas=job_datas, ) ) yml_file.write("\n") def generate_test_workflow(tox_ini_path: Path, workflow_directory_path: Path, *operating_systems) -> None: _generate_workflow( get_test_job_datas(get_tox_envs(tox_ini_path), operating_systems), "test", workflow_directory_path, ) def generate_lint_workflow( tox_ini_path: Path, workflow_directory_path: Path, ) -> None: _generate_workflow( get_lint_job_datas(get_tox_envs(tox_ini_path)), "lint", workflow_directory_path, ) def generate_contrib_workflow( workflow_directory_path: Path, ) -> None: _generate_workflow( get_contrib_job_datas(get_tox_envs(Path(__file__).parent.joinpath("tox.ini"))), "core_contrib_test", workflow_directory_path, ) def generate_misc_workflow( tox_ini_path: Path, workflow_directory_path: Path, ) -> None: _generate_workflow( get_misc_job_datas(get_tox_envs(tox_ini_path)), "misc", workflow_directory_path, ) def generate_ci_workflow( workflow_directory_path: Path, ) -> None: with open(workflow_directory_path.joinpath("ci.yml"), "w") as ci_yml_file: ci_yml_file.write( Environment(loader=FileSystemLoader(Path(__file__).parent)).get_template("ci.yml.j2").render() ) ci_yml_file.write("\n") ci.yml.j2000066400000000000000000000015041527070072300365740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/.github/workflows/generate_workflows_lib/src/generate_workflows_lib# Do not edit this file. # This file is generated automatically by executing tox -e generate-workflows name: CI on: push: branches: - 'main' pull_request: merge_group: permissions: contents: read concurrency: group: ${% raw %}{{ github.workflow }}-${{ github.head_ref || github.run_id }}{% endraw %} cancel-in-progress: true jobs: misc: uses: ./.github/workflows/misc.yml lint: uses: ./.github/workflows/lint.yml tests: uses: ./.github/workflows/test.yml check: if: always() needs: - misc - lint - tests runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35 # v1.3.0 with: jobs: ${% raw %}{{ toJSON(needs) }}{% endraw %} core_contrib_test.yml.j2000066400000000000000000000041131527070072300417070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/.github/workflows/generate_workflows_lib/src/generate_workflows_lib# Do not edit this file. # This file is generated automatically by executing tox -e generate-workflows name: Core Contrib Test {{ file_number }} on: workflow_call: inputs: CORE_REPO_SHA: required: true type: string CONTRIB_REPO_SHA: required: true type: string permissions: contents: read env: CORE_REPO_SHA: ${% raw %}{{ inputs.CORE_REPO_SHA }}{% endraw %} CONTRIB_REPO_SHA: ${% raw %}{{ inputs.CONTRIB_REPO_SHA }}{% endraw %} PIP_EXISTS_ACTION: w CORE_REPO_API: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python/opentelemetry-api CORE_REPO_SDK: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python/opentelemetry-sdk CORE_REPO_SEMCONV: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python/opentelemetry-semantic-conventions CORE_REPO_TEST_UTILS: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python/tests/opentelemetry-test-utils jobs: {%- for job_data in job_datas %} {{ job_data.tox_env }}: name: {{ job_data.ui_name }} runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout contrib repo @ SHA - ${% raw %}{{ env.CONTRIB_REPO_SHA }}{% endraw %} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python-contrib ref: ${% raw %}{{ env.CONTRIB_REPO_SHA }}{% endraw %} - name: Checkout core repo @ SHA - ${% raw %}{{ env.CORE_REPO_SHA }}{% endraw %} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: open-telemetry/opentelemetry-python ref: ${% raw %}{{ env.CORE_REPO_SHA }}{% endraw %} path: opentelemetry-python - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x64" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e {{ job_data.tox_env }} -- -ra {%- endfor %} lint.yml.j2000066400000000000000000000027671527070072300371630ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/.github/workflows/generate_workflows_lib/src/generate_workflows_lib# Do not edit this file. # This file is generated automatically by executing tox -e generate-workflows name: Lint on: workflow_call: permissions: contents: read env: # Set the SHA to the branch name if the PR has a label 'prepare-release' or 'backport' otherwise, set it to 'main' # For PRs you can change the inner fallback ('main') # For pushes you change the outer fallback ('main') # The logic below is used during releases and depends on having an equivalent branch name in the core repo. CORE_REPO_SHA: {% raw %}${{ github.event_name == 'pull_request' && ( contains(github.event.pull_request.labels.*.name, 'prepare-release') && github.event.pull_request.head.ref || contains(github.event.pull_request.labels.*.name, 'backport') && github.event.pull_request.base.ref || 'main' ) || 'main' }}{% endraw %} CONTRIB_REPO_SHA: main PIP_EXISTS_ACTION: w jobs: {%- for job_data in job_datas %} {{ job_data.name }}: name: {{ job_data.ui_name }} runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${% raw %}{{ github.sha }}{% endraw %} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e {{ job_data.tox_env }} {%- endfor %} misc.yml.j2000066400000000000000000000063771527070072300371510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/.github/workflows/generate_workflows_lib/src/generate_workflows_lib# Do not edit this file. # This file is generated automatically by executing tox -e generate-workflows name: Misc on: workflow_call: permissions: contents: read env: # Set the SHA to the branch name if the PR has a label 'prepare-release' or 'backport' otherwise, set it to 'main' # For PRs you can change the inner fallback ('main') # For pushes you change the outer fallback ('main') # The logic below is used during releases and depends on having an equivalent branch name in the core repo. CORE_REPO_SHA: {% raw %}${{ github.event_name == 'pull_request' && ( contains(github.event.pull_request.labels.*.name, 'prepare-release') && github.event.pull_request.head.ref || contains(github.event.pull_request.labels.*.name, 'backport') && github.event.pull_request.base.ref || 'main' ) || 'main' }}{% endraw %} CONTRIB_REPO_SHA: main PIP_EXISTS_ACTION: w jobs: {%- for job_data in job_datas %} {{ job_data }}: name: {{ job_data }} runs-on: ubuntu-latest timeout-minutes: 30 {%- if job_data == "generate-workflows" %} if: | !contains(github.event.pull_request.labels.*.name, 'Skip generate-workflows') && github.event.pull_request.user.login != 'otelbot[bot]' && github.event_name == 'pull_request' {%- endif %} {%- if job_data == "public-symbols-check" %} if: | !contains(github.event.pull_request.labels.*.name, 'Approve Public API check') && github.actor != 'otelbot[bot]' && github.event_name == 'pull_request' {%- endif %} {%- if job_data == "docs" %} if: | github.event_name == 'merge_group' || (github.event_name == 'pull_request' && github.event.pull_request.user.login != 'otelbot[bot]') {%- endif %} steps: - name: Checkout repo @ SHA - ${% raw %}{{ github.sha }}{% endraw %} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 {%- if job_data == "public-symbols-check" %} with: fetch-depth: 0 - name: Checkout main run: git checkout main - name: Pull origin run: git pull --rebase=false origin main - name: Checkout pull request run: git checkout ${% raw %}{{ github.event.pull_request.head.sha }}{% endraw %} {%- endif %} - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv {%- if job_data == "docker-tests" %} - name: Install Microsoft SQL ODBC driver run: sudo apt update -y && ACCEPT_EULA=Y sudo apt install -y msodbcsql18 unixodbc-dev unixodbc {%- endif %} - name: Run tests run: tox -e {{ job_data }} {%- if job_data == "generate-workflows" %} - name: Check workflows are up to date run: git diff --exit-code || (echo 'Generated workflows are out of date, run "tox -e generate-workflows" and commit the changes in this PR.' && exit 1) {%- endif %} {%- if job_data == "generate" %} - name: Check workflows are up to date run: git diff --exit-code || (echo 'Generated code is out of date, run "tox -e generate" and commit the changes in this PR.' && exit 1) {%- endif %} {%- endfor %} test.yml.j2000066400000000000000000000033431527070072300371630ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/.github/workflows/generate_workflows_lib/src/generate_workflows_lib# Do not edit this file. # This file is generated automatically by executing tox -e generate-workflows name: Test on: workflow_call: permissions: contents: read env: # Set the SHA to the branch name if the PR has a label 'prepare-release' or 'backport' otherwise, set it to 'main' # For PRs you can change the inner fallback ('main') # For pushes you change the outer fallback ('main') # The logic below is used during releases and depends on having an equivalent branch name in the core repo. CORE_REPO_SHA: {% raw %}${{ github.event_name == 'pull_request' && ( contains(github.event.pull_request.labels.*.name, 'prepare-release') && github.event.pull_request.head.ref || contains(github.event.pull_request.labels.*.name, 'backport') && github.event.pull_request.base.ref || 'main' ) || 'main' }}{% endraw %} CONTRIB_REPO_SHA: main PIP_EXISTS_ACTION: w jobs: {%- for job_data in job_datas %} {{ job_data.name }}: name: {{ job_data.ui_name }} runs-on: {{ job_data.os }} timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${% raw %}{{ github.sha }}{% endraw %} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python {{ job_data.python_version }} uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "{{ job_data.python_version }}" - name: Install tox run: pip install tox-uv {%- if job_data.os == "windows-latest" %} - name: Configure git to support long filenames run: git config --system core.longpaths true {%- endif %} - name: Run tests run: tox -e {{ job_data.tox_env }} -- -ra {%- endfor %} version.py000066400000000000000000000000261527070072300372010ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/.github/workflows/generate_workflows_lib/src/generate_workflows_lib__version__ = "0.1.0" opentelemetry-python-contrib-0.66b2/.github/workflows/labeler.yml000066400000000000000000000004761527070072300253020ustar00rootroot00000000000000name: PR Labeler on: pull_request_target: permissions: contents: read jobs: label: runs-on: ubuntu-latest permissions: contents: read pull-requests: write steps: - uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0 with: sync-labels: false opentelemetry-python-contrib-0.66b2/.github/workflows/lint.yml000066400000000000000000001165651527070072300246510ustar00rootroot00000000000000# Do not edit this file. # This file is generated automatically by executing tox -e generate-workflows name: Lint on: workflow_call: permissions: contents: read env: # Set the SHA to the branch name if the PR has a label 'prepare-release' or 'backport' otherwise, set it to 'main' # For PRs you can change the inner fallback ('main') # For pushes you change the outer fallback ('main') # The logic below is used during releases and depends on having an equivalent branch name in the core repo. CORE_REPO_SHA: ${{ github.event_name == 'pull_request' && ( contains(github.event.pull_request.labels.*.name, 'prepare-release') && github.event.pull_request.head.ref || contains(github.event.pull_request.labels.*.name, 'backport') && github.event.pull_request.base.ref || 'main' ) || 'main' }} CONTRIB_REPO_SHA: main PIP_EXISTS_ACTION: w jobs: lint-instrumentation-openai-v2: name: instrumentation-openai-v2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-openai-v2 lint-instrumentation-openai_agents-v2: name: instrumentation-openai_agents-v2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-openai_agents-v2 lint-instrumentation-vertexai: name: instrumentation-vertexai runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-vertexai lint-resource-detector-containerid: name: resource-detector-containerid runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-resource-detector-containerid lint-resource-detector-azure: name: resource-detector-azure runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-resource-detector-azure lint-resourcedetector-gcp: name: resourcedetector-gcp runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-resourcedetector-gcp lint-sdk-extension-aws: name: sdk-extension-aws runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-sdk-extension-aws lint-distro: name: distro runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-distro lint-opentelemetry-instrumentation: name: opentelemetry-instrumentation runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-opentelemetry-instrumentation lint-instrumentation-aiohttp-client: name: instrumentation-aiohttp-client runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-aiohttp-client lint-instrumentation-aiohttp-server: name: instrumentation-aiohttp-server runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-aiohttp-server lint-instrumentation-aiopg: name: instrumentation-aiopg runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-aiopg lint-instrumentation-aws-lambda: name: instrumentation-aws-lambda runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-aws-lambda lint-instrumentation-botocore: name: instrumentation-botocore runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-botocore lint-instrumentation-boto3sqs: name: instrumentation-boto3sqs runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-boto3sqs lint-instrumentation-django: name: instrumentation-django runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-django lint-instrumentation-dbapi: name: instrumentation-dbapi runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-dbapi lint-instrumentation-asyncclick: name: instrumentation-asyncclick runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-asyncclick lint-instrumentation-click: name: instrumentation-click runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-click lint-instrumentation-falcon: name: instrumentation-falcon runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-falcon lint-instrumentation-fastapi: name: instrumentation-fastapi runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-fastapi lint-instrumentation-flask: name: instrumentation-flask runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-flask lint-instrumentation-urllib: name: instrumentation-urllib runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-urllib lint-instrumentation-urllib3: name: instrumentation-urllib3 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-urllib3 lint-instrumentation-requests: name: instrumentation-requests runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-requests lint-instrumentation-starlette: name: instrumentation-starlette runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-starlette lint-instrumentation-structlog: name: instrumentation-structlog runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-structlog lint-instrumentation-jinja2: name: instrumentation-jinja2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-jinja2 lint-instrumentation-logging: name: instrumentation-logging runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-logging lint-instrumentation-exceptions: name: instrumentation-exceptions runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-exceptions lint-exporter-richconsole: name: exporter-richconsole runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-exporter-richconsole lint-exporter-prometheus-remote-write: name: exporter-prometheus-remote-write runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-exporter-prometheus-remote-write lint-instrumentation-mysql: name: instrumentation-mysql runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-mysql lint-instrumentation-mysqlclient: name: instrumentation-mysqlclient runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-mysqlclient lint-instrumentation-psycopg2: name: instrumentation-psycopg2 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-psycopg2 lint-instrumentation-psycopg: name: instrumentation-psycopg runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-psycopg lint-instrumentation-pymemcache: name: instrumentation-pymemcache runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-pymemcache lint-instrumentation-pymongo: name: instrumentation-pymongo runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-pymongo lint-instrumentation-pymysql: name: instrumentation-pymysql runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-pymysql lint-instrumentation-pymssql: name: instrumentation-pymssql runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-pymssql lint-instrumentation-pyramid: name: instrumentation-pyramid runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-pyramid lint-instrumentation-asgi: name: instrumentation-asgi runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-asgi lint-instrumentation-asyncpg: name: instrumentation-asyncpg runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-asyncpg lint-instrumentation-sqlite3: name: instrumentation-sqlite3 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-sqlite3 lint-instrumentation-wsgi: name: instrumentation-wsgi runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-wsgi lint-instrumentation-grpc: name: instrumentation-grpc runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-grpc lint-instrumentation-sqlalchemy: name: instrumentation-sqlalchemy runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-sqlalchemy lint-instrumentation-redis: name: instrumentation-redis runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-redis lint-instrumentation-remoulade: name: instrumentation-remoulade runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-remoulade lint-instrumentation-celery: name: instrumentation-celery runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-celery lint-instrumentation-system-metrics: name: instrumentation-system-metrics runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-system-metrics lint-instrumentation-threading: name: instrumentation-threading runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-threading lint-instrumentation-tornado: name: instrumentation-tornado runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-tornado lint-instrumentation-tortoiseorm: name: instrumentation-tortoiseorm runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-tortoiseorm lint-instrumentation-httpx: name: instrumentation-httpx runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-httpx lint-util-http: name: util-http runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-util-http lint-exporter-credential-provider-gcp: name: exporter-credential-provider-gcp runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-exporter-credential-provider-gcp lint-util-genai: name: util-genai runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-util-genai lint-propagator-aws-xray: name: propagator-aws-xray runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-propagator-aws-xray lint-propagator-ot-trace: name: propagator-ot-trace runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-propagator-ot-trace lint-instrumentation-sio-pika: name: instrumentation-sio-pika runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-sio-pika lint-instrumentation-aio-pika: name: instrumentation-aio-pika runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-aio-pika lint-instrumentation-aiokafka: name: instrumentation-aiokafka runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-aiokafka lint-instrumentation-kafka-python: name: instrumentation-kafka-python runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-kafka-python lint-instrumentation-confluent-kafka: name: instrumentation-confluent-kafka runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-confluent-kafka lint-instrumentation-asyncio: name: instrumentation-asyncio runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-asyncio lint-instrumentation-cassandra: name: instrumentation-cassandra runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-instrumentation-cassandra lint-processor-baggage: name: processor-baggage runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-processor-baggage lint-opamp-client: name: opamp-client runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-opamp-client lint-license-header-check: name: license-header-check runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e lint-license-header-check opentelemetry-python-contrib-0.66b2/.github/workflows/misc.yml000066400000000000000000000134221527070072300246220ustar00rootroot00000000000000# Do not edit this file. # This file is generated automatically by executing tox -e generate-workflows name: Misc on: workflow_call: permissions: contents: read env: # Set the SHA to the branch name if the PR has a label 'prepare-release' or 'backport' otherwise, set it to 'main' # For PRs you can change the inner fallback ('main') # For pushes you change the outer fallback ('main') # The logic below is used during releases and depends on having an equivalent branch name in the core repo. CORE_REPO_SHA: ${{ github.event_name == 'pull_request' && ( contains(github.event.pull_request.labels.*.name, 'prepare-release') && github.event.pull_request.head.ref || contains(github.event.pull_request.labels.*.name, 'backport') && github.event.pull_request.base.ref || 'main' ) || 'main' }} CONTRIB_REPO_SHA: main PIP_EXISTS_ACTION: w jobs: spellcheck: name: spellcheck runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e spellcheck docker-tests: name: docker-tests runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Install Microsoft SQL ODBC driver run: sudo apt update -y && ACCEPT_EULA=Y sudo apt install -y msodbcsql18 unixodbc-dev unixodbc - name: Run tests run: tox -e docker-tests docs: name: docs runs-on: ubuntu-latest timeout-minutes: 30 if: | github.event_name == 'merge_group' || (github.event_name == 'pull_request' && github.event.pull_request.user.login != 'otelbot[bot]') steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e docs generate: name: generate runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e generate - name: Check workflows are up to date run: git diff --exit-code || (echo 'Generated code is out of date, run "tox -e generate" and commit the changes in this PR.' && exit 1) generate-workflows: name: generate-workflows runs-on: ubuntu-latest timeout-minutes: 30 if: | !contains(github.event.pull_request.labels.*.name, 'Skip generate-workflows') && github.event.pull_request.user.login != 'otelbot[bot]' && github.event_name == 'pull_request' steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e generate-workflows - name: Check workflows are up to date run: git diff --exit-code || (echo 'Generated workflows are out of date, run "tox -e generate-workflows" and commit the changes in this PR.' && exit 1) shellcheck: name: shellcheck runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e shellcheck precommit: name: precommit runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e precommit typecheck: name: typecheck runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e typecheck opentelemetry-python-contrib-0.66b2/.github/workflows/ossf-scorecard.yml000066400000000000000000000007401527070072300266030ustar00rootroot00000000000000name: OSSF Scorecard on: push: branches: - main schedule: - cron: "10 6 * * 1" # once a week workflow_dispatch: permissions: {} jobs: analysis: permissions: contents: read # for actions/checkout id-token: write # for Scorecard to publish results security-events: write # for the SARIF upload to code scanning uses: open-telemetry/shared-workflows/.github/workflows/scorecard.yml@f13d0cb656d7244ca8c1638b6996dbaef1083bdd # v0.6.0 opentelemetry-python-contrib-0.66b2/.github/workflows/package-prepare-patch-release.yml000066400000000000000000000116101527070072300314260ustar00rootroot00000000000000name: "[Package] Prepare patch release" on: workflow_dispatch: inputs: package: type: choice options: - opentelemetry-opamp-client - opentelemetry-propagator-aws-xray - opentelemetry-resource-detector-azure - opentelemetry-resourcedetector-gcp - opentelemetry-sdk-extension-aws - opentelemetry-instrumentation-openai-v2 - opentelemetry-instrumentation-openai-agents-v2 - opentelemetry-instrumentation-vertexai - opentelemetry-instrumentation-valkey-py - opentelemetry-util-genai description: 'Package to be released' required: true permissions: contents: read run-name: "[Package][${{ inputs.package }}] Prepare patch release" jobs: prepare-patch-release: permissions: contents: write # required for pushing branches pull-requests: write # required for creating pull requests runs-on: ubuntu-latest steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Verify prerequisites run: | if [[ $GITHUB_REF_NAME != package-release/${{ inputs.package }}/v* ]]; then echo this workflow should only be run against package-release/${{ inputs.package }}* branches, but is running on $GITHUB_REF_NAME exit 1 fi path=./$(./scripts/eachdist.py find-package --package ${{ inputs.package }}) changelog=$path/CHANGELOG.md if [ ! -f $changelog ]; then echo "missing $changelog file" exit 1 fi version=$(./scripts/eachdist.py version --package ${{ inputs.package }}) version_file=$(find $path -type f -path "**/version.py") file_count=$(echo "$version_file" | wc -l) if [ "$file_count" -ne 1 ]; then echo "Error: expected one version file, found $file_count" echo "$version_file" exit 1 fi if [[ $version =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then # 1.2.3 or 1.2.3rc1 major="${BASH_REMATCH[1]}" minor="${BASH_REMATCH[2]}" patch="${BASH_REMATCH[3]}" next_version="$major.$minor.$((patch + 1))" release_branch_name="package-release/${{ inputs.package }}/v$major.$minor.x" elif [[ $version =~ ^([0-9]+)\.([0-9]+)b([0-9]+)$ ]]; then # 0.1b1 major="${BASH_REMATCH[1]}" minor="${BASH_REMATCH[2]}" patch="${BASH_REMATCH[3]}" next_version="$major.${minor}b$((patch + 1))" release_branch_name="package-release/${{ inputs.package }}/v$major.${minor}bx" else echo "unexpected version: '$version'" exit 1 fi if [[ $GITHUB_REF_NAME != $release_branch_name ]]; then echo this workflow should only be run against $release_branch_name branch, but is running on $GITHUB_REF_NAME exit 1 fi echo "PACKAGE_NAME=${{ inputs.package }}" >> $GITHUB_ENV echo "VERSION=$version" >> $GITHUB_ENV echo "NEXT_VERSION=$next_version" >> $GITHUB_ENV echo "CHANGELOG=$changelog" >> $GITHUB_ENV echo "VERSION_FILE=$version_file" >> $GITHUB_ENV - name: Update version run: | # replace the version in the version file (1.2.3 -> 1.2.4) sed -i -E "s/__version__\s*=\s*\"${VERSION}\"/__version__ = \"${NEXT_VERSION}\"/g" $VERSION_FILE - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.10' - name: Install tox run: pip install tox - name: run tox run: tox -e generate - name: Install towncrier run: pip install towncrier==25.8.0 - name: Generate changelog run: towncrier build --yes --version "$NEXT_VERSION" --dir "$(dirname $CHANGELOG)" - name: Use CLA approved github bot run: .github/scripts/use-cla-approved-github-bot.sh - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 id: otelbot-token with: client-id: ${{ vars.OTELBOT_CLIENT_ID }} private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} - name: Create pull request env: # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} run: | message="Prepare patch release for ${PACKAGE_NAME} v${NEXT_VERSION}" branch="otelbot/patch-${PACKAGE_NAME}-version-to-v${NEXT_VERSION}" git commit -a -m "$message" git push origin HEAD:$branch gh pr create --title "[$GITHUB_REF_NAME] $message" \ --body "$message." \ --head $branch \ --base $GITHUB_REF_NAME opentelemetry-python-contrib-0.66b2/.github/workflows/package-prepare-release.yml000066400000000000000000000202201527070072300303260ustar00rootroot00000000000000name: "[Package] Prepare release" on: workflow_dispatch: inputs: package: type: choice options: - opentelemetry-opamp-client - opentelemetry-propagator-aws-xray - opentelemetry-resource-detector-azure - opentelemetry-resourcedetector-gcp - opentelemetry-sdk-extension-aws - opentelemetry-instrumentation-valkey-py - opentelemetry-util-genai description: 'Package to be released' required: true permissions: contents: read run-name: "[Package][${{ inputs.package }}] Prepare release" jobs: prereqs: runs-on: ubuntu-latest outputs: version: ${{ steps.verify.outputs.version }} changelog: ${{ steps.verify.outputs.changelog }} version_file: ${{ steps.verify.outputs.version_file }} release_branch_name: ${{ steps.verify.outputs.release_branch_name }} next_version: ${{ steps.verify.outputs.next_version }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 - id: verify name: Verify prerequisites run: | if [[ $GITHUB_REF_NAME != main ]]; then echo this workflow should only be run against main exit 1 fi path=./$(./scripts/eachdist.py find-package --package ${{ inputs.package }}) changelog=$path/CHANGELOG.md if [ ! -f $changelog ]; then echo "missing $changelog file" exit 1 fi version_dev=$(./scripts/eachdist.py version --package ${{ inputs.package }}) if [[ ! $version_dev =~ ^([0-9]+)\.([0-9]+)[\.|b]{1}([0-9]+).*.dev$ ]]; then echo "unexpected version: $version" exit 1 fi version=${version_dev%.dev} version_file=$(find $path -type f -path "**/version.py") file_count=$(echo "$version_file" | wc -l) if [ "$file_count" -ne 1 ]; then echo "Error: expected one version file, found $file_count" echo "$version_file" exit 1 fi if [[ $version =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then # 1.2.3 or 1.2.3rc1 major="${BASH_REMATCH[1]}" minor="${BASH_REMATCH[2]}" release_branch_name="package-release/${{ inputs.package }}/v$major.$minor.x" next_version="$major.$((minor + 1)).0" elif [[ $version =~ ^([0-9]+)\.([0-9]+)b([0-9]+)$ ]]; then # 0.1b1 major="${BASH_REMATCH[1]}" minor="${BASH_REMATCH[2]}" release_branch_name="package-release/${{ inputs.package }}/v$major.${minor}bx" next_version="$major.$((minor + 1))b0" else echo "unexpected version: '$version'" exit 1 fi echo "version=$version" >> $GITHUB_OUTPUT echo "changelog=$changelog" >> $GITHUB_OUTPUT echo "version_file=$version_file" >> $GITHUB_OUTPUT echo "release_branch_name=$release_branch_name" >> $GITHUB_OUTPUT echo "next_version=$next_version" >> $GITHUB_OUTPUT create-pull-request-against-release-branch: runs-on: ubuntu-latest needs: prereqs permissions: contents: write # required for pushing branches pull-requests: write # required for creating pull requests steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set environment variables run: | echo "RELEASE_BRANCH_NAME=${{ needs.prereqs.outputs.release_branch_name }}" >> $GITHUB_ENV echo "VERSION=${{ needs.prereqs.outputs.version }}" >> $GITHUB_ENV echo "CHANGELOG=${{ needs.prereqs.outputs.changelog }}" >> $GITHUB_ENV echo "VERSION_FILE=${{ needs.prereqs.outputs.version_file }}" >> $GITHUB_ENV echo "PACKAGE_NAME=${{ github.event.inputs.package }}" >> $GITHUB_ENV - name: Create package release branch run: | git push origin HEAD:$RELEASE_BRANCH_NAME - name: Update package version run: | # replace the version in the version file (1.2.3dev -> 1.2.3) sed -i -E "s/__version__\s*=\s*\"${VERSION}\.dev\"/__version__ = \"${VERSION}\"/g" $VERSION_FILE - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.10' - name: Install tox run: pip install tox - name: run tox run: tox -e generate - name: Install towncrier run: pip install towncrier==25.8.0 - name: Generate changelog run: towncrier build --yes --version "$VERSION" --dir "$(dirname $CHANGELOG)" - name: Use CLA approved github bot run: .github/scripts/use-cla-approved-github-bot.sh - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 id: otelbot-token with: client-id: ${{ vars.OTELBOT_CLIENT_ID }} private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} - name: Create pull request against the release branch env: # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} run: | message="Prepare release for ${PACKAGE_NAME} v${VERSION}" branch="otelbot/prepare-${RELEASE_BRANCH_NAME}" git commit -a -m "$message" git push origin HEAD:$branch gh pr create --title "[$RELEASE_BRANCH_NAME] $message" \ --body "$message." \ --head $branch \ --base $RELEASE_BRANCH_NAME create-pull-request-against-main: runs-on: ubuntu-latest needs: prereqs permissions: contents: write # required for pushing branches pull-requests: write # required for creating pull requests steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set environment variables run: | echo "PACKAGE_NAME=${{ github.event.inputs.package }}" >> $GITHUB_ENV echo "VERSION=${{ needs.prereqs.outputs.version }}" >> $GITHUB_ENV echo "VERSION_FILE=${{ needs.prereqs.outputs.version_file }}" >> $GITHUB_ENV echo "NEXT_VERSION=${{ needs.prereqs.outputs.next_version }}" >> $GITHUB_ENV echo "CHANGELOG=${{ needs.prereqs.outputs.changelog }}" >> $GITHUB_ENV - name: Update version run: | # replace the version in the version file (1.2.3dev -> 1.3.3.dev) sed -i -E "s/__version__\s*=\s*\"${VERSION}\.dev\"/__version__ = \"${NEXT_VERSION}.dev\"/g" $VERSION_FILE - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.10' - name: Install tox run: pip install tox - name: run tox run: tox -e generate - name: Install towncrier run: pip install towncrier==25.8.0 - name: Generate changelog run: towncrier build --yes --version "$VERSION" --dir "$(dirname $CHANGELOG)" - name: Use CLA approved github bot run: .github/scripts/use-cla-approved-github-bot.sh - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 id: otelbot-token-main with: client-id: ${{ vars.OTELBOT_CLIENT_ID }} private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} - name: Create pull request against main env: # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows GITHUB_TOKEN: ${{ steps.otelbot-token-main.outputs.token }} run: | message="Update ${PACKAGE_NAME} version to v${NEXT_VERSION}" body="Update \`${PACKAGE_NAME}\` version to v\`${NEXT_VERSION}\`." branch="otelbot/update-${PACKAGE_NAME}-version-to-v${NEXT_VERSION}" git commit -a -m "$message" git push origin HEAD:$branch gh pr create --title "$message" \ --body "$body" \ --head $branch \ --base main opentelemetry-python-contrib-0.66b2/.github/workflows/package-release.yml000066400000000000000000000151541527070072300267040ustar00rootroot00000000000000name: "[Package] Release" on: workflow_dispatch: inputs: package: type: choice options: - opentelemetry-opamp-client - opentelemetry-propagator-aws-xray - opentelemetry-resource-detector-azure - opentelemetry-resourcedetector-gcp - opentelemetry-sdk-extension-aws - opentelemetry-instrumentation-openai-v2 - opentelemetry-instrumentation-openai-agents-v2 - opentelemetry-instrumentation-vertexai - opentelemetry-instrumentation-valkey-py - opentelemetry-util-genai description: 'Package to be released' required: true permissions: contents: read run-name: "[Package][${{ inputs.package }}] Release" jobs: release: permissions: contents: write # required for creating releases pull-requests: write # required for creating pull requests runs-on: ubuntu-latest steps: - run: | if [[ $GITHUB_REF_NAME != package-release/${{ inputs.package }}* ]]; then echo this workflow should only be run against package-release/${{ inputs.package }}* branches exit 1 fi - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set environment variables run: | version=$(./scripts/eachdist.py version --package ${{ inputs.package }}) if [[ $version =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then major="${BASH_REMATCH[1]}" minor="${BASH_REMATCH[2]}" patch="${BASH_REMATCH[3]}" if [[ $patch != 0 ]]; then prior_version_when_patch="${major}.${minor}.$((patch - 1))" fi elif [[ $version =~ ^([0-9]+)\.([0-9]+)b([0-9]+)$ ]]; then major="${BASH_REMATCH[1]}" minor="${BASH_REMATCH[2]}" patch="${BASH_REMATCH[3]}" if [[ $patch != 0 ]]; then prior_version_when_patch="${major}.${minor}b$((patch - 1))" fi else echo "unexpected version: $version" exit 1 fi path=$(./scripts/eachdist.py find-package --package ${{ inputs.package }}) echo "CHANGELOG=./$path/CHANGELOG.md" >> $GITHUB_ENV echo "PACKAGE_NAME=${{ inputs.package }}" >> $GITHUB_ENV echo "VERSION=$version" >> $GITHUB_ENV echo "RELEASE_TAG=${{ inputs.package }}==$version" >> $GITHUB_ENV echo "PRIOR_VERSION_WHEN_PATCH=$prior_version_when_patch" >> $GITHUB_ENV # check out main branch to verify there won't be problems with merging the change log # at the end of this workflow - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: ref: main - name: Check that change log update was merged to main run: | if [[ -z $PRIOR_VERSION_WHEN_PATCH ]]; then # not making a patch release if ! grep --quiet "^## Version ${VERSION}" ${CHANGELOG}; then echo the pull request generated by prepare-release-branch.yml needs to be merged first exit 1 fi fi # back to the release branch - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 # next few steps publish to pypi - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.10' - name: Build wheels run: ./scripts/build_a_package.sh - name: Install twine run: | pip install twine # The step below publishes to testpypi in order to catch any issues # with the package configuration that would cause a failure to upload # to pypi. One example of such a failure is if a classifier is # rejected by pypi (e.g "3 - Beta"). This would cause a failure during the # middle of the package upload causing the action to fail, and certain packages # might have already been updated, this would be bad. # EDIT: 5/31/2024 - TestPypi now requires a verified email. Commenting out as a temporary measure # until we found TestPypi credentials. # - name: Publish to TestPyPI # env: # TWINE_USERNAME: '__token__' # TWINE_PASSWORD: ${{ secrets.test_pypi_token }} # run: | # twine upload --repository testpypi --skip-existing --verbose dist/* - name: Publish to PyPI env: TWINE_USERNAME: '__token__' TWINE_PASSWORD: ${{ secrets.pypi_password }} run: | twine upload --skip-existing --verbose dist/* - name: Generate release notes env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: ./scripts/generate_release_notes.sh - name: Create GitHub release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release create --target $GITHUB_REF_NAME \ --title "${PACKAGE_NAME} ${VERSION}" \ --notes-file /tmp/release-notes.txt \ --discussion-category announcements \ $RELEASE_TAG - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: ref: main - name: Copy change log updates to main env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: ./scripts/merge_changelog_to_main.sh - name: Use CLA approved github bot run: .github/scripts/use-cla-approved-github-bot.sh - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 id: otelbot-token with: client-id: ${{ vars.OTELBOT_CLIENT_ID }} private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} - name: Create pull request against main env: # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} run: | message="Copy changelog updates from $GITHUB_REF_NAME" body="Copy changelog updates from \`$GITHUB_REF_NAME\`." branch="otelbot/changelog-${GITHUB_REF_NAME//\//-}" if [[ -z $PRIOR_VERSION_WHEN_PATCH ]]; then if git diff --quiet; then echo there are no updates needed to the change log on main, not creating pull request exit 0 # success fi fi git commit -a -m "$message" git push origin HEAD:$branch gh pr create --title "$message" \ --body "$body" \ --head $branch \ --base main opentelemetry-python-contrib-0.66b2/.github/workflows/prepare-patch-release.yml000066400000000000000000000124251527070072300300420ustar00rootroot00000000000000name: Prepare patch release on: workflow_dispatch: permissions: contents: read jobs: prepare-patch-release: permissions: contents: write # required for pushing branches pull-requests: write # required for creating and editing pull requests runs-on: ubuntu-latest steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - run: | if [[ ! $GITHUB_REF_NAME =~ ^release/v[0-9]+\.[0-9]+\.x-0\.[0-9]+bx$ ]]; then echo this workflow should only be run against long-term release branches exit 1 fi - name: Set environment variables run: | stable_version=$(./scripts/eachdist.py version --mode stable) unstable_version=$(./scripts/eachdist.py version --mode prerelease) if [[ $stable_version =~ ^([0-9]+\.[0-9]+)\.([0-9]+)$ ]]; then stable_major_minor="${BASH_REMATCH[1]}" stable_patch="${BASH_REMATCH[2]}" else echo "unexpected stable_version: $stable_version" exit 1 fi if [[ $unstable_version =~ ^0\.([0-9]+)b([0-9]+)$ ]]; then unstable_minor="${BASH_REMATCH[1]}" unstable_patch="${BASH_REMATCH[2]}" else echo "unexpected unstable_version: $unstable_version" exit 1 fi stable_version_prev="$stable_major_minor.$((stable_patch))" unstable_version_prev="0.${unstable_minor}b$((unstable_patch))" stable_version="$stable_major_minor.$((stable_patch + 1))" unstable_version="0.${unstable_minor}b$((unstable_patch + 1))" echo "STABLE_VERSION=$stable_version" >> $GITHUB_ENV echo "UNSTABLE_VERSION=$unstable_version" >> $GITHUB_ENV echo "STABLE_VERSION_PREV=$stable_version_prev" >> $GITHUB_ENV echo "UNSTABLE_VERSION_PREV=$unstable_version_prev" >> $GITHUB_ENV - name: Update version run: .github/scripts/update-version-patch.sh $STABLE_VERSION $UNSTABLE_VERSION $STABLE_VERSION_PREV $UNSTABLE_VERSION_PREV - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.10' - name: Install tox run: pip install tox - name: run tox run: tox -e generate - name: Install towncrier run: pip install towncrier==25.8.0 - name: Generate changelog run: towncrier build --yes --version "$STABLE_VERSION/$UNSTABLE_VERSION" - name: Use CLA approved github bot run: .github/scripts/use-cla-approved-github-bot.sh - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 id: otelbot-token with: client-id: ${{ vars.OTELBOT_CLIENT_ID }} private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} - name: Create pull request id: create_pr env: # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} run: | message="Prepare release ${STABLE_VERSION}/${UNSTABLE_VERSION}" branch="otelbot/prepare-release-${STABLE_VERSION}-${UNSTABLE_VERSION}" git commit -a -m "$message" git push origin HEAD:$branch pr_url=$(gh pr create --title "[$GITHUB_REF_NAME] $message" \ --body "$message." \ --head $branch \ --base $GITHUB_REF_NAME) echo "pr_url=$pr_url" >> $GITHUB_OUTPUT - name: Add prepare-release label to PR if: steps.create_pr.outputs.pr_url != '' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh pr edit ${{ steps.create_pr.outputs.pr_url }} --add-label "prepare-release" - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: ref: main - name: Use CLA approved github bot run: .github/scripts/use-cla-approved-github-bot.sh - name: Backport patch release changelog to main id: backport_pr env: GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} run: | release_branch="otelbot/prepare-release-${STABLE_VERSION}-${UNSTABLE_VERSION}" message="Backport ${STABLE_VERSION}/${UNSTABLE_VERSION} changelog" body="Backport \`${STABLE_VERSION}/${UNSTABLE_VERSION}\` changelog" branch="otelbot/backport-changelog-from-${STABLE_VERSION}-${UNSTABLE_VERSION}" git fetch origin $release_branch # Copy the updated CHANGELOG.md from the release branch git checkout FETCH_HEAD -- CHANGELOG.md git commit -m "$message" git push origin HEAD:$branch pr_url=$(gh pr create --title "$message" \ --body "$body" \ --head $branch \ --base main) echo "pr_url=$pr_url" >> $GITHUB_OUTPUT - name: Add Skip Changelog label to backport PR if: steps.backport_pr.outputs.pr_url != '' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh pr edit ${{ steps.backport_pr.outputs.pr_url }} --add-label "Skip Changelog" opentelemetry-python-contrib-0.66b2/.github/workflows/prepare-release-branch.yml000066400000000000000000000214111527070072300301730ustar00rootroot00000000000000name: Prepare release branch on: workflow_dispatch: inputs: prerelease_version: description: "Pre-release version number? (e.g. 1.9.0rc2)" required: false permissions: contents: read jobs: prereqs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Verify prerequisites env: PRERELEASE_VERSION: ${{ github.event.inputs.prerelease_version }} run: | if [[ $GITHUB_REF_NAME != main ]]; then echo this workflow should only be run against main exit 1 fi if [[ ! -z $PRERELEASE_VERSION ]]; then stable_version=$(./scripts/eachdist.py version --mode stable) stable_version=${stable_version//.dev/} if [[ $PRERELEASE_VERSION != ${stable_version}* ]]; then echo "$PRERELEASE_VERSION is not a prerelease for the version on main ($stable_version)" exit 1 fi fi create-pull-request-against-release-branch: runs-on: ubuntu-latest needs: prereqs permissions: contents: write # required for pushing branches pull-requests: write # required for creating and editing pull requests steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Create release branch env: PRERELEASE_VERSION: ${{ github.event.inputs.prerelease_version }} run: | if [[ -z $PRERELEASE_VERSION ]]; then stable_version=$(./scripts/eachdist.py version --mode stable) stable_version=${stable_version//.dev/} else stable_version=$PRERELEASE_VERSION fi unstable_version=$(./scripts/eachdist.py version --mode prerelease) unstable_version=${unstable_version//.dev/} if [[ $stable_version =~ ^([0-9]+)\.([0-9]+)\.0$ ]]; then stable_version_branch_part=$(echo $stable_version | sed -E 's/([0-9]+)\.([0-9]+)\.0/\1.\2.x/') unstable_version_branch_part=$(echo $unstable_version | sed -E 's/0\.([0-9]+)b0/0.\1bx/') release_branch_name="release/v${stable_version_branch_part}-${unstable_version_branch_part}" elif [[ $stable_version =~ ^([0-9]+)\.([0-9]+)\.0 ]]; then # pre-release version, e.g. 1.9.0rc2 release_branch_name="release/v$stable_version-$unstable_version" else echo "unexpected version: $stable_version" exit 1 fi git push origin HEAD:$release_branch_name echo "STABLE_VERSION=$stable_version" >> $GITHUB_ENV echo "UNSTABLE_VERSION=$unstable_version" >> $GITHUB_ENV echo "RELEASE_BRANCH_NAME=$release_branch_name" >> $GITHUB_ENV - name: Update version run: .github/scripts/update-version.sh $STABLE_VERSION $UNSTABLE_VERSION - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.10' - name: Install tox run: pip install tox - name: run tox run: tox -e generate - name: Install towncrier run: pip install towncrier==25.8.0 - name: Generate changelog run: towncrier build --yes --version "$STABLE_VERSION/$UNSTABLE_VERSION" - name: Use CLA approved github bot run: .github/scripts/use-cla-approved-github-bot.sh - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 id: otelbot-token with: client-id: ${{ vars.OTELBOT_CLIENT_ID }} private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} - name: Create pull request against the release branch id: create_release_branch_pr env: # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} run: | message="Prepare release ${STABLE_VERSION}/${UNSTABLE_VERSION}" branch="otelbot/prepare-release-${STABLE_VERSION}-${UNSTABLE_VERSION}" git commit -a -m "$message" git push origin HEAD:$branch pr_url=$(gh pr create --title "[$RELEASE_BRANCH_NAME] $message" \ --body "$message." \ --head $branch \ --base $RELEASE_BRANCH_NAME) echo "pr_url=$pr_url" >> $GITHUB_OUTPUT - name: Add prepare-release label to PR if: steps.create_release_branch_pr.outputs.pr_url != '' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh pr edit ${{ steps.create_release_branch_pr.outputs.pr_url }} --add-label "prepare-release" create-pull-request-against-main: runs-on: ubuntu-latest needs: prereqs permissions: contents: write # required for pushing branches pull-requests: write # required for creating and editing pull requests steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set environment variables env: PRERELEASE_VERSION: ${{ github.event.inputs.prerelease_version }} run: | if [[ -z $PRERELEASE_VERSION ]]; then stable_version=$(./scripts/eachdist.py version --mode stable) stable_version=${stable_version//.dev/} else stable_version=$PRERELEASE_VERSION fi unstable_version=$(./scripts/eachdist.py version --mode prerelease) unstable_version=${unstable_version//.dev/} if [[ $stable_version =~ ^([0-9]+)\.([0-9]+)\.0$ ]]; then stable_major="${BASH_REMATCH[1]}" stable_minor="${BASH_REMATCH[2]}" stable_next_version="$stable_major.$((stable_minor + 1)).0" elif [[ $stable_version =~ ^([0-9]+)\.([0-9]+)\.0 ]]; then # pre-release version, e.g. 1.9.0rc2 stable_major="${BASH_REMATCH[1]}" stable_minor="${BASH_REMATCH[2]}" stable_next_version="$stable_major.$stable_minor.0" else echo "unexpected stable_version: $stable_version" exit 1 fi if [[ $unstable_version =~ ^0\.([0-9]+)b[0-9]+$ ]]; then unstable_minor="${BASH_REMATCH[1]}" else echo "unexpected unstable_version: $unstable_version" exit 1 fi unstable_next_version="0.$((unstable_minor + 1))b0" echo "STABLE_VERSION=${stable_version}" >> $GITHUB_ENV echo "STABLE_NEXT_VERSION=${stable_next_version}.dev" >> $GITHUB_ENV echo "UNSTABLE_VERSION=${unstable_version}" >> $GITHUB_ENV echo "UNSTABLE_NEXT_VERSION=${unstable_next_version}.dev" >> $GITHUB_ENV - name: Update version run: .github/scripts/update-version.sh $STABLE_NEXT_VERSION $UNSTABLE_NEXT_VERSION - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.10' - name: Install tox run: pip install tox - name: run tox run: tox -e generate - name: Install towncrier run: pip install towncrier==25.8.0 - name: Generate changelog run: towncrier build --yes --version "$STABLE_VERSION/$UNSTABLE_VERSION" - name: Use CLA approved github bot run: .github/scripts/use-cla-approved-github-bot.sh - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 id: otelbot-token-main with: client-id: ${{ vars.OTELBOT_CLIENT_ID }} private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} - name: Create pull request against main id: create_main_pr env: # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows GITHUB_TOKEN: ${{ steps.otelbot-token-main.outputs.token }} run: | message="Update version to ${STABLE_NEXT_VERSION}/${UNSTABLE_NEXT_VERSION}" body="Update version to \`${STABLE_NEXT_VERSION}/${UNSTABLE_NEXT_VERSION}\`." branch="otelbot/update-version-to-${STABLE_NEXT_VERSION}-${UNSTABLE_NEXT_VERSION}" git commit -a -m "$message" git push origin HEAD:$branch pr_url=$(gh pr create --title "$message" \ --body "$body" \ --head $branch \ --base main) echo "pr_url=$pr_url" >> $GITHUB_OUTPUT - name: Add prepare-release label to PR if: steps.create_main_pr.outputs.pr_url != '' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh pr edit ${{ steps.create_main_pr.outputs.pr_url }} --add-label "prepare-release" --add-label "Skip Changelog" opentelemetry-python-contrib-0.66b2/.github/workflows/release.yml000066400000000000000000000107551527070072300253150ustar00rootroot00000000000000name: Release on: workflow_dispatch: permissions: contents: read jobs: release: permissions: contents: write # required for creating releases runs-on: ubuntu-latest steps: - run: | if [[ $GITHUB_REF_NAME != release/* ]]; then echo this workflow should only be run against release branches exit 1 fi - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set environment variables run: | stable_version=$(./scripts/eachdist.py version --mode stable) unstable_version=$(./scripts/eachdist.py version --mode prerelease) if [[ $stable_version =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then stable_major="${BASH_REMATCH[1]}" stable_minor="${BASH_REMATCH[2]}" stable_patch="${BASH_REMATCH[3]}" else echo "unexpected stable_version: $stable_version" exit 1 fi if [[ $stable_patch != 0 ]]; then if [[ $unstable_version =~ ^0\.([0-9]+)b([0-9]+)$ ]]; then unstable_minor="${BASH_REMATCH[1]}" unstable_patch="${BASH_REMATCH[2]}" else echo "unexpected unstable_version: $unstable_version" exit 1 fi if [[ $unstable_patch != 0 ]]; then prior_version_when_patch="$stable_major.$stable_minor.$((stable_patch - 1))/0.${unstable_minor}b$((unstable_patch - 1))" fi fi echo "STABLE_VERSION=$stable_version" >> $GITHUB_ENV echo "UNSTABLE_VERSION=$unstable_version" >> $GITHUB_ENV echo "CHANGELOG=CHANGELOG.md" >> $GITHUB_ENV echo "PRIOR_VERSION_WHEN_PATCH=$prior_version_when_patch" >> $GITHUB_ENV - run: | if [[ -z $PRIOR_VERSION_WHEN_PATCH ]]; then # not making a patch release if ! grep --quiet "^## Version ${STABLE_VERSION}/${UNSTABLE_VERSION} " CHANGELOG.md; then echo the pull request generated by prepare-release-branch.yml needs to be merged first exit 1 fi fi # check out main branch to verify there won't be problems with merging the change log # at the end of this workflow - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: ref: main # back to the release branch - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 # next few steps publish to pypi - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.10' - name: Build wheels run: ./scripts/build.sh - name: Install twine run: | pip install twine # The step below publishes to testpypi in order to catch any issues # with the package configuration that would cause a failure to upload # to pypi. One example of such a failure is if a classifier is # rejected by pypi (e.g "3 - Beta"). This would cause a failure during the # middle of the package upload causing the action to fail, and certain packages # might have already been updated, this would be bad. - name: Publish to TestPyPI env: TWINE_USERNAME: '__token__' TWINE_PASSWORD: ${{ secrets.test_pypi_token }} run: | twine upload --repository testpypi --skip-existing --verbose dist/* - name: Publish to PyPI env: TWINE_USERNAME: '__token__' TWINE_PASSWORD: ${{ secrets.pypi_password }} run: | twine upload --skip-existing --verbose dist/* - name: Generate release notes env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "VERSION=${STABLE_VERSION}\/${UNSTABLE_VERSION}" >> $GITHUB_ENV ./scripts/generate_release_notes.sh - name: Create GitHub release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release create --target $GITHUB_REF_NAME \ --title "Version ${STABLE_VERSION}/${UNSTABLE_VERSION}" \ --notes-file /tmp/release-notes.txt \ --discussion-category announcements \ v$UNSTABLE_VERSION - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: # the step below is creating a pull request against main ref: main opentelemetry-python-contrib-0.66b2/.github/workflows/stale.yml000066400000000000000000000023121527070072300247730ustar00rootroot00000000000000name: Mark stale PRs on: schedule: - cron: "12 3 * * *" workflow_dispatch: permissions: contents: read jobs: stale: permissions: contents: read issues: write pull-requests: write runs-on: ubuntu-latest steps: - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-pr-stale: 14 days-before-pr-close: 14 days-before-issue-stale: -1 days-before-issue-close: -1 stale-pr-message: > This PR has been automatically marked as stale because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 days of this comment. If you're still working on this, please add a comment or push new commits. close-pr-message: > This PR has been closed due to inactivity. Please reopen if you would like to continue working on it. exempt-pr-labels: "hold,WIP,blocked-by-spec,do not merge" # TODO: Revert back to default of 30 after we have cleared the backlog of stale PRs. operations-per-run: 500 opentelemetry-python-contrib-0.66b2/.github/workflows/test.yml000066400000000000000000014671211527070072300246600ustar00rootroot00000000000000# Do not edit this file. # This file is generated automatically by executing tox -e generate-workflows name: Test on: workflow_call: permissions: contents: read env: # Set the SHA to the branch name if the PR has a label 'prepare-release' or 'backport' otherwise, set it to 'main' # For PRs you can change the inner fallback ('main') # For pushes you change the outer fallback ('main') # The logic below is used during releases and depends on having an equivalent branch name in the core repo. CORE_REPO_SHA: ${{ github.event_name == 'pull_request' && ( contains(github.event.pull_request.labels.*.name, 'prepare-release') && github.event.pull_request.head.ref || contains(github.event.pull_request.labels.*.name, 'backport') && github.event.pull_request.base.ref || 'main' ) || 'main' }} CONTRIB_REPO_SHA: main PIP_EXISTS_ACTION: w jobs: py310-test-instrumentation-openai-v2-oldest_ubuntu-latest: name: instrumentation-openai-v2-oldest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-openai-v2-oldest -- -ra py310-test-instrumentation-openai-v2-latest_ubuntu-latest: name: instrumentation-openai-v2-latest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-openai-v2-latest -- -ra py311-test-instrumentation-openai-v2-oldest_ubuntu-latest: name: instrumentation-openai-v2-oldest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-openai-v2-oldest -- -ra py311-test-instrumentation-openai-v2-latest_ubuntu-latest: name: instrumentation-openai-v2-latest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-openai-v2-latest -- -ra py312-test-instrumentation-openai-v2-oldest_ubuntu-latest: name: instrumentation-openai-v2-oldest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-openai-v2-oldest -- -ra py312-test-instrumentation-openai-v2-latest_ubuntu-latest: name: instrumentation-openai-v2-latest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-openai-v2-latest -- -ra py313-test-instrumentation-openai-v2-oldest_ubuntu-latest: name: instrumentation-openai-v2-oldest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-openai-v2-oldest -- -ra py313-test-instrumentation-openai-v2-latest_ubuntu-latest: name: instrumentation-openai-v2-latest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-openai-v2-latest -- -ra py314-test-instrumentation-openai-v2-oldest_ubuntu-latest: name: instrumentation-openai-v2-oldest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-openai-v2-oldest -- -ra py314-test-instrumentation-openai-v2-latest_ubuntu-latest: name: instrumentation-openai-v2-latest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-openai-v2-latest -- -ra pypy3-test-instrumentation-openai-v2-oldest_ubuntu-latest: name: instrumentation-openai-v2-oldest pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-openai-v2-oldest -- -ra pypy3-test-instrumentation-openai-v2-latest_ubuntu-latest: name: instrumentation-openai-v2-latest pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-openai-v2-latest -- -ra py310-test-instrumentation-openai_agents-v2-oldest_ubuntu-latest: name: instrumentation-openai_agents-v2-oldest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-openai_agents-v2-oldest -- -ra py310-test-instrumentation-openai_agents-v2-latest_ubuntu-latest: name: instrumentation-openai_agents-v2-latest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-openai_agents-v2-latest -- -ra py311-test-instrumentation-openai_agents-v2-oldest_ubuntu-latest: name: instrumentation-openai_agents-v2-oldest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-openai_agents-v2-oldest -- -ra py311-test-instrumentation-openai_agents-v2-latest_ubuntu-latest: name: instrumentation-openai_agents-v2-latest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-openai_agents-v2-latest -- -ra py312-test-instrumentation-openai_agents-v2-oldest_ubuntu-latest: name: instrumentation-openai_agents-v2-oldest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-openai_agents-v2-oldest -- -ra py312-test-instrumentation-openai_agents-v2-latest_ubuntu-latest: name: instrumentation-openai_agents-v2-latest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-openai_agents-v2-latest -- -ra py313-test-instrumentation-openai_agents-v2-oldest_ubuntu-latest: name: instrumentation-openai_agents-v2-oldest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-openai_agents-v2-oldest -- -ra py313-test-instrumentation-openai_agents-v2-latest_ubuntu-latest: name: instrumentation-openai_agents-v2-latest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-openai_agents-v2-latest -- -ra py314-test-instrumentation-openai_agents-v2-oldest_ubuntu-latest: name: instrumentation-openai_agents-v2-oldest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-openai_agents-v2-oldest -- -ra py314-test-instrumentation-openai_agents-v2-latest_ubuntu-latest: name: instrumentation-openai_agents-v2-latest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-openai_agents-v2-latest -- -ra py310-test-instrumentation-vertexai-oldest_ubuntu-latest: name: instrumentation-vertexai-oldest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-vertexai-oldest -- -ra py310-test-instrumentation-vertexai-latest_ubuntu-latest: name: instrumentation-vertexai-latest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-vertexai-latest -- -ra py311-test-instrumentation-vertexai-oldest_ubuntu-latest: name: instrumentation-vertexai-oldest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-vertexai-oldest -- -ra py311-test-instrumentation-vertexai-latest_ubuntu-latest: name: instrumentation-vertexai-latest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-vertexai-latest -- -ra py312-test-instrumentation-vertexai-oldest_ubuntu-latest: name: instrumentation-vertexai-oldest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-vertexai-oldest -- -ra py312-test-instrumentation-vertexai-latest_ubuntu-latest: name: instrumentation-vertexai-latest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-vertexai-latest -- -ra py313-test-instrumentation-vertexai-oldest_ubuntu-latest: name: instrumentation-vertexai-oldest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-vertexai-oldest -- -ra py313-test-instrumentation-vertexai-latest_ubuntu-latest: name: instrumentation-vertexai-latest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-vertexai-latest -- -ra py314-test-instrumentation-vertexai-oldest_ubuntu-latest: name: instrumentation-vertexai-oldest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-vertexai-oldest -- -ra py314-test-instrumentation-vertexai-latest_ubuntu-latest: name: instrumentation-vertexai-latest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-vertexai-latest -- -ra py310-test-resource-detector-containerid_ubuntu-latest: name: resource-detector-containerid 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-resource-detector-containerid -- -ra py311-test-resource-detector-containerid_ubuntu-latest: name: resource-detector-containerid 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-resource-detector-containerid -- -ra py312-test-resource-detector-containerid_ubuntu-latest: name: resource-detector-containerid 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-resource-detector-containerid -- -ra py313-test-resource-detector-containerid_ubuntu-latest: name: resource-detector-containerid 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-resource-detector-containerid -- -ra py314-test-resource-detector-containerid_ubuntu-latest: name: resource-detector-containerid 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-resource-detector-containerid -- -ra pypy3-test-resource-detector-containerid_ubuntu-latest: name: resource-detector-containerid pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-resource-detector-containerid -- -ra py310-test-resource-detector-azure-0_ubuntu-latest: name: resource-detector-azure-0 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-resource-detector-azure-0 -- -ra py310-test-resource-detector-azure-1_ubuntu-latest: name: resource-detector-azure-1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-resource-detector-azure-1 -- -ra py311-test-resource-detector-azure-0_ubuntu-latest: name: resource-detector-azure-0 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-resource-detector-azure-0 -- -ra py311-test-resource-detector-azure-1_ubuntu-latest: name: resource-detector-azure-1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-resource-detector-azure-1 -- -ra py312-test-resource-detector-azure-0_ubuntu-latest: name: resource-detector-azure-0 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-resource-detector-azure-0 -- -ra py312-test-resource-detector-azure-1_ubuntu-latest: name: resource-detector-azure-1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-resource-detector-azure-1 -- -ra py313-test-resource-detector-azure-0_ubuntu-latest: name: resource-detector-azure-0 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-resource-detector-azure-0 -- -ra py313-test-resource-detector-azure-1_ubuntu-latest: name: resource-detector-azure-1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-resource-detector-azure-1 -- -ra py314-test-resource-detector-azure-0_ubuntu-latest: name: resource-detector-azure-0 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-resource-detector-azure-0 -- -ra py314-test-resource-detector-azure-1_ubuntu-latest: name: resource-detector-azure-1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-resource-detector-azure-1 -- -ra pypy3-test-resource-detector-azure-0_ubuntu-latest: name: resource-detector-azure-0 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-resource-detector-azure-0 -- -ra pypy3-test-resource-detector-azure-1_ubuntu-latest: name: resource-detector-azure-1 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-resource-detector-azure-1 -- -ra py310-test-resourcedetector-gcp-0_ubuntu-latest: name: resourcedetector-gcp-0 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-resourcedetector-gcp-0 -- -ra py310-test-resourcedetector-gcp-1_ubuntu-latest: name: resourcedetector-gcp-1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-resourcedetector-gcp-1 -- -ra py311-test-resourcedetector-gcp-0_ubuntu-latest: name: resourcedetector-gcp-0 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-resourcedetector-gcp-0 -- -ra py311-test-resourcedetector-gcp-1_ubuntu-latest: name: resourcedetector-gcp-1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-resourcedetector-gcp-1 -- -ra py312-test-resourcedetector-gcp-0_ubuntu-latest: name: resourcedetector-gcp-0 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-resourcedetector-gcp-0 -- -ra py312-test-resourcedetector-gcp-1_ubuntu-latest: name: resourcedetector-gcp-1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-resourcedetector-gcp-1 -- -ra py313-test-resourcedetector-gcp-0_ubuntu-latest: name: resourcedetector-gcp-0 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-resourcedetector-gcp-0 -- -ra py313-test-resourcedetector-gcp-1_ubuntu-latest: name: resourcedetector-gcp-1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-resourcedetector-gcp-1 -- -ra py314-test-resourcedetector-gcp-0_ubuntu-latest: name: resourcedetector-gcp-0 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-resourcedetector-gcp-0 -- -ra py314-test-resourcedetector-gcp-1_ubuntu-latest: name: resourcedetector-gcp-1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-resourcedetector-gcp-1 -- -ra pypy3-test-resourcedetector-gcp-0_ubuntu-latest: name: resourcedetector-gcp-0 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-resourcedetector-gcp-0 -- -ra pypy3-test-resourcedetector-gcp-1_ubuntu-latest: name: resourcedetector-gcp-1 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-resourcedetector-gcp-1 -- -ra py310-test-sdk-extension-aws-0_ubuntu-latest: name: sdk-extension-aws-0 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-sdk-extension-aws-0 -- -ra py310-test-sdk-extension-aws-1_ubuntu-latest: name: sdk-extension-aws-1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-sdk-extension-aws-1 -- -ra py311-test-sdk-extension-aws-0_ubuntu-latest: name: sdk-extension-aws-0 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-sdk-extension-aws-0 -- -ra py311-test-sdk-extension-aws-1_ubuntu-latest: name: sdk-extension-aws-1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-sdk-extension-aws-1 -- -ra py312-test-sdk-extension-aws-0_ubuntu-latest: name: sdk-extension-aws-0 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-sdk-extension-aws-0 -- -ra py312-test-sdk-extension-aws-1_ubuntu-latest: name: sdk-extension-aws-1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-sdk-extension-aws-1 -- -ra py313-test-sdk-extension-aws-0_ubuntu-latest: name: sdk-extension-aws-0 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-sdk-extension-aws-0 -- -ra py313-test-sdk-extension-aws-1_ubuntu-latest: name: sdk-extension-aws-1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-sdk-extension-aws-1 -- -ra py314-test-sdk-extension-aws-0_ubuntu-latest: name: sdk-extension-aws-0 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-sdk-extension-aws-0 -- -ra py314-test-sdk-extension-aws-1_ubuntu-latest: name: sdk-extension-aws-1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-sdk-extension-aws-1 -- -ra pypy3-test-sdk-extension-aws-0_ubuntu-latest: name: sdk-extension-aws-0 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-sdk-extension-aws-0 -- -ra pypy3-test-sdk-extension-aws-1_ubuntu-latest: name: sdk-extension-aws-1 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-sdk-extension-aws-1 -- -ra py310-test-distro_ubuntu-latest: name: distro 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-distro -- -ra py311-test-distro_ubuntu-latest: name: distro 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-distro -- -ra py312-test-distro_ubuntu-latest: name: distro 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-distro -- -ra py313-test-distro_ubuntu-latest: name: distro 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-distro -- -ra py314-test-distro_ubuntu-latest: name: distro 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-distro -- -ra pypy3-test-distro_ubuntu-latest: name: distro pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-distro -- -ra py310-test-opentelemetry-instrumentation-wrapt1_ubuntu-latest: name: opentelemetry-instrumentation-wrapt1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-opentelemetry-instrumentation-wrapt1 -- -ra py310-test-opentelemetry-instrumentation-wrapt2_ubuntu-latest: name: opentelemetry-instrumentation-wrapt2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-opentelemetry-instrumentation-wrapt2 -- -ra py311-test-opentelemetry-instrumentation-wrapt1_ubuntu-latest: name: opentelemetry-instrumentation-wrapt1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-opentelemetry-instrumentation-wrapt1 -- -ra py311-test-opentelemetry-instrumentation-wrapt2_ubuntu-latest: name: opentelemetry-instrumentation-wrapt2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-opentelemetry-instrumentation-wrapt2 -- -ra py312-test-opentelemetry-instrumentation-wrapt1_ubuntu-latest: name: opentelemetry-instrumentation-wrapt1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-opentelemetry-instrumentation-wrapt1 -- -ra py312-test-opentelemetry-instrumentation-wrapt2_ubuntu-latest: name: opentelemetry-instrumentation-wrapt2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-opentelemetry-instrumentation-wrapt2 -- -ra py313-test-opentelemetry-instrumentation-wrapt1_ubuntu-latest: name: opentelemetry-instrumentation-wrapt1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-opentelemetry-instrumentation-wrapt1 -- -ra py313-test-opentelemetry-instrumentation-wrapt2_ubuntu-latest: name: opentelemetry-instrumentation-wrapt2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-opentelemetry-instrumentation-wrapt2 -- -ra py314-test-opentelemetry-instrumentation-wrapt1_ubuntu-latest: name: opentelemetry-instrumentation-wrapt1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-opentelemetry-instrumentation-wrapt1 -- -ra py314-test-opentelemetry-instrumentation-wrapt2_ubuntu-latest: name: opentelemetry-instrumentation-wrapt2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-opentelemetry-instrumentation-wrapt2 -- -ra pypy3-test-opentelemetry-instrumentation-wrapt2_ubuntu-latest: name: opentelemetry-instrumentation-wrapt2 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-opentelemetry-instrumentation-wrapt2 -- -ra py310-test-instrumentation-aiohttp-client_ubuntu-latest: name: instrumentation-aiohttp-client 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-aiohttp-client -- -ra py311-test-instrumentation-aiohttp-client_ubuntu-latest: name: instrumentation-aiohttp-client 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-aiohttp-client -- -ra py312-test-instrumentation-aiohttp-client_ubuntu-latest: name: instrumentation-aiohttp-client 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-aiohttp-client -- -ra py313-test-instrumentation-aiohttp-client_ubuntu-latest: name: instrumentation-aiohttp-client 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-aiohttp-client -- -ra py314-test-instrumentation-aiohttp-client_ubuntu-latest: name: instrumentation-aiohttp-client 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-aiohttp-client -- -ra pypy3-test-instrumentation-aiohttp-client_ubuntu-latest: name: instrumentation-aiohttp-client pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-aiohttp-client -- -ra py310-test-instrumentation-aiohttp-server_ubuntu-latest: name: instrumentation-aiohttp-server 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-aiohttp-server -- -ra py311-test-instrumentation-aiohttp-server_ubuntu-latest: name: instrumentation-aiohttp-server 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-aiohttp-server -- -ra py312-test-instrumentation-aiohttp-server_ubuntu-latest: name: instrumentation-aiohttp-server 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-aiohttp-server -- -ra py313-test-instrumentation-aiohttp-server_ubuntu-latest: name: instrumentation-aiohttp-server 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-aiohttp-server -- -ra py314-test-instrumentation-aiohttp-server_ubuntu-latest: name: instrumentation-aiohttp-server 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-aiohttp-server -- -ra pypy3-test-instrumentation-aiohttp-server_ubuntu-latest: name: instrumentation-aiohttp-server pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-aiohttp-server -- -ra py310-test-instrumentation-aiopg-wrapt1_ubuntu-latest: name: instrumentation-aiopg-wrapt1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-aiopg-wrapt1 -- -ra py310-test-instrumentation-aiopg-wrapt2_ubuntu-latest: name: instrumentation-aiopg-wrapt2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-aiopg-wrapt2 -- -ra py311-test-instrumentation-aiopg-wrapt1_ubuntu-latest: name: instrumentation-aiopg-wrapt1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-aiopg-wrapt1 -- -ra py311-test-instrumentation-aiopg-wrapt2_ubuntu-latest: name: instrumentation-aiopg-wrapt2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-aiopg-wrapt2 -- -ra py312-test-instrumentation-aiopg-wrapt1_ubuntu-latest: name: instrumentation-aiopg-wrapt1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-aiopg-wrapt1 -- -ra py312-test-instrumentation-aiopg-wrapt2_ubuntu-latest: name: instrumentation-aiopg-wrapt2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-aiopg-wrapt2 -- -ra py313-test-instrumentation-aiopg-wrapt1_ubuntu-latest: name: instrumentation-aiopg-wrapt1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-aiopg-wrapt1 -- -ra py313-test-instrumentation-aiopg-wrapt2_ubuntu-latest: name: instrumentation-aiopg-wrapt2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-aiopg-wrapt2 -- -ra py314-test-instrumentation-aiopg-wrapt1_ubuntu-latest: name: instrumentation-aiopg-wrapt1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-aiopg-wrapt1 -- -ra py314-test-instrumentation-aiopg-wrapt2_ubuntu-latest: name: instrumentation-aiopg-wrapt2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-aiopg-wrapt2 -- -ra py310-test-instrumentation-aws-lambda_ubuntu-latest: name: instrumentation-aws-lambda 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-aws-lambda -- -ra py311-test-instrumentation-aws-lambda_ubuntu-latest: name: instrumentation-aws-lambda 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-aws-lambda -- -ra py312-test-instrumentation-aws-lambda_ubuntu-latest: name: instrumentation-aws-lambda 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-aws-lambda -- -ra py313-test-instrumentation-aws-lambda_ubuntu-latest: name: instrumentation-aws-lambda 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-aws-lambda -- -ra py314-test-instrumentation-aws-lambda_ubuntu-latest: name: instrumentation-aws-lambda 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-aws-lambda -- -ra pypy3-test-instrumentation-aws-lambda_ubuntu-latest: name: instrumentation-aws-lambda pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-aws-lambda -- -ra py310-test-instrumentation-botocore-0-wrapt1_ubuntu-latest: name: instrumentation-botocore-0-wrapt1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-botocore-0-wrapt1 -- -ra py310-test-instrumentation-botocore-0-wrapt2_ubuntu-latest: name: instrumentation-botocore-0-wrapt2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-botocore-0-wrapt2 -- -ra py310-test-instrumentation-botocore-1-wrapt1_ubuntu-latest: name: instrumentation-botocore-1-wrapt1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-botocore-1-wrapt1 -- -ra py310-test-instrumentation-botocore-1-wrapt2_ubuntu-latest: name: instrumentation-botocore-1-wrapt2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-botocore-1-wrapt2 -- -ra py311-test-instrumentation-botocore-0-wrapt1_ubuntu-latest: name: instrumentation-botocore-0-wrapt1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-botocore-0-wrapt1 -- -ra py311-test-instrumentation-botocore-0-wrapt2_ubuntu-latest: name: instrumentation-botocore-0-wrapt2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-botocore-0-wrapt2 -- -ra py311-test-instrumentation-botocore-1-wrapt1_ubuntu-latest: name: instrumentation-botocore-1-wrapt1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-botocore-1-wrapt1 -- -ra py311-test-instrumentation-botocore-1-wrapt2_ubuntu-latest: name: instrumentation-botocore-1-wrapt2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-botocore-1-wrapt2 -- -ra py312-test-instrumentation-botocore-0-wrapt1_ubuntu-latest: name: instrumentation-botocore-0-wrapt1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-botocore-0-wrapt1 -- -ra py312-test-instrumentation-botocore-0-wrapt2_ubuntu-latest: name: instrumentation-botocore-0-wrapt2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-botocore-0-wrapt2 -- -ra py312-test-instrumentation-botocore-1-wrapt1_ubuntu-latest: name: instrumentation-botocore-1-wrapt1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-botocore-1-wrapt1 -- -ra py312-test-instrumentation-botocore-1-wrapt2_ubuntu-latest: name: instrumentation-botocore-1-wrapt2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-botocore-1-wrapt2 -- -ra py313-test-instrumentation-botocore-0-wrapt1_ubuntu-latest: name: instrumentation-botocore-0-wrapt1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-botocore-0-wrapt1 -- -ra py313-test-instrumentation-botocore-0-wrapt2_ubuntu-latest: name: instrumentation-botocore-0-wrapt2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-botocore-0-wrapt2 -- -ra py313-test-instrumentation-botocore-1-wrapt1_ubuntu-latest: name: instrumentation-botocore-1-wrapt1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-botocore-1-wrapt1 -- -ra py313-test-instrumentation-botocore-1-wrapt2_ubuntu-latest: name: instrumentation-botocore-1-wrapt2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-botocore-1-wrapt2 -- -ra py314-test-instrumentation-botocore-0-wrapt1_ubuntu-latest: name: instrumentation-botocore-0-wrapt1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-botocore-0-wrapt1 -- -ra py314-test-instrumentation-botocore-0-wrapt2_ubuntu-latest: name: instrumentation-botocore-0-wrapt2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-botocore-0-wrapt2 -- -ra py314-test-instrumentation-botocore-1-wrapt1_ubuntu-latest: name: instrumentation-botocore-1-wrapt1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-botocore-1-wrapt1 -- -ra py314-test-instrumentation-botocore-1-wrapt2_ubuntu-latest: name: instrumentation-botocore-1-wrapt2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-botocore-1-wrapt2 -- -ra py310-test-instrumentation-botocore-2_ubuntu-latest: name: instrumentation-botocore-2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-botocore-2 -- -ra py310-test-instrumentation-botocore-3_ubuntu-latest: name: instrumentation-botocore-3 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-botocore-3 -- -ra py311-test-instrumentation-botocore-2_ubuntu-latest: name: instrumentation-botocore-2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-botocore-2 -- -ra py311-test-instrumentation-botocore-3_ubuntu-latest: name: instrumentation-botocore-3 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-botocore-3 -- -ra py312-test-instrumentation-botocore-2_ubuntu-latest: name: instrumentation-botocore-2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-botocore-2 -- -ra py312-test-instrumentation-botocore-3_ubuntu-latest: name: instrumentation-botocore-3 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-botocore-3 -- -ra py313-test-instrumentation-botocore-2_ubuntu-latest: name: instrumentation-botocore-2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-botocore-2 -- -ra py313-test-instrumentation-botocore-3_ubuntu-latest: name: instrumentation-botocore-3 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-botocore-3 -- -ra py314-test-instrumentation-botocore-2_ubuntu-latest: name: instrumentation-botocore-2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-botocore-2 -- -ra py314-test-instrumentation-botocore-3_ubuntu-latest: name: instrumentation-botocore-3 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-botocore-3 -- -ra pypy3-test-instrumentation-botocore-1-wrapt2_ubuntu-latest: name: instrumentation-botocore-1-wrapt2 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-botocore-1-wrapt2 -- -ra pypy3-test-instrumentation-botocore-3_ubuntu-latest: name: instrumentation-botocore-3 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-botocore-3 -- -ra py310-test-instrumentation-boto3sqs_ubuntu-latest: name: instrumentation-boto3sqs 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-boto3sqs -- -ra py311-test-instrumentation-boto3sqs_ubuntu-latest: name: instrumentation-boto3sqs 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-boto3sqs -- -ra py312-test-instrumentation-boto3sqs_ubuntu-latest: name: instrumentation-boto3sqs 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-boto3sqs -- -ra py313-test-instrumentation-boto3sqs_ubuntu-latest: name: instrumentation-boto3sqs 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-boto3sqs -- -ra py314-test-instrumentation-boto3sqs_ubuntu-latest: name: instrumentation-boto3sqs 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-boto3sqs -- -ra pypy3-test-instrumentation-boto3sqs_ubuntu-latest: name: instrumentation-boto3sqs pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-boto3sqs -- -ra py310-test-instrumentation-django-0_ubuntu-latest: name: instrumentation-django-0 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-django-0 -- -ra py310-test-instrumentation-django-1_ubuntu-latest: name: instrumentation-django-1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-django-1 -- -ra py310-test-instrumentation-django-2_ubuntu-latest: name: instrumentation-django-2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-django-2 -- -ra py310-test-instrumentation-django-3_ubuntu-latest: name: instrumentation-django-3 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-django-3 -- -ra py311-test-instrumentation-django-0_ubuntu-latest: name: instrumentation-django-0 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-django-0 -- -ra py311-test-instrumentation-django-1_ubuntu-latest: name: instrumentation-django-1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-django-1 -- -ra py311-test-instrumentation-django-2_ubuntu-latest: name: instrumentation-django-2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-django-2 -- -ra py311-test-instrumentation-django-3_ubuntu-latest: name: instrumentation-django-3 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-django-3 -- -ra py312-test-instrumentation-django-0_ubuntu-latest: name: instrumentation-django-0 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-django-0 -- -ra py312-test-instrumentation-django-1_ubuntu-latest: name: instrumentation-django-1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-django-1 -- -ra py312-test-instrumentation-django-2_ubuntu-latest: name: instrumentation-django-2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-django-2 -- -ra py312-test-instrumentation-django-3_ubuntu-latest: name: instrumentation-django-3 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-django-3 -- -ra py313-test-instrumentation-django-3_ubuntu-latest: name: instrumentation-django-3 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-django-3 -- -ra py314-test-instrumentation-django-3_ubuntu-latest: name: instrumentation-django-3 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-django-3 -- -ra pypy3-test-instrumentation-django-0_ubuntu-latest: name: instrumentation-django-0 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-django-0 -- -ra pypy3-test-instrumentation-django-1_ubuntu-latest: name: instrumentation-django-1 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-django-1 -- -ra pypy3-test-instrumentation-django-2_ubuntu-latest: name: instrumentation-django-2 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-django-2 -- -ra pypy3-test-instrumentation-django-3_ubuntu-latest: name: instrumentation-django-3 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-django-3 -- -ra py310-test-instrumentation-dbapi-wrapt1_ubuntu-latest: name: instrumentation-dbapi-wrapt1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-dbapi-wrapt1 -- -ra py310-test-instrumentation-dbapi-wrapt2_ubuntu-latest: name: instrumentation-dbapi-wrapt2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-dbapi-wrapt2 -- -ra py311-test-instrumentation-dbapi-wrapt1_ubuntu-latest: name: instrumentation-dbapi-wrapt1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-dbapi-wrapt1 -- -ra py311-test-instrumentation-dbapi-wrapt2_ubuntu-latest: name: instrumentation-dbapi-wrapt2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-dbapi-wrapt2 -- -ra py312-test-instrumentation-dbapi-wrapt1_ubuntu-latest: name: instrumentation-dbapi-wrapt1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-dbapi-wrapt1 -- -ra py312-test-instrumentation-dbapi-wrapt2_ubuntu-latest: name: instrumentation-dbapi-wrapt2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-dbapi-wrapt2 -- -ra py313-test-instrumentation-dbapi-wrapt1_ubuntu-latest: name: instrumentation-dbapi-wrapt1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-dbapi-wrapt1 -- -ra py313-test-instrumentation-dbapi-wrapt2_ubuntu-latest: name: instrumentation-dbapi-wrapt2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-dbapi-wrapt2 -- -ra py314-test-instrumentation-dbapi-wrapt1_ubuntu-latest: name: instrumentation-dbapi-wrapt1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-dbapi-wrapt1 -- -ra py314-test-instrumentation-dbapi-wrapt2_ubuntu-latest: name: instrumentation-dbapi-wrapt2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-dbapi-wrapt2 -- -ra pypy3-test-instrumentation-dbapi-wrapt2_ubuntu-latest: name: instrumentation-dbapi-wrapt2 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-dbapi-wrapt2 -- -ra py310-test-instrumentation-asyncclick_ubuntu-latest: name: instrumentation-asyncclick 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-asyncclick -- -ra py311-test-instrumentation-asyncclick_ubuntu-latest: name: instrumentation-asyncclick 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-asyncclick -- -ra py312-test-instrumentation-asyncclick_ubuntu-latest: name: instrumentation-asyncclick 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-asyncclick -- -ra py313-test-instrumentation-asyncclick_ubuntu-latest: name: instrumentation-asyncclick 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-asyncclick -- -ra py314-test-instrumentation-asyncclick_ubuntu-latest: name: instrumentation-asyncclick 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-asyncclick -- -ra pypy3-test-instrumentation-asyncclick_ubuntu-latest: name: instrumentation-asyncclick pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-asyncclick -- -ra py310-test-instrumentation-click_ubuntu-latest: name: instrumentation-click 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-click -- -ra py311-test-instrumentation-click_ubuntu-latest: name: instrumentation-click 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-click -- -ra py312-test-instrumentation-click_ubuntu-latest: name: instrumentation-click 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-click -- -ra py313-test-instrumentation-click_ubuntu-latest: name: instrumentation-click 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-click -- -ra py314-test-instrumentation-click_ubuntu-latest: name: instrumentation-click 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-click -- -ra pypy3-test-instrumentation-click_ubuntu-latest: name: instrumentation-click pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-click -- -ra py310-test-instrumentation-falcon-1_ubuntu-latest: name: instrumentation-falcon-1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-falcon-1 -- -ra py310-test-instrumentation-falcon-2_ubuntu-latest: name: instrumentation-falcon-2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-falcon-2 -- -ra py310-test-instrumentation-falcon-3_ubuntu-latest: name: instrumentation-falcon-3 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-falcon-3 -- -ra py310-test-instrumentation-falcon-4_ubuntu-latest: name: instrumentation-falcon-4 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-falcon-4 -- -ra py311-test-instrumentation-falcon-1_ubuntu-latest: name: instrumentation-falcon-1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-falcon-1 -- -ra py311-test-instrumentation-falcon-2_ubuntu-latest: name: instrumentation-falcon-2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-falcon-2 -- -ra py311-test-instrumentation-falcon-3_ubuntu-latest: name: instrumentation-falcon-3 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-falcon-3 -- -ra py311-test-instrumentation-falcon-4_ubuntu-latest: name: instrumentation-falcon-4 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-falcon-4 -- -ra py312-test-instrumentation-falcon-1_ubuntu-latest: name: instrumentation-falcon-1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-falcon-1 -- -ra py312-test-instrumentation-falcon-2_ubuntu-latest: name: instrumentation-falcon-2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-falcon-2 -- -ra py312-test-instrumentation-falcon-3_ubuntu-latest: name: instrumentation-falcon-3 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-falcon-3 -- -ra py312-test-instrumentation-falcon-4_ubuntu-latest: name: instrumentation-falcon-4 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-falcon-4 -- -ra py313-test-instrumentation-falcon-4_ubuntu-latest: name: instrumentation-falcon-4 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-falcon-4 -- -ra py314-test-instrumentation-falcon-4_ubuntu-latest: name: instrumentation-falcon-4 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-falcon-4 -- -ra pypy3-test-instrumentation-falcon-1_ubuntu-latest: name: instrumentation-falcon-1 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-falcon-1 -- -ra pypy3-test-instrumentation-falcon-2_ubuntu-latest: name: instrumentation-falcon-2 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-falcon-2 -- -ra pypy3-test-instrumentation-falcon-3_ubuntu-latest: name: instrumentation-falcon-3 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-falcon-3 -- -ra pypy3-test-instrumentation-falcon-4_ubuntu-latest: name: instrumentation-falcon-4 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-falcon-4 -- -ra py310-test-instrumentation-fastapi-oldest_ubuntu-latest: name: instrumentation-fastapi-oldest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-fastapi-oldest -- -ra py310-test-instrumentation-fastapi-latest_ubuntu-latest: name: instrumentation-fastapi-latest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-fastapi-latest -- -ra py311-test-instrumentation-fastapi-oldest_ubuntu-latest: name: instrumentation-fastapi-oldest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-fastapi-oldest -- -ra py311-test-instrumentation-fastapi-latest_ubuntu-latest: name: instrumentation-fastapi-latest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-fastapi-latest -- -ra py312-test-instrumentation-fastapi-oldest_ubuntu-latest: name: instrumentation-fastapi-oldest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-fastapi-oldest -- -ra py312-test-instrumentation-fastapi-latest_ubuntu-latest: name: instrumentation-fastapi-latest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-fastapi-latest -- -ra py313-test-instrumentation-fastapi-oldest_ubuntu-latest: name: instrumentation-fastapi-oldest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-fastapi-oldest -- -ra py313-test-instrumentation-fastapi-latest_ubuntu-latest: name: instrumentation-fastapi-latest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-fastapi-latest -- -ra py314-test-instrumentation-fastapi-oldest_ubuntu-latest: name: instrumentation-fastapi-oldest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-fastapi-oldest -- -ra py314-test-instrumentation-fastapi-latest_ubuntu-latest: name: instrumentation-fastapi-latest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-fastapi-latest -- -ra pypy3-test-instrumentation-fastapi-oldest_ubuntu-latest: name: instrumentation-fastapi-oldest pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-fastapi-oldest -- -ra pypy3-test-instrumentation-fastapi-latest_ubuntu-latest: name: instrumentation-fastapi-latest pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-fastapi-latest -- -ra py310-test-instrumentation-flask-0_ubuntu-latest: name: instrumentation-flask-0 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-flask-0 -- -ra py310-test-instrumentation-flask-1_ubuntu-latest: name: instrumentation-flask-1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-flask-1 -- -ra py310-test-instrumentation-flask-2_ubuntu-latest: name: instrumentation-flask-2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-flask-2 -- -ra py310-test-instrumentation-flask-3_ubuntu-latest: name: instrumentation-flask-3 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-flask-3 -- -ra py311-test-instrumentation-flask-0_ubuntu-latest: name: instrumentation-flask-0 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-flask-0 -- -ra py311-test-instrumentation-flask-1_ubuntu-latest: name: instrumentation-flask-1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-flask-1 -- -ra py311-test-instrumentation-flask-2_ubuntu-latest: name: instrumentation-flask-2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-flask-2 -- -ra py311-test-instrumentation-flask-3_ubuntu-latest: name: instrumentation-flask-3 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-flask-3 -- -ra py312-test-instrumentation-flask-0_ubuntu-latest: name: instrumentation-flask-0 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-flask-0 -- -ra py312-test-instrumentation-flask-1_ubuntu-latest: name: instrumentation-flask-1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-flask-1 -- -ra py312-test-instrumentation-flask-2_ubuntu-latest: name: instrumentation-flask-2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-flask-2 -- -ra py312-test-instrumentation-flask-3_ubuntu-latest: name: instrumentation-flask-3 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-flask-3 -- -ra py313-test-instrumentation-flask-0_ubuntu-latest: name: instrumentation-flask-0 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-flask-0 -- -ra py313-test-instrumentation-flask-1_ubuntu-latest: name: instrumentation-flask-1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-flask-1 -- -ra py313-test-instrumentation-flask-2_ubuntu-latest: name: instrumentation-flask-2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-flask-2 -- -ra py313-test-instrumentation-flask-3_ubuntu-latest: name: instrumentation-flask-3 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-flask-3 -- -ra py314-test-instrumentation-flask-0_ubuntu-latest: name: instrumentation-flask-0 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-flask-0 -- -ra py314-test-instrumentation-flask-1_ubuntu-latest: name: instrumentation-flask-1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-flask-1 -- -ra py314-test-instrumentation-flask-2_ubuntu-latest: name: instrumentation-flask-2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-flask-2 -- -ra py314-test-instrumentation-flask-3_ubuntu-latest: name: instrumentation-flask-3 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-flask-3 -- -ra pypy3-test-instrumentation-flask-0_ubuntu-latest: name: instrumentation-flask-0 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-flask-0 -- -ra pypy3-test-instrumentation-flask-1_ubuntu-latest: name: instrumentation-flask-1 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-flask-1 -- -ra py310-test-instrumentation-urllib_ubuntu-latest: name: instrumentation-urllib 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-urllib -- -ra py311-test-instrumentation-urllib_ubuntu-latest: name: instrumentation-urllib 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-urllib -- -ra py312-test-instrumentation-urllib_ubuntu-latest: name: instrumentation-urllib 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-urllib -- -ra py313-test-instrumentation-urllib_ubuntu-latest: name: instrumentation-urllib 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-urllib -- -ra py314-test-instrumentation-urllib_ubuntu-latest: name: instrumentation-urllib 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-urllib -- -ra pypy3-test-instrumentation-urllib_ubuntu-latest: name: instrumentation-urllib pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-urllib -- -ra py310-test-instrumentation-urllib3-0_ubuntu-latest: name: instrumentation-urllib3-0 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-urllib3-0 -- -ra py310-test-instrumentation-urllib3-1_ubuntu-latest: name: instrumentation-urllib3-1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-urllib3-1 -- -ra py311-test-instrumentation-urllib3-0_ubuntu-latest: name: instrumentation-urllib3-0 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-urllib3-0 -- -ra py311-test-instrumentation-urllib3-1_ubuntu-latest: name: instrumentation-urllib3-1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-urllib3-1 -- -ra py312-test-instrumentation-urllib3-0_ubuntu-latest: name: instrumentation-urllib3-0 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-urllib3-0 -- -ra py312-test-instrumentation-urllib3-1_ubuntu-latest: name: instrumentation-urllib3-1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-urllib3-1 -- -ra py313-test-instrumentation-urllib3-0_ubuntu-latest: name: instrumentation-urllib3-0 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-urllib3-0 -- -ra py313-test-instrumentation-urllib3-1_ubuntu-latest: name: instrumentation-urllib3-1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-urllib3-1 -- -ra py314-test-instrumentation-urllib3-0_ubuntu-latest: name: instrumentation-urllib3-0 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-urllib3-0 -- -ra py314-test-instrumentation-urllib3-1_ubuntu-latest: name: instrumentation-urllib3-1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-urllib3-1 -- -ra pypy3-test-instrumentation-urllib3-0_ubuntu-latest: name: instrumentation-urllib3-0 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-urllib3-0 -- -ra pypy3-test-instrumentation-urllib3-1_ubuntu-latest: name: instrumentation-urllib3-1 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-urllib3-1 -- -ra py310-test-instrumentation-requests_ubuntu-latest: name: instrumentation-requests 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-requests -- -ra py311-test-instrumentation-requests_ubuntu-latest: name: instrumentation-requests 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-requests -- -ra py312-test-instrumentation-requests_ubuntu-latest: name: instrumentation-requests 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-requests -- -ra py313-test-instrumentation-requests_ubuntu-latest: name: instrumentation-requests 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-requests -- -ra py314-test-instrumentation-requests_ubuntu-latest: name: instrumentation-requests 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-requests -- -ra py310-test-instrumentation-starlette-oldest_ubuntu-latest: name: instrumentation-starlette-oldest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-starlette-oldest -- -ra py310-test-instrumentation-starlette-latest_ubuntu-latest: name: instrumentation-starlette-latest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-starlette-latest -- -ra py311-test-instrumentation-starlette-oldest_ubuntu-latest: name: instrumentation-starlette-oldest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-starlette-oldest -- -ra py311-test-instrumentation-starlette-latest_ubuntu-latest: name: instrumentation-starlette-latest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-starlette-latest -- -ra py312-test-instrumentation-starlette-oldest_ubuntu-latest: name: instrumentation-starlette-oldest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-starlette-oldest -- -ra py312-test-instrumentation-starlette-latest_ubuntu-latest: name: instrumentation-starlette-latest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-starlette-latest -- -ra py313-test-instrumentation-starlette-oldest_ubuntu-latest: name: instrumentation-starlette-oldest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-starlette-oldest -- -ra py313-test-instrumentation-starlette-latest_ubuntu-latest: name: instrumentation-starlette-latest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-starlette-latest -- -ra py314-test-instrumentation-starlette-oldest_ubuntu-latest: name: instrumentation-starlette-oldest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-starlette-oldest -- -ra py314-test-instrumentation-starlette-latest_ubuntu-latest: name: instrumentation-starlette-latest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-starlette-latest -- -ra pypy3-test-instrumentation-starlette-oldest_ubuntu-latest: name: instrumentation-starlette-oldest pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-starlette-oldest -- -ra pypy3-test-instrumentation-starlette-latest_ubuntu-latest: name: instrumentation-starlette-latest pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-starlette-latest -- -ra py310-test-instrumentation-structlog-oldest_ubuntu-latest: name: instrumentation-structlog-oldest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-structlog-oldest -- -ra py310-test-instrumentation-structlog-latest_ubuntu-latest: name: instrumentation-structlog-latest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-structlog-latest -- -ra py311-test-instrumentation-structlog-oldest_ubuntu-latest: name: instrumentation-structlog-oldest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-structlog-oldest -- -ra py311-test-instrumentation-structlog-latest_ubuntu-latest: name: instrumentation-structlog-latest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-structlog-latest -- -ra py312-test-instrumentation-structlog-oldest_ubuntu-latest: name: instrumentation-structlog-oldest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-structlog-oldest -- -ra py312-test-instrumentation-structlog-latest_ubuntu-latest: name: instrumentation-structlog-latest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-structlog-latest -- -ra py313-test-instrumentation-structlog-oldest_ubuntu-latest: name: instrumentation-structlog-oldest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-structlog-oldest -- -ra py313-test-instrumentation-structlog-latest_ubuntu-latest: name: instrumentation-structlog-latest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-structlog-latest -- -ra py314-test-instrumentation-structlog-oldest_ubuntu-latest: name: instrumentation-structlog-oldest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-structlog-oldest -- -ra py314-test-instrumentation-structlog-latest_ubuntu-latest: name: instrumentation-structlog-latest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-structlog-latest -- -ra pypy3-test-instrumentation-structlog-oldest_ubuntu-latest: name: instrumentation-structlog-oldest pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-structlog-oldest -- -ra pypy3-test-instrumentation-structlog-latest_ubuntu-latest: name: instrumentation-structlog-latest pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-structlog-latest -- -ra py310-test-instrumentation-jinja2_ubuntu-latest: name: instrumentation-jinja2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-jinja2 -- -ra py311-test-instrumentation-jinja2_ubuntu-latest: name: instrumentation-jinja2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-jinja2 -- -ra py312-test-instrumentation-jinja2_ubuntu-latest: name: instrumentation-jinja2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-jinja2 -- -ra py313-test-instrumentation-jinja2_ubuntu-latest: name: instrumentation-jinja2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-jinja2 -- -ra py314-test-instrumentation-jinja2_ubuntu-latest: name: instrumentation-jinja2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-jinja2 -- -ra pypy3-test-instrumentation-jinja2_ubuntu-latest: name: instrumentation-jinja2 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-jinja2 -- -ra py310-test-instrumentation-logging_ubuntu-latest: name: instrumentation-logging 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-logging -- -ra py311-test-instrumentation-logging_ubuntu-latest: name: instrumentation-logging 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-logging -- -ra py312-test-instrumentation-logging_ubuntu-latest: name: instrumentation-logging 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-logging -- -ra py313-test-instrumentation-logging_ubuntu-latest: name: instrumentation-logging 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-logging -- -ra py314-test-instrumentation-logging_ubuntu-latest: name: instrumentation-logging 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-logging -- -ra pypy3-test-instrumentation-logging_ubuntu-latest: name: instrumentation-logging pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-logging -- -ra py310-test-instrumentation-exceptions_ubuntu-latest: name: instrumentation-exceptions 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-exceptions -- -ra py311-test-instrumentation-exceptions_ubuntu-latest: name: instrumentation-exceptions 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-exceptions -- -ra py312-test-instrumentation-exceptions_ubuntu-latest: name: instrumentation-exceptions 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-exceptions -- -ra py313-test-instrumentation-exceptions_ubuntu-latest: name: instrumentation-exceptions 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-exceptions -- -ra py314-test-instrumentation-exceptions_ubuntu-latest: name: instrumentation-exceptions 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-exceptions -- -ra pypy3-test-instrumentation-exceptions_ubuntu-latest: name: instrumentation-exceptions pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-exceptions -- -ra py310-test-exporter-richconsole_ubuntu-latest: name: exporter-richconsole 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-exporter-richconsole -- -ra py311-test-exporter-richconsole_ubuntu-latest: name: exporter-richconsole 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-exporter-richconsole -- -ra py312-test-exporter-richconsole_ubuntu-latest: name: exporter-richconsole 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-exporter-richconsole -- -ra py313-test-exporter-richconsole_ubuntu-latest: name: exporter-richconsole 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-exporter-richconsole -- -ra py314-test-exporter-richconsole_ubuntu-latest: name: exporter-richconsole 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-exporter-richconsole -- -ra pypy3-test-exporter-richconsole_ubuntu-latest: name: exporter-richconsole pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-exporter-richconsole -- -ra py310-test-exporter-prometheus-remote-write_ubuntu-latest: name: exporter-prometheus-remote-write 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-exporter-prometheus-remote-write -- -ra py311-test-exporter-prometheus-remote-write_ubuntu-latest: name: exporter-prometheus-remote-write 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-exporter-prometheus-remote-write -- -ra py312-test-exporter-prometheus-remote-write_ubuntu-latest: name: exporter-prometheus-remote-write 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-exporter-prometheus-remote-write -- -ra py313-test-exporter-prometheus-remote-write_ubuntu-latest: name: exporter-prometheus-remote-write 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-exporter-prometheus-remote-write -- -ra py314-test-exporter-prometheus-remote-write_ubuntu-latest: name: exporter-prometheus-remote-write 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-exporter-prometheus-remote-write -- -ra pypy3-test-exporter-prometheus-remote-write_ubuntu-latest: name: exporter-prometheus-remote-write pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-exporter-prometheus-remote-write -- -ra py310-test-instrumentation-mysql-0_ubuntu-latest: name: instrumentation-mysql-0 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-mysql-0 -- -ra py310-test-instrumentation-mysql-1_ubuntu-latest: name: instrumentation-mysql-1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-mysql-1 -- -ra py310-test-instrumentation-mysql-2_ubuntu-latest: name: instrumentation-mysql-2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-mysql-2 -- -ra py311-test-instrumentation-mysql-0_ubuntu-latest: name: instrumentation-mysql-0 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-mysql-0 -- -ra py311-test-instrumentation-mysql-1_ubuntu-latest: name: instrumentation-mysql-1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-mysql-1 -- -ra py311-test-instrumentation-mysql-2_ubuntu-latest: name: instrumentation-mysql-2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-mysql-2 -- -ra py312-test-instrumentation-mysql-0_ubuntu-latest: name: instrumentation-mysql-0 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-mysql-0 -- -ra py312-test-instrumentation-mysql-1_ubuntu-latest: name: instrumentation-mysql-1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-mysql-1 -- -ra py312-test-instrumentation-mysql-2_ubuntu-latest: name: instrumentation-mysql-2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-mysql-2 -- -ra py313-test-instrumentation-mysql-0_ubuntu-latest: name: instrumentation-mysql-0 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-mysql-0 -- -ra py313-test-instrumentation-mysql-1_ubuntu-latest: name: instrumentation-mysql-1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-mysql-1 -- -ra py313-test-instrumentation-mysql-2_ubuntu-latest: name: instrumentation-mysql-2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-mysql-2 -- -ra py314-test-instrumentation-mysql-0_ubuntu-latest: name: instrumentation-mysql-0 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-mysql-0 -- -ra py314-test-instrumentation-mysql-1_ubuntu-latest: name: instrumentation-mysql-1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-mysql-1 -- -ra py314-test-instrumentation-mysql-2_ubuntu-latest: name: instrumentation-mysql-2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-mysql-2 -- -ra pypy3-test-instrumentation-mysql-0_ubuntu-latest: name: instrumentation-mysql-0 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-mysql-0 -- -ra pypy3-test-instrumentation-mysql-1_ubuntu-latest: name: instrumentation-mysql-1 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-mysql-1 -- -ra pypy3-test-instrumentation-mysql-2_ubuntu-latest: name: instrumentation-mysql-2 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-mysql-2 -- -ra py310-test-instrumentation-mysqlclient_ubuntu-latest: name: instrumentation-mysqlclient 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-mysqlclient -- -ra py311-test-instrumentation-mysqlclient_ubuntu-latest: name: instrumentation-mysqlclient 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-mysqlclient -- -ra py312-test-instrumentation-mysqlclient_ubuntu-latest: name: instrumentation-mysqlclient 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-mysqlclient -- -ra py313-test-instrumentation-mysqlclient_ubuntu-latest: name: instrumentation-mysqlclient 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-mysqlclient -- -ra py314-test-instrumentation-mysqlclient_ubuntu-latest: name: instrumentation-mysqlclient 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-mysqlclient -- -ra pypy3-test-instrumentation-mysqlclient_ubuntu-latest: name: instrumentation-mysqlclient pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-mysqlclient -- -ra py310-test-instrumentation-psycopg2_ubuntu-latest: name: instrumentation-psycopg2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-psycopg2 -- -ra py311-test-instrumentation-psycopg2_ubuntu-latest: name: instrumentation-psycopg2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-psycopg2 -- -ra py312-test-instrumentation-psycopg2_ubuntu-latest: name: instrumentation-psycopg2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-psycopg2 -- -ra py313-test-instrumentation-psycopg2_ubuntu-latest: name: instrumentation-psycopg2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-psycopg2 -- -ra py314-test-instrumentation-psycopg2_ubuntu-latest: name: instrumentation-psycopg2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-psycopg2 -- -ra py310-test-instrumentation-psycopg2-binary_ubuntu-latest: name: instrumentation-psycopg2-binary 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-psycopg2-binary -- -ra py311-test-instrumentation-psycopg2-binary_ubuntu-latest: name: instrumentation-psycopg2-binary 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-psycopg2-binary -- -ra py312-test-instrumentation-psycopg2-binary_ubuntu-latest: name: instrumentation-psycopg2-binary 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-psycopg2-binary -- -ra py313-test-instrumentation-psycopg2-binary_ubuntu-latest: name: instrumentation-psycopg2-binary 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-psycopg2-binary -- -ra py314-test-instrumentation-psycopg2-binary_ubuntu-latest: name: instrumentation-psycopg2-binary 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-psycopg2-binary -- -ra py310-test-instrumentation-psycopg_ubuntu-latest: name: instrumentation-psycopg 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-psycopg -- -ra py311-test-instrumentation-psycopg_ubuntu-latest: name: instrumentation-psycopg 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-psycopg -- -ra py312-test-instrumentation-psycopg_ubuntu-latest: name: instrumentation-psycopg 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-psycopg -- -ra py313-test-instrumentation-psycopg_ubuntu-latest: name: instrumentation-psycopg 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-psycopg -- -ra py314-test-instrumentation-psycopg_ubuntu-latest: name: instrumentation-psycopg 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-psycopg -- -ra pypy3-test-instrumentation-psycopg_ubuntu-latest: name: instrumentation-psycopg pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-psycopg -- -ra py310-test-instrumentation-pymemcache-0_ubuntu-latest: name: instrumentation-pymemcache-0 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-pymemcache-0 -- -ra py310-test-instrumentation-pymemcache-1_ubuntu-latest: name: instrumentation-pymemcache-1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-pymemcache-1 -- -ra py310-test-instrumentation-pymemcache-2_ubuntu-latest: name: instrumentation-pymemcache-2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-pymemcache-2 -- -ra py310-test-instrumentation-pymemcache-3_ubuntu-latest: name: instrumentation-pymemcache-3 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-pymemcache-3 -- -ra py310-test-instrumentation-pymemcache-4_ubuntu-latest: name: instrumentation-pymemcache-4 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-pymemcache-4 -- -ra py311-test-instrumentation-pymemcache-0_ubuntu-latest: name: instrumentation-pymemcache-0 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-pymemcache-0 -- -ra py311-test-instrumentation-pymemcache-1_ubuntu-latest: name: instrumentation-pymemcache-1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-pymemcache-1 -- -ra py311-test-instrumentation-pymemcache-2_ubuntu-latest: name: instrumentation-pymemcache-2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-pymemcache-2 -- -ra py311-test-instrumentation-pymemcache-3_ubuntu-latest: name: instrumentation-pymemcache-3 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-pymemcache-3 -- -ra py311-test-instrumentation-pymemcache-4_ubuntu-latest: name: instrumentation-pymemcache-4 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-pymemcache-4 -- -ra py312-test-instrumentation-pymemcache-0_ubuntu-latest: name: instrumentation-pymemcache-0 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-pymemcache-0 -- -ra py312-test-instrumentation-pymemcache-1_ubuntu-latest: name: instrumentation-pymemcache-1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-pymemcache-1 -- -ra py312-test-instrumentation-pymemcache-2_ubuntu-latest: name: instrumentation-pymemcache-2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-pymemcache-2 -- -ra py312-test-instrumentation-pymemcache-3_ubuntu-latest: name: instrumentation-pymemcache-3 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-pymemcache-3 -- -ra py312-test-instrumentation-pymemcache-4_ubuntu-latest: name: instrumentation-pymemcache-4 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-pymemcache-4 -- -ra py313-test-instrumentation-pymemcache-0_ubuntu-latest: name: instrumentation-pymemcache-0 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-pymemcache-0 -- -ra py313-test-instrumentation-pymemcache-1_ubuntu-latest: name: instrumentation-pymemcache-1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-pymemcache-1 -- -ra py313-test-instrumentation-pymemcache-2_ubuntu-latest: name: instrumentation-pymemcache-2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-pymemcache-2 -- -ra py313-test-instrumentation-pymemcache-3_ubuntu-latest: name: instrumentation-pymemcache-3 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-pymemcache-3 -- -ra py313-test-instrumentation-pymemcache-4_ubuntu-latest: name: instrumentation-pymemcache-4 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-pymemcache-4 -- -ra py314-test-instrumentation-pymemcache-0_ubuntu-latest: name: instrumentation-pymemcache-0 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-pymemcache-0 -- -ra py314-test-instrumentation-pymemcache-1_ubuntu-latest: name: instrumentation-pymemcache-1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-pymemcache-1 -- -ra py314-test-instrumentation-pymemcache-2_ubuntu-latest: name: instrumentation-pymemcache-2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-pymemcache-2 -- -ra py314-test-instrumentation-pymemcache-3_ubuntu-latest: name: instrumentation-pymemcache-3 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-pymemcache-3 -- -ra py314-test-instrumentation-pymemcache-4_ubuntu-latest: name: instrumentation-pymemcache-4 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-pymemcache-4 -- -ra pypy3-test-instrumentation-pymemcache-0_ubuntu-latest: name: instrumentation-pymemcache-0 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-pymemcache-0 -- -ra pypy3-test-instrumentation-pymemcache-1_ubuntu-latest: name: instrumentation-pymemcache-1 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-pymemcache-1 -- -ra pypy3-test-instrumentation-pymemcache-2_ubuntu-latest: name: instrumentation-pymemcache-2 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-pymemcache-2 -- -ra pypy3-test-instrumentation-pymemcache-3_ubuntu-latest: name: instrumentation-pymemcache-3 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-pymemcache-3 -- -ra pypy3-test-instrumentation-pymemcache-4_ubuntu-latest: name: instrumentation-pymemcache-4 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-pymemcache-4 -- -ra py310-test-instrumentation-pymongo_ubuntu-latest: name: instrumentation-pymongo 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-pymongo -- -ra py311-test-instrumentation-pymongo_ubuntu-latest: name: instrumentation-pymongo 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-pymongo -- -ra py312-test-instrumentation-pymongo_ubuntu-latest: name: instrumentation-pymongo 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-pymongo -- -ra py313-test-instrumentation-pymongo_ubuntu-latest: name: instrumentation-pymongo 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-pymongo -- -ra py314-test-instrumentation-pymongo_ubuntu-latest: name: instrumentation-pymongo 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-pymongo -- -ra pypy3-test-instrumentation-pymongo_ubuntu-latest: name: instrumentation-pymongo pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-pymongo -- -ra py310-test-instrumentation-pymysql_ubuntu-latest: name: instrumentation-pymysql 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-pymysql -- -ra py311-test-instrumentation-pymysql_ubuntu-latest: name: instrumentation-pymysql 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-pymysql -- -ra py312-test-instrumentation-pymysql_ubuntu-latest: name: instrumentation-pymysql 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-pymysql -- -ra py313-test-instrumentation-pymysql_ubuntu-latest: name: instrumentation-pymysql 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-pymysql -- -ra py314-test-instrumentation-pymysql_ubuntu-latest: name: instrumentation-pymysql 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-pymysql -- -ra pypy3-test-instrumentation-pymysql_ubuntu-latest: name: instrumentation-pymysql pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-pymysql -- -ra py310-test-instrumentation-pymssql_ubuntu-latest: name: instrumentation-pymssql 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-pymssql -- -ra py311-test-instrumentation-pymssql_ubuntu-latest: name: instrumentation-pymssql 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-pymssql -- -ra py312-test-instrumentation-pymssql_ubuntu-latest: name: instrumentation-pymssql 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-pymssql -- -ra py313-test-instrumentation-pymssql_ubuntu-latest: name: instrumentation-pymssql 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-pymssql -- -ra py314-test-instrumentation-pymssql_ubuntu-latest: name: instrumentation-pymssql 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-pymssql -- -ra py310-test-instrumentation-pyramid_ubuntu-latest: name: instrumentation-pyramid 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-pyramid -- -ra py311-test-instrumentation-pyramid_ubuntu-latest: name: instrumentation-pyramid 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-pyramid -- -ra py312-test-instrumentation-pyramid_ubuntu-latest: name: instrumentation-pyramid 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-pyramid -- -ra py313-test-instrumentation-pyramid_ubuntu-latest: name: instrumentation-pyramid 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-pyramid -- -ra py314-test-instrumentation-pyramid_ubuntu-latest: name: instrumentation-pyramid 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-pyramid -- -ra pypy3-test-instrumentation-pyramid_ubuntu-latest: name: instrumentation-pyramid pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-pyramid -- -ra py310-test-instrumentation-asgi_ubuntu-latest: name: instrumentation-asgi 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-asgi -- -ra py311-test-instrumentation-asgi_ubuntu-latest: name: instrumentation-asgi 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-asgi -- -ra py312-test-instrumentation-asgi_ubuntu-latest: name: instrumentation-asgi 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-asgi -- -ra py313-test-instrumentation-asgi_ubuntu-latest: name: instrumentation-asgi 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-asgi -- -ra py314-test-instrumentation-asgi_ubuntu-latest: name: instrumentation-asgi 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-asgi -- -ra pypy3-test-instrumentation-asgi_ubuntu-latest: name: instrumentation-asgi pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-asgi -- -ra py310-test-instrumentation-asyncpg-wrapt1_ubuntu-latest: name: instrumentation-asyncpg-wrapt1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-asyncpg-wrapt1 -- -ra py310-test-instrumentation-asyncpg-wrapt2_ubuntu-latest: name: instrumentation-asyncpg-wrapt2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-asyncpg-wrapt2 -- -ra py311-test-instrumentation-asyncpg-wrapt1_ubuntu-latest: name: instrumentation-asyncpg-wrapt1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-asyncpg-wrapt1 -- -ra py311-test-instrumentation-asyncpg-wrapt2_ubuntu-latest: name: instrumentation-asyncpg-wrapt2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-asyncpg-wrapt2 -- -ra py312-test-instrumentation-asyncpg-wrapt1_ubuntu-latest: name: instrumentation-asyncpg-wrapt1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-asyncpg-wrapt1 -- -ra py312-test-instrumentation-asyncpg-wrapt2_ubuntu-latest: name: instrumentation-asyncpg-wrapt2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-asyncpg-wrapt2 -- -ra py313-test-instrumentation-asyncpg-wrapt1_ubuntu-latest: name: instrumentation-asyncpg-wrapt1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-asyncpg-wrapt1 -- -ra py313-test-instrumentation-asyncpg-wrapt2_ubuntu-latest: name: instrumentation-asyncpg-wrapt2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-asyncpg-wrapt2 -- -ra py314-test-instrumentation-asyncpg-wrapt1_ubuntu-latest: name: instrumentation-asyncpg-wrapt1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-asyncpg-wrapt1 -- -ra py314-test-instrumentation-asyncpg-wrapt2_ubuntu-latest: name: instrumentation-asyncpg-wrapt2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-asyncpg-wrapt2 -- -ra py310-test-instrumentation-sqlite3_ubuntu-latest: name: instrumentation-sqlite3 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-sqlite3 -- -ra py311-test-instrumentation-sqlite3_ubuntu-latest: name: instrumentation-sqlite3 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-sqlite3 -- -ra py312-test-instrumentation-sqlite3_ubuntu-latest: name: instrumentation-sqlite3 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-sqlite3 -- -ra py313-test-instrumentation-sqlite3_ubuntu-latest: name: instrumentation-sqlite3 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-sqlite3 -- -ra py314-test-instrumentation-sqlite3_ubuntu-latest: name: instrumentation-sqlite3 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-sqlite3 -- -ra pypy3-test-instrumentation-sqlite3_ubuntu-latest: name: instrumentation-sqlite3 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-sqlite3 -- -ra py310-test-instrumentation-wsgi_ubuntu-latest: name: instrumentation-wsgi 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-wsgi -- -ra py311-test-instrumentation-wsgi_ubuntu-latest: name: instrumentation-wsgi 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-wsgi -- -ra py312-test-instrumentation-wsgi_ubuntu-latest: name: instrumentation-wsgi 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-wsgi -- -ra py313-test-instrumentation-wsgi_ubuntu-latest: name: instrumentation-wsgi 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-wsgi -- -ra py314-test-instrumentation-wsgi_ubuntu-latest: name: instrumentation-wsgi 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-wsgi -- -ra pypy3-test-instrumentation-wsgi_ubuntu-latest: name: instrumentation-wsgi pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-wsgi -- -ra py310-test-instrumentation-grpc-0-wrapt1_ubuntu-latest: name: instrumentation-grpc-0-wrapt1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-grpc-0-wrapt1 -- -ra py310-test-instrumentation-grpc-0-wrapt2_ubuntu-latest: name: instrumentation-grpc-0-wrapt2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-grpc-0-wrapt2 -- -ra py310-test-instrumentation-grpc-1-wrapt1_ubuntu-latest: name: instrumentation-grpc-1-wrapt1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-grpc-1-wrapt1 -- -ra py310-test-instrumentation-grpc-1-wrapt2_ubuntu-latest: name: instrumentation-grpc-1-wrapt2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-grpc-1-wrapt2 -- -ra py311-test-instrumentation-grpc-0-wrapt1_ubuntu-latest: name: instrumentation-grpc-0-wrapt1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-grpc-0-wrapt1 -- -ra py311-test-instrumentation-grpc-0-wrapt2_ubuntu-latest: name: instrumentation-grpc-0-wrapt2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-grpc-0-wrapt2 -- -ra py311-test-instrumentation-grpc-1-wrapt1_ubuntu-latest: name: instrumentation-grpc-1-wrapt1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-grpc-1-wrapt1 -- -ra py311-test-instrumentation-grpc-1-wrapt2_ubuntu-latest: name: instrumentation-grpc-1-wrapt2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-grpc-1-wrapt2 -- -ra py312-test-instrumentation-grpc-0-wrapt1_ubuntu-latest: name: instrumentation-grpc-0-wrapt1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-grpc-0-wrapt1 -- -ra py312-test-instrumentation-grpc-0-wrapt2_ubuntu-latest: name: instrumentation-grpc-0-wrapt2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-grpc-0-wrapt2 -- -ra py312-test-instrumentation-grpc-1-wrapt1_ubuntu-latest: name: instrumentation-grpc-1-wrapt1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-grpc-1-wrapt1 -- -ra py312-test-instrumentation-grpc-1-wrapt2_ubuntu-latest: name: instrumentation-grpc-1-wrapt2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-grpc-1-wrapt2 -- -ra py313-test-instrumentation-grpc-1-wrapt1_ubuntu-latest: name: instrumentation-grpc-1-wrapt1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-grpc-1-wrapt1 -- -ra py313-test-instrumentation-grpc-1-wrapt2_ubuntu-latest: name: instrumentation-grpc-1-wrapt2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-grpc-1-wrapt2 -- -ra py314-test-instrumentation-grpc-1-wrapt1_ubuntu-latest: name: instrumentation-grpc-1-wrapt1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-grpc-1-wrapt1 -- -ra py314-test-instrumentation-grpc-1-wrapt2_ubuntu-latest: name: instrumentation-grpc-1-wrapt2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-grpc-1-wrapt2 -- -ra py310-test-instrumentation-sqlalchemy-1_ubuntu-latest: name: instrumentation-sqlalchemy-1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-sqlalchemy-1 -- -ra py310-test-instrumentation-sqlalchemy-2_ubuntu-latest: name: instrumentation-sqlalchemy-2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-sqlalchemy-2 -- -ra py311-test-instrumentation-sqlalchemy-1_ubuntu-latest: name: instrumentation-sqlalchemy-1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-sqlalchemy-1 -- -ra py311-test-instrumentation-sqlalchemy-2_ubuntu-latest: name: instrumentation-sqlalchemy-2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-sqlalchemy-2 -- -ra py312-test-instrumentation-sqlalchemy-1_ubuntu-latest: name: instrumentation-sqlalchemy-1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-sqlalchemy-1 -- -ra py312-test-instrumentation-sqlalchemy-2_ubuntu-latest: name: instrumentation-sqlalchemy-2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-sqlalchemy-2 -- -ra py313-test-instrumentation-sqlalchemy-1_ubuntu-latest: name: instrumentation-sqlalchemy-1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-sqlalchemy-1 -- -ra py313-test-instrumentation-sqlalchemy-2_ubuntu-latest: name: instrumentation-sqlalchemy-2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-sqlalchemy-2 -- -ra py314-test-instrumentation-sqlalchemy-2_ubuntu-latest: name: instrumentation-sqlalchemy-2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-sqlalchemy-2 -- -ra pypy3-test-instrumentation-sqlalchemy-1_ubuntu-latest: name: instrumentation-sqlalchemy-1 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-sqlalchemy-1 -- -ra pypy3-test-instrumentation-sqlalchemy-2_ubuntu-latest: name: instrumentation-sqlalchemy-2 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-sqlalchemy-2 -- -ra py310-test-instrumentation-redis_ubuntu-latest: name: instrumentation-redis 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-redis -- -ra py311-test-instrumentation-redis_ubuntu-latest: name: instrumentation-redis 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-redis -- -ra py312-test-instrumentation-redis_ubuntu-latest: name: instrumentation-redis 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-redis -- -ra py313-test-instrumentation-redis_ubuntu-latest: name: instrumentation-redis 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-redis -- -ra py314-test-instrumentation-redis_ubuntu-latest: name: instrumentation-redis 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-redis -- -ra pypy3-test-instrumentation-redis_ubuntu-latest: name: instrumentation-redis pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-redis -- -ra py310-test-instrumentation-remoulade_ubuntu-latest: name: instrumentation-remoulade 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-remoulade -- -ra py311-test-instrumentation-remoulade_ubuntu-latest: name: instrumentation-remoulade 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-remoulade -- -ra py312-test-instrumentation-remoulade_ubuntu-latest: name: instrumentation-remoulade 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-remoulade -- -ra py313-test-instrumentation-remoulade_ubuntu-latest: name: instrumentation-remoulade 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-remoulade -- -ra py314-test-instrumentation-remoulade_ubuntu-latest: name: instrumentation-remoulade 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-remoulade -- -ra py310-test-instrumentation-celery_ubuntu-latest: name: instrumentation-celery 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-celery -- -ra py311-test-instrumentation-celery_ubuntu-latest: name: instrumentation-celery 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-celery -- -ra py312-test-instrumentation-celery_ubuntu-latest: name: instrumentation-celery 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-celery -- -ra py313-test-instrumentation-celery_ubuntu-latest: name: instrumentation-celery 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-celery -- -ra py314-test-instrumentation-celery_ubuntu-latest: name: instrumentation-celery 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-celery -- -ra pypy3-test-instrumentation-celery_ubuntu-latest: name: instrumentation-celery pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-celery -- -ra py310-test-instrumentation-system-metrics_ubuntu-latest: name: instrumentation-system-metrics 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-system-metrics -- -ra py311-test-instrumentation-system-metrics_ubuntu-latest: name: instrumentation-system-metrics 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-system-metrics -- -ra py312-test-instrumentation-system-metrics_ubuntu-latest: name: instrumentation-system-metrics 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-system-metrics -- -ra py313-test-instrumentation-system-metrics_ubuntu-latest: name: instrumentation-system-metrics 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-system-metrics -- -ra py314-test-instrumentation-system-metrics_ubuntu-latest: name: instrumentation-system-metrics 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-system-metrics -- -ra pypy3-test-instrumentation-system-metrics_ubuntu-latest: name: instrumentation-system-metrics pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-system-metrics -- -ra py310-test-instrumentation-threading_ubuntu-latest: name: instrumentation-threading 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-threading -- -ra py311-test-instrumentation-threading_ubuntu-latest: name: instrumentation-threading 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-threading -- -ra py312-test-instrumentation-threading_ubuntu-latest: name: instrumentation-threading 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-threading -- -ra py313-test-instrumentation-threading_ubuntu-latest: name: instrumentation-threading 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-threading -- -ra py314-test-instrumentation-threading_ubuntu-latest: name: instrumentation-threading 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-threading -- -ra pypy3-test-instrumentation-threading_ubuntu-latest: name: instrumentation-threading pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-threading -- -ra py310-test-instrumentation-tornado_ubuntu-latest: name: instrumentation-tornado 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-tornado -- -ra py311-test-instrumentation-tornado_ubuntu-latest: name: instrumentation-tornado 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-tornado -- -ra py312-test-instrumentation-tornado_ubuntu-latest: name: instrumentation-tornado 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-tornado -- -ra py313-test-instrumentation-tornado_ubuntu-latest: name: instrumentation-tornado 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-tornado -- -ra py314-test-instrumentation-tornado_ubuntu-latest: name: instrumentation-tornado 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-tornado -- -ra pypy3-test-instrumentation-tornado_ubuntu-latest: name: instrumentation-tornado pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-tornado -- -ra py310-test-instrumentation-tortoiseorm_ubuntu-latest: name: instrumentation-tortoiseorm 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-tortoiseorm -- -ra py311-test-instrumentation-tortoiseorm_ubuntu-latest: name: instrumentation-tortoiseorm 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-tortoiseorm -- -ra py312-test-instrumentation-tortoiseorm_ubuntu-latest: name: instrumentation-tortoiseorm 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-tortoiseorm -- -ra py313-test-instrumentation-tortoiseorm_ubuntu-latest: name: instrumentation-tortoiseorm 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-tortoiseorm -- -ra py314-test-instrumentation-tortoiseorm_ubuntu-latest: name: instrumentation-tortoiseorm 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-tortoiseorm -- -ra pypy3-test-instrumentation-tortoiseorm_ubuntu-latest: name: instrumentation-tortoiseorm pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-tortoiseorm -- -ra py310-test-instrumentation-httpx-0-wrapt1_ubuntu-latest: name: instrumentation-httpx-0-wrapt1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-httpx-0-wrapt1 -- -ra py310-test-instrumentation-httpx-0-wrapt2_ubuntu-latest: name: instrumentation-httpx-0-wrapt2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-httpx-0-wrapt2 -- -ra py310-test-instrumentation-httpx-1-wrapt1_ubuntu-latest: name: instrumentation-httpx-1-wrapt1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-httpx-1-wrapt1 -- -ra py310-test-instrumentation-httpx-1-wrapt2_ubuntu-latest: name: instrumentation-httpx-1-wrapt2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-httpx-1-wrapt2 -- -ra py311-test-instrumentation-httpx-0-wrapt1_ubuntu-latest: name: instrumentation-httpx-0-wrapt1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-httpx-0-wrapt1 -- -ra py311-test-instrumentation-httpx-0-wrapt2_ubuntu-latest: name: instrumentation-httpx-0-wrapt2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-httpx-0-wrapt2 -- -ra py311-test-instrumentation-httpx-1-wrapt1_ubuntu-latest: name: instrumentation-httpx-1-wrapt1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-httpx-1-wrapt1 -- -ra py311-test-instrumentation-httpx-1-wrapt2_ubuntu-latest: name: instrumentation-httpx-1-wrapt2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-httpx-1-wrapt2 -- -ra py312-test-instrumentation-httpx-0-wrapt1_ubuntu-latest: name: instrumentation-httpx-0-wrapt1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-httpx-0-wrapt1 -- -ra py312-test-instrumentation-httpx-0-wrapt2_ubuntu-latest: name: instrumentation-httpx-0-wrapt2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-httpx-0-wrapt2 -- -ra py312-test-instrumentation-httpx-1-wrapt1_ubuntu-latest: name: instrumentation-httpx-1-wrapt1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-httpx-1-wrapt1 -- -ra py312-test-instrumentation-httpx-1-wrapt2_ubuntu-latest: name: instrumentation-httpx-1-wrapt2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-httpx-1-wrapt2 -- -ra py310-test-instrumentation-httpx-2-wrapt1_ubuntu-latest: name: instrumentation-httpx-2-wrapt1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-httpx-2-wrapt1 -- -ra py310-test-instrumentation-httpx-2-wrapt2_ubuntu-latest: name: instrumentation-httpx-2-wrapt2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-httpx-2-wrapt2 -- -ra py311-test-instrumentation-httpx-2-wrapt1_ubuntu-latest: name: instrumentation-httpx-2-wrapt1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-httpx-2-wrapt1 -- -ra py311-test-instrumentation-httpx-2-wrapt2_ubuntu-latest: name: instrumentation-httpx-2-wrapt2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-httpx-2-wrapt2 -- -ra py312-test-instrumentation-httpx-2-wrapt1_ubuntu-latest: name: instrumentation-httpx-2-wrapt1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-httpx-2-wrapt1 -- -ra py312-test-instrumentation-httpx-2-wrapt2_ubuntu-latest: name: instrumentation-httpx-2-wrapt2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-httpx-2-wrapt2 -- -ra py313-test-instrumentation-httpx-2-wrapt1_ubuntu-latest: name: instrumentation-httpx-2-wrapt1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-httpx-2-wrapt1 -- -ra py313-test-instrumentation-httpx-2-wrapt2_ubuntu-latest: name: instrumentation-httpx-2-wrapt2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-httpx-2-wrapt2 -- -ra py314-test-instrumentation-httpx-2-wrapt1_ubuntu-latest: name: instrumentation-httpx-2-wrapt1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-httpx-2-wrapt1 -- -ra py314-test-instrumentation-httpx-2-wrapt2_ubuntu-latest: name: instrumentation-httpx-2-wrapt2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-httpx-2-wrapt2 -- -ra py310-test-instrumentation-httpx-3-wrapt1_ubuntu-latest: name: instrumentation-httpx-3-wrapt1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-httpx-3-wrapt1 -- -ra py310-test-instrumentation-httpx-3-wrapt2_ubuntu-latest: name: instrumentation-httpx-3-wrapt2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-httpx-3-wrapt2 -- -ra py311-test-instrumentation-httpx-3-wrapt1_ubuntu-latest: name: instrumentation-httpx-3-wrapt1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-httpx-3-wrapt1 -- -ra py311-test-instrumentation-httpx-3-wrapt2_ubuntu-latest: name: instrumentation-httpx-3-wrapt2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-httpx-3-wrapt2 -- -ra py312-test-instrumentation-httpx-3-wrapt1_ubuntu-latest: name: instrumentation-httpx-3-wrapt1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-httpx-3-wrapt1 -- -ra py312-test-instrumentation-httpx-3-wrapt2_ubuntu-latest: name: instrumentation-httpx-3-wrapt2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-httpx-3-wrapt2 -- -ra py313-test-instrumentation-httpx-3-wrapt1_ubuntu-latest: name: instrumentation-httpx-3-wrapt1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-httpx-3-wrapt1 -- -ra py313-test-instrumentation-httpx-3-wrapt2_ubuntu-latest: name: instrumentation-httpx-3-wrapt2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-httpx-3-wrapt2 -- -ra py314-test-instrumentation-httpx-3-wrapt1_ubuntu-latest: name: instrumentation-httpx-3-wrapt1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-httpx-3-wrapt1 -- -ra py314-test-instrumentation-httpx-3-wrapt2_ubuntu-latest: name: instrumentation-httpx-3-wrapt2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-httpx-3-wrapt2 -- -ra py313-test-instrumentation-httpx-1-wrapt1_ubuntu-latest: name: instrumentation-httpx-1-wrapt1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-httpx-1-wrapt1 -- -ra py313-test-instrumentation-httpx-1-wrapt2_ubuntu-latest: name: instrumentation-httpx-1-wrapt2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-httpx-1-wrapt2 -- -ra py314-test-instrumentation-httpx-1-wrapt1_ubuntu-latest: name: instrumentation-httpx-1-wrapt1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-httpx-1-wrapt1 -- -ra py314-test-instrumentation-httpx-1-wrapt2_ubuntu-latest: name: instrumentation-httpx-1-wrapt2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-httpx-1-wrapt2 -- -ra pypy3-test-instrumentation-httpx-0-wrapt2_ubuntu-latest: name: instrumentation-httpx-0-wrapt2 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-httpx-0-wrapt2 -- -ra pypy3-test-instrumentation-httpx-1-wrapt2_ubuntu-latest: name: instrumentation-httpx-1-wrapt2 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-httpx-1-wrapt2 -- -ra py310-test-util-http_ubuntu-latest: name: util-http 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-util-http -- -ra py311-test-util-http_ubuntu-latest: name: util-http 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-util-http -- -ra py312-test-util-http_ubuntu-latest: name: util-http 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-util-http -- -ra py313-test-util-http_ubuntu-latest: name: util-http 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-util-http -- -ra py314-test-util-http_ubuntu-latest: name: util-http 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-util-http -- -ra pypy3-test-util-http_ubuntu-latest: name: util-http pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-util-http -- -ra py310-test-exporter-credential-provider-gcp_ubuntu-latest: name: exporter-credential-provider-gcp 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-exporter-credential-provider-gcp -- -ra py311-test-exporter-credential-provider-gcp_ubuntu-latest: name: exporter-credential-provider-gcp 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-exporter-credential-provider-gcp -- -ra py312-test-exporter-credential-provider-gcp_ubuntu-latest: name: exporter-credential-provider-gcp 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-exporter-credential-provider-gcp -- -ra py313-test-exporter-credential-provider-gcp_ubuntu-latest: name: exporter-credential-provider-gcp 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-exporter-credential-provider-gcp -- -ra py314-test-exporter-credential-provider-gcp_ubuntu-latest: name: exporter-credential-provider-gcp 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-exporter-credential-provider-gcp -- -ra py310-test-util-genai_ubuntu-latest: name: util-genai 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-util-genai -- -ra py311-test-util-genai_ubuntu-latest: name: util-genai 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-util-genai -- -ra py312-test-util-genai_ubuntu-latest: name: util-genai 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-util-genai -- -ra py313-test-util-genai_ubuntu-latest: name: util-genai 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-util-genai -- -ra py314-test-util-genai_ubuntu-latest: name: util-genai 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-util-genai -- -ra pypy3-test-util-genai_ubuntu-latest: name: util-genai pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-util-genai -- -ra py310-test-propagator-aws-xray-0_ubuntu-latest: name: propagator-aws-xray-0 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-propagator-aws-xray-0 -- -ra py310-test-propagator-aws-xray-1_ubuntu-latest: name: propagator-aws-xray-1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-propagator-aws-xray-1 -- -ra py311-test-propagator-aws-xray-0_ubuntu-latest: name: propagator-aws-xray-0 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-propagator-aws-xray-0 -- -ra py311-test-propagator-aws-xray-1_ubuntu-latest: name: propagator-aws-xray-1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-propagator-aws-xray-1 -- -ra py312-test-propagator-aws-xray-0_ubuntu-latest: name: propagator-aws-xray-0 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-propagator-aws-xray-0 -- -ra py312-test-propagator-aws-xray-1_ubuntu-latest: name: propagator-aws-xray-1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-propagator-aws-xray-1 -- -ra py313-test-propagator-aws-xray-0_ubuntu-latest: name: propagator-aws-xray-0 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-propagator-aws-xray-0 -- -ra py313-test-propagator-aws-xray-1_ubuntu-latest: name: propagator-aws-xray-1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-propagator-aws-xray-1 -- -ra py314-test-propagator-aws-xray-0_ubuntu-latest: name: propagator-aws-xray-0 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-propagator-aws-xray-0 -- -ra py314-test-propagator-aws-xray-1_ubuntu-latest: name: propagator-aws-xray-1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-propagator-aws-xray-1 -- -ra pypy3-test-propagator-aws-xray-0_ubuntu-latest: name: propagator-aws-xray-0 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-propagator-aws-xray-0 -- -ra pypy3-test-propagator-aws-xray-1_ubuntu-latest: name: propagator-aws-xray-1 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-propagator-aws-xray-1 -- -ra py310-test-propagator-ot-trace_ubuntu-latest: name: propagator-ot-trace 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-propagator-ot-trace -- -ra py311-test-propagator-ot-trace_ubuntu-latest: name: propagator-ot-trace 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-propagator-ot-trace -- -ra py312-test-propagator-ot-trace_ubuntu-latest: name: propagator-ot-trace 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-propagator-ot-trace -- -ra py313-test-propagator-ot-trace_ubuntu-latest: name: propagator-ot-trace 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-propagator-ot-trace -- -ra py314-test-propagator-ot-trace_ubuntu-latest: name: propagator-ot-trace 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-propagator-ot-trace -- -ra pypy3-test-propagator-ot-trace_ubuntu-latest: name: propagator-ot-trace pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-propagator-ot-trace -- -ra py310-test-instrumentation-sio-pika-0-wrapt1_ubuntu-latest: name: instrumentation-sio-pika-0-wrapt1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-sio-pika-0-wrapt1 -- -ra py310-test-instrumentation-sio-pika-0-wrapt2_ubuntu-latest: name: instrumentation-sio-pika-0-wrapt2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-sio-pika-0-wrapt2 -- -ra py310-test-instrumentation-sio-pika-1-wrapt1_ubuntu-latest: name: instrumentation-sio-pika-1-wrapt1 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-sio-pika-1-wrapt1 -- -ra py310-test-instrumentation-sio-pika-1-wrapt2_ubuntu-latest: name: instrumentation-sio-pika-1-wrapt2 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-sio-pika-1-wrapt2 -- -ra py311-test-instrumentation-sio-pika-0-wrapt1_ubuntu-latest: name: instrumentation-sio-pika-0-wrapt1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-sio-pika-0-wrapt1 -- -ra py311-test-instrumentation-sio-pika-0-wrapt2_ubuntu-latest: name: instrumentation-sio-pika-0-wrapt2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-sio-pika-0-wrapt2 -- -ra py311-test-instrumentation-sio-pika-1-wrapt1_ubuntu-latest: name: instrumentation-sio-pika-1-wrapt1 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-sio-pika-1-wrapt1 -- -ra py311-test-instrumentation-sio-pika-1-wrapt2_ubuntu-latest: name: instrumentation-sio-pika-1-wrapt2 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-sio-pika-1-wrapt2 -- -ra py312-test-instrumentation-sio-pika-0-wrapt1_ubuntu-latest: name: instrumentation-sio-pika-0-wrapt1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-sio-pika-0-wrapt1 -- -ra py312-test-instrumentation-sio-pika-0-wrapt2_ubuntu-latest: name: instrumentation-sio-pika-0-wrapt2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-sio-pika-0-wrapt2 -- -ra py312-test-instrumentation-sio-pika-1-wrapt1_ubuntu-latest: name: instrumentation-sio-pika-1-wrapt1 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-sio-pika-1-wrapt1 -- -ra py312-test-instrumentation-sio-pika-1-wrapt2_ubuntu-latest: name: instrumentation-sio-pika-1-wrapt2 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-sio-pika-1-wrapt2 -- -ra py313-test-instrumentation-sio-pika-0-wrapt1_ubuntu-latest: name: instrumentation-sio-pika-0-wrapt1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-sio-pika-0-wrapt1 -- -ra py313-test-instrumentation-sio-pika-0-wrapt2_ubuntu-latest: name: instrumentation-sio-pika-0-wrapt2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-sio-pika-0-wrapt2 -- -ra py313-test-instrumentation-sio-pika-1-wrapt1_ubuntu-latest: name: instrumentation-sio-pika-1-wrapt1 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-sio-pika-1-wrapt1 -- -ra py313-test-instrumentation-sio-pika-1-wrapt2_ubuntu-latest: name: instrumentation-sio-pika-1-wrapt2 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-sio-pika-1-wrapt2 -- -ra py314-test-instrumentation-sio-pika-0-wrapt1_ubuntu-latest: name: instrumentation-sio-pika-0-wrapt1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-sio-pika-0-wrapt1 -- -ra py314-test-instrumentation-sio-pika-0-wrapt2_ubuntu-latest: name: instrumentation-sio-pika-0-wrapt2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-sio-pika-0-wrapt2 -- -ra py314-test-instrumentation-sio-pika-1-wrapt1_ubuntu-latest: name: instrumentation-sio-pika-1-wrapt1 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-sio-pika-1-wrapt1 -- -ra py314-test-instrumentation-sio-pika-1-wrapt2_ubuntu-latest: name: instrumentation-sio-pika-1-wrapt2 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-sio-pika-1-wrapt2 -- -ra pypy3-test-instrumentation-sio-pika-0-wrapt2_ubuntu-latest: name: instrumentation-sio-pika-0-wrapt2 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-sio-pika-0-wrapt2 -- -ra pypy3-test-instrumentation-sio-pika-1-wrapt2_ubuntu-latest: name: instrumentation-sio-pika-1-wrapt2 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-sio-pika-1-wrapt2 -- -ra py310-test-instrumentation-aio-pika-oldest_ubuntu-latest: name: instrumentation-aio-pika-oldest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-aio-pika-oldest -- -ra py311-test-instrumentation-aio-pika-oldest_ubuntu-latest: name: instrumentation-aio-pika-oldest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-aio-pika-oldest -- -ra py312-test-instrumentation-aio-pika-oldest_ubuntu-latest: name: instrumentation-aio-pika-oldest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-aio-pika-oldest -- -ra py313-test-instrumentation-aio-pika-oldest_ubuntu-latest: name: instrumentation-aio-pika-oldest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-aio-pika-oldest -- -ra py314-test-instrumentation-aio-pika-oldest_ubuntu-latest: name: instrumentation-aio-pika-oldest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-aio-pika-oldest -- -ra py310-test-instrumentation-aio-pika-latest_ubuntu-latest: name: instrumentation-aio-pika-latest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-aio-pika-latest -- -ra py311-test-instrumentation-aio-pika-latest_ubuntu-latest: name: instrumentation-aio-pika-latest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-aio-pika-latest -- -ra py312-test-instrumentation-aio-pika-latest_ubuntu-latest: name: instrumentation-aio-pika-latest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-aio-pika-latest -- -ra py313-test-instrumentation-aio-pika-latest_ubuntu-latest: name: instrumentation-aio-pika-latest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-aio-pika-latest -- -ra py314-test-instrumentation-aio-pika-latest_ubuntu-latest: name: instrumentation-aio-pika-latest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-aio-pika-latest -- -ra pypy3-test-instrumentation-aio-pika-oldest_ubuntu-latest: name: instrumentation-aio-pika-oldest pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-aio-pika-oldest -- -ra pypy3-test-instrumentation-aio-pika-latest_ubuntu-latest: name: instrumentation-aio-pika-latest pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-aio-pika-latest -- -ra py310-test-instrumentation-aiokafka_ubuntu-latest: name: instrumentation-aiokafka 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-aiokafka -- -ra py311-test-instrumentation-aiokafka_ubuntu-latest: name: instrumentation-aiokafka 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-aiokafka -- -ra py312-test-instrumentation-aiokafka_ubuntu-latest: name: instrumentation-aiokafka 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-aiokafka -- -ra py313-test-instrumentation-aiokafka_ubuntu-latest: name: instrumentation-aiokafka 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-aiokafka -- -ra py314-test-instrumentation-aiokafka_ubuntu-latest: name: instrumentation-aiokafka 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-aiokafka -- -ra pypy3-test-instrumentation-aiokafka_ubuntu-latest: name: instrumentation-aiokafka pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-aiokafka -- -ra py310-test-instrumentation-kafka-python-oldest_ubuntu-latest: name: instrumentation-kafka-python-oldest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-kafka-python-oldest -- -ra py310-test-instrumentation-kafka-python-latest_ubuntu-latest: name: instrumentation-kafka-python-latest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-kafka-python-latest -- -ra py311-test-instrumentation-kafka-python-oldest_ubuntu-latest: name: instrumentation-kafka-python-oldest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-kafka-python-oldest -- -ra py311-test-instrumentation-kafka-python-latest_ubuntu-latest: name: instrumentation-kafka-python-latest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-kafka-python-latest -- -ra py312-test-instrumentation-kafka-python-oldest_ubuntu-latest: name: instrumentation-kafka-python-oldest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-kafka-python-oldest -- -ra py312-test-instrumentation-kafka-python-latest_ubuntu-latest: name: instrumentation-kafka-python-latest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-kafka-python-latest -- -ra py313-test-instrumentation-kafka-python-oldest_ubuntu-latest: name: instrumentation-kafka-python-oldest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-kafka-python-oldest -- -ra py313-test-instrumentation-kafka-python-latest_ubuntu-latest: name: instrumentation-kafka-python-latest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-kafka-python-latest -- -ra py314-test-instrumentation-kafka-python-oldest_ubuntu-latest: name: instrumentation-kafka-python-oldest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-kafka-python-oldest -- -ra py314-test-instrumentation-kafka-python-latest_ubuntu-latest: name: instrumentation-kafka-python-latest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-kafka-python-latest -- -ra pypy3-test-instrumentation-kafka-python-oldest_ubuntu-latest: name: instrumentation-kafka-python-oldest pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-kafka-python-oldest -- -ra pypy3-test-instrumentation-kafka-python-latest_ubuntu-latest: name: instrumentation-kafka-python-latest pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-kafka-python-latest -- -ra py310-test-instrumentation-confluent-kafka_ubuntu-latest: name: instrumentation-confluent-kafka 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-confluent-kafka -- -ra py311-test-instrumentation-confluent-kafka_ubuntu-latest: name: instrumentation-confluent-kafka 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-confluent-kafka -- -ra py312-test-instrumentation-confluent-kafka_ubuntu-latest: name: instrumentation-confluent-kafka 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-confluent-kafka -- -ra py313-test-instrumentation-confluent-kafka_ubuntu-latest: name: instrumentation-confluent-kafka 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-confluent-kafka -- -ra py314-test-instrumentation-confluent-kafka_ubuntu-latest: name: instrumentation-confluent-kafka 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-confluent-kafka -- -ra py310-test-instrumentation-asyncio_ubuntu-latest: name: instrumentation-asyncio 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-asyncio -- -ra py311-test-instrumentation-asyncio_ubuntu-latest: name: instrumentation-asyncio 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-asyncio -- -ra py312-test-instrumentation-asyncio_ubuntu-latest: name: instrumentation-asyncio 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-asyncio -- -ra py313-test-instrumentation-asyncio_ubuntu-latest: name: instrumentation-asyncio 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-asyncio -- -ra py314-test-instrumentation-asyncio_ubuntu-latest: name: instrumentation-asyncio 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-asyncio -- -ra py310-test-instrumentation-cassandra-driver_ubuntu-latest: name: instrumentation-cassandra-driver 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-cassandra-driver -- -ra py310-test-instrumentation-cassandra-scylla_ubuntu-latest: name: instrumentation-cassandra-scylla 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-instrumentation-cassandra-scylla -- -ra py311-test-instrumentation-cassandra-driver_ubuntu-latest: name: instrumentation-cassandra-driver 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-cassandra-driver -- -ra py311-test-instrumentation-cassandra-scylla_ubuntu-latest: name: instrumentation-cassandra-scylla 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-instrumentation-cassandra-scylla -- -ra py312-test-instrumentation-cassandra-driver_ubuntu-latest: name: instrumentation-cassandra-driver 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-cassandra-driver -- -ra py312-test-instrumentation-cassandra-scylla_ubuntu-latest: name: instrumentation-cassandra-scylla 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-instrumentation-cassandra-scylla -- -ra py313-test-instrumentation-cassandra-driver_ubuntu-latest: name: instrumentation-cassandra-driver 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-cassandra-driver -- -ra py313-test-instrumentation-cassandra-scylla_ubuntu-latest: name: instrumentation-cassandra-scylla 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-instrumentation-cassandra-scylla -- -ra py314-test-instrumentation-cassandra-driver_ubuntu-latest: name: instrumentation-cassandra-driver 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-cassandra-driver -- -ra py314-test-instrumentation-cassandra-scylla_ubuntu-latest: name: instrumentation-cassandra-scylla 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-instrumentation-cassandra-scylla -- -ra pypy3-test-instrumentation-cassandra-scylla_ubuntu-latest: name: instrumentation-cassandra-scylla pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-instrumentation-cassandra-scylla -- -ra py310-test-processor-baggage_ubuntu-latest: name: processor-baggage 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-processor-baggage -- -ra py311-test-processor-baggage_ubuntu-latest: name: processor-baggage 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-processor-baggage -- -ra py312-test-processor-baggage_ubuntu-latest: name: processor-baggage 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-processor-baggage -- -ra py313-test-processor-baggage_ubuntu-latest: name: processor-baggage 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-processor-baggage -- -ra py314-test-processor-baggage_ubuntu-latest: name: processor-baggage 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-processor-baggage -- -ra pypy3-test-processor-baggage_ubuntu-latest: name: processor-baggage pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python pypy-3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy-3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e pypy3-test-processor-baggage -- -ra py310-test-opamp-client-latest_ubuntu-latest: name: opamp-client-latest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-opamp-client-latest -- -ra py310-test-opamp-client-lowest_ubuntu-latest: name: opamp-client-lowest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.10 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py310-test-opamp-client-lowest -- -ra py311-test-opamp-client-latest_ubuntu-latest: name: opamp-client-latest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-opamp-client-latest -- -ra py311-test-opamp-client-lowest_ubuntu-latest: name: opamp-client-lowest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.11 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py311-test-opamp-client-lowest -- -ra py312-test-opamp-client-latest_ubuntu-latest: name: opamp-client-latest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-opamp-client-latest -- -ra py312-test-opamp-client-lowest_ubuntu-latest: name: opamp-client-lowest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.12 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py312-test-opamp-client-lowest -- -ra py313-test-opamp-client-latest_ubuntu-latest: name: opamp-client-latest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-opamp-client-latest -- -ra py313-test-opamp-client-lowest_ubuntu-latest: name: opamp-client-lowest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py313-test-opamp-client-lowest -- -ra py314-test-opamp-client-latest_ubuntu-latest: name: opamp-client-latest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-opamp-client-latest -- -ra py314-test-opamp-client-lowest_ubuntu-latest: name: opamp-client-lowest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.14 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.14" - name: Install tox run: pip install tox-uv - name: Run tests run: tox -e py314-test-opamp-client-lowest -- -ra opentelemetry-python-contrib-0.66b2/.gitignore000066400000000000000000000011401527070072300215310ustar00rootroot00000000000000*.py[cod] *.sw[op] # C extensions *.so # Packages *.egg *.egg-info dist dist-info build eggs parts bin var sdist develop-eggs .installed.cfg pyvenv.cfg lib lib64 __pycache__ venv*/ .venv*/ # Installer logs pip-log.txt # Unit test / coverage reports coverage.xml .coverage .nox .tox .cache htmlcov # Translations *.mo # Mac .DS_Store # Mr Developer .mr.developer.cfg .project .pydevproject # JetBrains .idea # VSCode .vscode # Sphinx _build/ # mypy .mypy_cache/ target # Benchmark result files *-benchmark.json # opentelemetry-admin jobs opentelemetry-admin-jobs.txt .claude/settings.local.json opentelemetry-python-contrib-0.66b2/.pre-commit-config.yaml000066400000000000000000000017751527070072300240400ustar00rootroot00000000000000repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: v0.16.1 hooks: # Run the linter. - id: ruff args: ["--fix", "--show-fixes"] # Run the formatter. - id: ruff-format - id: ruff-format name: ruff-format (markdown & towncrier) types_or: [python, pyi, jupyter, markdown, text] files: \.(md|added|changed|deprecated|removed|fixed)$ args: ["--preview"] - repo: https://github.com/astral-sh/uv-pre-commit # uv version. rev: 8ff2449591c8de025b17661ba76d60237a1ae62b # frozen: 0.12.1 hooks: - id: uv-lock args: ["--index-url", "https://pypi.org/simple"] - repo: https://github.com/rstcheck/rstcheck rev: 77490ffa33bfc0928975ae3cf904219903db755d # frozen: v6.2.5 hooks: - id: rstcheck additional_dependencies: - rstcheck[sphinx] - rstcheck-core==1.3.0 args: ["--report-level", "warning"] exclude: ^_template/autodoc_entry\.rst$ opentelemetry-python-contrib-0.66b2/.pylintrc000066400000000000000000000372351527070072300214240ustar00rootroot00000000000000[MASTER] # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. extension-pkg-whitelist=cassandra # Add list of files or directories to be excluded. They should be base names, not # paths. ignore=CVS,gen,Dockerfile,docker-compose.yml,README.md,requirements.txt,docs,.venv,site-packages,.tox,proto # Add files or directories matching the regex patterns to be excluded. The # regex matches against base names, not paths. ignore-patterns= ignore-paths= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the # number of processors available to use. jobs=0 # Control the amount of potential inferred values when inferring a single # object. This can help the performance when dealing with large functions or # complex, nested conditions. limit-inference-results=100 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins=pylint.extensions.no_self_use # Pickle collected data for later comparisons. persistent=yes # Specify a configuration file. #rcfile= # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no # Run python dependant checks considering the baseline version py-version=3.10 [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. confidence= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once). You can also use "--disable=all" to # disable everything first and then reenable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". disable=missing-docstring, fixme, # Warns about FIXME, TODO, etc. comments. too-few-public-methods, # Might be good to re-enable this later. too-many-instance-attributes, too-many-arguments, duplicate-code, ungrouped-imports, # Leave this up to isort wrong-import-order, # Leave this up to isort line-too-long, # Leave this up to black exec-used, super-with-arguments, # temp-pylint-upgrade isinstance-second-argument-not-valid-type, # temp-pylint-upgrade raise-missing-from, # temp-pylint-upgrade unused-argument, # temp-pylint-upgrade protected-access, # temp-pylint-upgrade super-init-not-called, # temp-pylint-upgrade invalid-overridden-method, # temp-pylint-upgrade missing-module-docstring, # temp-pylint-upgrade import-error, # needed as a workaround as reported here: https://github.com/open-telemetry/opentelemetry-python-contrib/issues/290 cyclic-import, not-context-manager, attribute-defined-outside-init # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). See also the "--disable" option for examples. # enable=c-extension-no-member [REPORTS] # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (RP0004). #evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details. #msg-template= # Set the output format. Available formats are text, parseable, colorized, json # and msvs (visual studio). You can also give a reporter class, e.g. # mypackage.mymodule.MyReporterClass. #output-format=text # Tells whether to display a full report or only the messages. #reports=no # Activate the evaluation score. score=yes [REFACTORING] # Maximum number of nested blocks for function / method body max-nested-blocks=5 # Complete name of functions that never returns. When checking for # inconsistent-return-statements if a never returning function is called then # it will be considered as an explicit return statement and no message will be # printed. never-returning-functions=sys.exit [LOGGING] # Format style used to check logging format string. `old` means using % # formatting, while `new` is for `{}` formatting. logging-format-style=old # Logging modules to check that the string format arguments are in logging # function parameter format. logging-modules=logging [SPELLING] # Limits count of emitted suggestions for spelling mistakes. max-spelling-suggestions=4 # Spelling dictionary name. Available dictionaries: none. To make it working # install python-enchant package.. spelling-dict= # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains private dictionary; one word per line. spelling-private-dict-file= # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. spelling-store-unknown-words=no [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME, XXX, TODO [TYPECHECK] # List of decorators that produce context managers, such as # contextlib.contextmanager. Add to this list to register other decorators that # produce valid context managers. contextmanager-decorators=contextlib.contextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. generated-members=types_pb2.*,anyvalue_pb2.*,opamp_pb2.* # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). #ignore-mixin-members=yes # Tells whether to warn about missing members when the owner of the attribute # is inferred to be None. #ignore-none=yes # This flag controls whether pylint should warn about no-member and similar # checks whenever an opaque object is returned when inferring. The inference # can return multiple potential results while evaluating a Python object, but # some branches might not be evaluated, which results in partial inference. In # that case, it might be useful to still emit no-member and other checks for # the rest of the inferred objects. #ignore-on-opaque-inference=yes # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. ignored-classes=optparse.Values,thread._local,_thread._local # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis. It # supports qualified module names, as well as Unix pattern matching. ignored-modules= # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. missing-member-hint=yes # The minimum edit distance a name should have in order to be considered a # similar match for a missing member name. missing-member-hint-distance=1 # The total number of similar names that should be taken in consideration when # showing a hint for a missing member. missing-member-max-choices=1 [VARIABLES] # List of additional names supposed to be defined in builtins. Remember that # you should avoid defining new builtins when possible. additional-builtins= # Tells whether unused global variables should be treated as a violation. allow-global-unused-variables=yes # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_, _cb # A regular expression matching the name of dummy variables (i.e. expected to # not be used). dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ # Argument names that match this expression will be ignored. Default to name # with leading underscore. ignored-argument-names=_.*|^ignored_|^unused_|^kwargs|^args # Tells whether we should check for unused import in __init__ files. init-import=no # List of qualified module names which can have objects that can redefine # builtins. redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io [FORMAT] # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format=LF # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=4 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Maximum number of characters on a single line. max-line-length=79 # Maximum number of lines in a module. max-module-lines=1000 # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no [SIMILARITIES] # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes # Ignore imports when computing similarities. ignore-imports=no # Minimum lines number of a similarity. min-similarity-lines=4 [BASIC] # Naming style matching correct argument names. argument-naming-style=snake_case # Regular expression matching correct argument names. Overrides argument- # naming-style. #argument-rgx= # Naming style matching correct attribute names. attr-naming-style=snake_case # Regular expression matching correct attribute names. Overrides attr-naming- # style. #attr-rgx= # Bad variable names which should always be refused, separated by a comma. bad-names=foo, bar, baz, toto, tutu, tata # Naming style matching correct class attribute names. class-attribute-naming-style=any # Regular expression matching correct class attribute names. Overrides class- # attribute-naming-style. #class-attribute-rgx= # Naming style matching correct class names. class-naming-style=PascalCase # Regular expression matching correct class names. Overrides class-naming- # style. #class-rgx= # Naming style matching correct constant names. const-naming-style=any # Regular expression matching correct constant names. Overrides const-naming- # style. #const-rgx= # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=-1 # Naming style matching correct function names. function-naming-style=snake_case # Regular expression matching correct function names. Overrides function- # naming-style. #function-rgx= # Good variable names which should always be accepted, separated by a comma. good-names=_, log, logger # Include a hint for the correct naming format with invalid-name. include-naming-hint=yes # Naming style matching correct inline iteration names. inlinevar-naming-style=any # Regular expression matching correct inline iteration names. Overrides # inlinevar-naming-style. #inlinevar-rgx= # Naming style matching correct method names. method-naming-style=snake_case # Regular expression matching correct method names. Overrides method-naming- # style. #method-rgx= # Naming style matching correct module names. module-naming-style=snake_case # Regular expression matching correct module names. Overrides module-naming- # style. #module-rgx= # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=^_ # List of decorators that produce properties, such as abc.abstractproperty. Add # to this list to register other decorators that produce valid properties. # These decorators are taken in consideration only for invalid-name. property-classes=abc.abstractproperty # Naming style matching correct variable names. variable-naming-style=snake_case # Regular expression matching correct variable names. Overrides variable- # naming-style. variable-rgx=(([a-z_][a-z0-9_]{1,})|(_[a-z0-9_]*)|(__[a-z][a-z0-9_]+__))$ [IMPORTS] # Allow wildcard imports from modules that define __all__. allow-wildcard-with-all=no # Analyse import fallback blocks. This can be used to support both Python 2 and # 3 compatible code, which means that the block might have code that exists # only in one or another interpreter, leading to false positives when analysed. analyse-fallback-blocks=yes # Deprecated modules which should not be used, separated by a comma. deprecated-modules=optparse,tkinter.tix # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled). ext-import-graph= # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled). import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled). int-import-graph= # Force import order to recognize a module as part of the standard # compatibility libraries. known-standard-library=six # Force import order to recognize a module as part of a third party library. known-third-party=enchant [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__, __new__, setUp # List of member names, which should be excluded from the protected access # warning. exclude-protected=_asdict, _fields, _replace, _source, _make, _Span # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=cls [DESIGN] # Maximum number of arguments for function / method. max-args=5 # Maximum number of positional arguments for function / method. max-positional-arguments=12 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Maximum number of boolean expressions in an if statement. max-bool-expr=5 # Maximum number of branch for function / method body. max-branches=12 # Maximum number of locals for function / method body. max-locals=15 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of public methods for a class (see R0904). max-public-methods=20 # Maximum number of return / yield for function / method body. max-returns=6 # Maximum number of statements in function / method body. max-statements=50 # Minimum number of public methods for a class (see R0903). min-public-methods=2 [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to # "Exception". overgeneral-exceptions=builtins.Exception opentelemetry-python-contrib-0.66b2/.readthedocs.yml000066400000000000000000000004271527070072300226360ustar00rootroot00000000000000# Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 sphinx: configuration: docs/conf.py build: os: "ubuntu-24.04" tools: python: "3.11" python: install: - requirements: rtd-requirements.txt opentelemetry-python-contrib-0.66b2/.rstcheck.cfg000066400000000000000000000000521527070072300221070ustar00rootroot00000000000000[rstcheck] ignore_directives = automodule opentelemetry-python-contrib-0.66b2/AGENTS.md000066400000000000000000000117211527070072300210520ustar00rootroot00000000000000# OpenTelemetry Python Contrib 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 50+ OpenTelemetry instrumentation packages for Python. ## General Rules and Guidelines 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](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](#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. ## PR description Follow the repo's [PR template](.github/pull_request_template.md) and fill applicable sections. 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. ## Structure - `instrumentation/` - instrumentation packages (Flask, Django, FastAPI, gRPC, databases, etc.) - `instrumentation-genai/` - GenAI instrumentations; deprecated and no longer developed here, see [instrumentation-genai/AGENTS.md](instrumentation-genai/AGENTS.md) - `util/` - shared utilities (`util-http`, `util-genai`) - `exporter/` - custom exporters - `propagator/` - context propagators Instrumentation packages live under `src/opentelemetry/instrumentation/{name}/` with their own `pyproject.toml` and `tests/`. Other package types follow the equivalent layout under their own namespace (e.g. `src/opentelemetry/util/{name}/`, `src/opentelemetry/exporter/{name}/`). ## Commands ```sh # Install all packages and dev tools uv sync --frozen --all-packages # Lint (runs ruff via pre-commit) uv run pre-commit run ruff --all-files # Test a specific package (append -0, -1, etc. for version variants) uv run tox -e py312-test-instrumentation-flask-0 # Type check uv run tox -e typecheck ``` ## Guidelines - Each package has its own `pyproject.toml` with version, dependencies, and entry points. - The monorepo uses `uv` workspaces. - `tox.ini` defines the test matrix - check it for available test environments. - Do not add `type: ignore` comments. 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 annotations` over 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 as `typing.cast(...)`, unless the referenced type is imported at runtime. - Whenever applicable, all code changes should have tests that actually validate the changes. ## Instrumentation rules Apply to packages under `instrumentation/` and `instrumentation-genai/`. ### Exception handling - 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. ### Semantic conventions - 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. ### Tests - For every public API instrumented, cover sync/async variants when both exist. - Cover happy path and error scenarios. - 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}.txt` and `{oldest,latest}` `tox.ini` factors. The parallel PR-review rules live in [`.github/instructions/instrumentation.instructions.md`](.github/instructions/instrumentation.instructions.md) and should be kept in sync with this section. ## Commit formatting 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 ``` opentelemetry-python-contrib-0.66b2/CHANGELOG.md000066400000000000000000005672411527070072300213750ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). > [!NOTE] > The following components are released independently and maintain individual CHANGELOG files. > Use [this search for a list of all CHANGELOG.md files in this repo](https://github.com/search?q=repo%3Aopen-telemetry%2Fopentelemetry-python-contrib+path%3A**%2FCHANGELOG.md&type=code). ## Version 1.45.2/0.66b2 (2026-10-29) ## Version 1.45.1/0.66b1 (2026-10-06) No significant changes. ## Version 1.45.0/0.66b0 (2026-09-25) ### Added - `opentelemetry-instrumentation-psycopg2`, `opentelemetry-instrumentation-psycopg`: Add sqlcommenter support for `instrument_connection` ([#4267](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4267)) - `opentelemetry-instrumentation-fastapi`: Document auto-instrumentation setup and HTTP/WebSocket trace propagation `opentelemetry-instrumentation`: Include the instrumentation README in API docs ([#4555](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4555)) - `opentelemetry-instrumentation-botocore`: capture Bedrock prompt cache token usage for `converse` and `converse_stream` API ([#4615](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4615)) - `opentelemetry-instrumentation-fastapi`: advertise inline type annotations ([#4685](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4685)) - `opentelemetry-instrumentation-kafka-python`: Update kafka python instrumentation to support kafka-python>=3 ([#4786](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4786)) - `opentelemetry-instrumentation-mysql`: add semantic convention stability opt-in mode tests ([#4810](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4810)) - `opentelemetry-instrumentation-aiopg`: Add DB client operation duration and returned rows metrics. ([#4826](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4826)) - `opentelemetry-instrumentation-httpx`: support strict typing ([#4833](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4833)) - `opentelemetry-instrumentation-mysqlclient`: add semantic convention stability opt-in mode tests ([#4834](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4834)) - `opentelemetry-instrumentation-pymysql`: add semantic convention stability opt-in mode tests ([#4835](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4835)) - `opentelemetry-instrumentation-sqlite3`: add semantic convention stability opt-in mode tests ([#4836](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4836)) - `opentelemetry-instrumentation-psycopg`: add semantic convention stability opt-in mode tests ([#4838](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4838)) - infra: add validation for changelog file format ([#4856](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4856)) - `opentelemetry-instrumentation-aio-pika`: bump maximum admissible aio-pika version to 11.0.0 ([#4998](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4998)) ### Changed - `opentelemetry-instrumentation`: clarify dependency conflict messages, in logs and in `DependencyConflictError`, and promote auto-instrumentation version conflict logs to error level ([#5025](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/5025)) ### Removed - `opentelemetry-instrumentation-kafka-python`: drop support for the deprecated `kafka-python-ng` fork now that `kafka-python` is actively maintained again ([#4839](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4839)) - `opentelemetry-instrumentation-kafka-python`: remove unused `KafkaPropertiesExtractor.extract_send_key` and `KafkaPropertiesExtractor.extract_send_value` helpers ([#4858](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4858)) - `opentelemetry-instrumentation-anthropic`, `opentelemetry-instrumentation-claude-agent-sdk`, `opentelemetry-instrumentation-langchain`, `opentelemetry-instrumentation-weaviate`: removed from this repository; these instrumentations now live in and are maintained from the [opentelemetry-python-genai](https://github.com/open-telemetry/opentelemetry-python-genai) repository ([#4903](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4903)) ### Fixed - `opentelemetry-instrumentation-aws-lambda`: support ALB multiValueHeaders ([#4605](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4605)) - `opentelemetry-instrumentation-redis`: extract `ClusterPipeline` commands from `_execution_strategy` in redis-py 6+ so pipeline span attributes include the queued commands instead of being empty ([#4728](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4728)) - `opentelemetry-instrumentation-aiopg`: fix connection metadata for manual instrumentation ([#4821](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4821)) - `opentelemetry-instrumentation-redis`: treat explicit `db=None` as database index 0 ([#4828](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4828)) - `opentelemetry-instrumentation-aws-lambda`: use the correct camel case when accessing the SQS message attributes ([#4831](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4831)) - `opentelemetry-instrumentation-botocore`: Restore PyPy test coverage. ([#4841](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4841)) - `opentelemetry-instrumentation-botocore`: don't emit error status on 3xx ClientError responses ([#4851](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4851)) - `opentelemetry-instrumentation-kafka-python`: record the actual `messaging.kafka.partition` read back from the producer `send()` future, instead of a pre-computed estimate that did not match the delivered partition for randomly-partitioned messages ([#4858](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4858)) - `opentelemetry-instrumentation-logging`: Promote otel.event.name to LogRecord.event_name in _translate ([#4864](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4864)) - `opentelemetry-instrumentation-grpc`: respect suppressed instrumentation in server interceptors ([#4870](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4870)) - `opentelemetry-instrumentation-logging`: Import code_attributes from stable semconv package ([#4873](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4873)) - `opentelemetry-instrumentation-asyncio`: fix `to_thread` instrumentation so spans and duration metrics measure the actual function execution instead of ending immediately, repeated calls of the same function are all recorded instead of only the first one, and failures are recorded with the same `state` values (`exception`, `timeout`, `cancelled`) as the coroutine and future paths; the `AsyncioInstrumentor.trace_to_thread` method is renamed to `AsyncioInstrumentor.wrap_to_thread_func` to reflect that it now returns a wrapper ([#4901](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4901)) - `opentelemetry-instrumentation-logging`: fix documented name of the code attributes env var to `OTEL_PYTHON_LOG_CODE_ATTRIBUTES` ([#4912](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4912)) - `opentelemetry-instrumentation-psycopg`: avoid `IndexError` in `get_operation_name` for comment-only or whitespace-only Composed statements ([#4936](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4936)) - `opentelemetry-instrumentation-psycopg2`: avoid `IndexError` in `get_operation_name` for comment-only or whitespace-only statements ([#4942](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4942)) - `opentelemetry-instrumentation-sqlalchemy`: avoid `IndexError` in `_operation_name` for comment-only or whitespace-only statements ([#4943](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4943)) - `opentelemetry-instrumentation-mysql`: support mysql-connector-python 26.7.0 and later ([#4951](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4951)) - `opentelemetry-util-http`: stop `_parse_url_query` raising on unparsable urls ([#4965](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4965)) - `opentelemetry-instrumentation-asyncio`: fix metric name and future example in module docs ([#5083](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/5083)) - `opentelemetry-instrumentation-pymemcache`: honor instrumentation suppression ([#5086](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/5086)) - `opentelemetry-instrumentation-asyncpg`: honor instrumentation suppression ([#5087](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/5087)) - `opentelemetry-propagator-ot-trace`: cap ot-baggage-* entries on extract and inject ([#5098](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/5098)) ## Version 1.44.0/0.65b0 (2026-07-16) ### Added - Add `opentelemetry-instrumentation-structlog` instrumentation package and shared `log_utils` helpers for emitting structlog events as OpenTelemetry logs. ([#4286](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4286)) - `opentelemetry-instrumentation-celery`: add `use_span_links` option to link task execution spans to producer spans instead of creating parent-child relationships ([#4537](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4537)) - `opentelemetry-instrumentation-aws-lambda`: add SQS context propagation support ([#4668](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4668)) - `opentelemetry-instrumentation-httpx`: add support for httpx2 ([#4730](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4730)) - `opentelemetry-instrumentation-tortoiseorm`: Implement database semantic convention stability migration support through `OTEL_SEMCONV_STABILITY_OPT_IN` ([#4731](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4731)) - `opentelemetry-instrumentation-asyncpg`: add database semantic convention stability support through `OTEL_SEMCONV_STABILITY_OPT_IN` ([#4733](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4733)) - `opentelemetry-instrumentation-pymemcache`: add database semconv stability migration support ([#4739](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4739)) - `opentelemetry-instrumentation-cassandra`: add semconv stability migration support ([#4747](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4747)) - `opentelemetry-instrumentation-pymongo`: add database semantic convention stability support through `OTEL_SEMCONV_STABILITY_OPT_IN` ([#4772](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4772)) - `opentelemetry-instrumentation-aiopg`: add database semantic convention stability support through `OTEL_SEMCONV_STABILITY_OPT_IN` ([#4820](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4820)) ### Changed - opentelemetry-instrumentation-urllib3: remove multiple calls to sanitize_method ([#4718](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4718)) ### Removed - `opentelemetry-instrumentation-elasticsearch`: drop the package since all supported `elasticsearch` versions have a native OTel instrumentation ([#4759](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4759)) ### Fixed - `opentelemetry-instrumentation-grpc`: handle `NotImplementedError` from `add_done_callback` in aio client instrumentation ([#4429](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4429)) - `opentelemetry-util-http`: recognize QUERY as known method in `sanitize_method` ([#4734](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4734)) - `opentelemetry-instrumentation-pika`: prevent duplicate consumer spans ([#4740](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4740)) - `opentelemetry-instrumentation-pyramid`: avoid duplicate traversal subscribers ([#4741](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4741)) - `opentelemetry-instrumentation-boto3sqs`: handle missing `Successful` key in `send_message_batch` response when all messages fail ([#4746](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4746)) - `opentelemetry-instrumentation-logging`: export formatter-added attributes ([#4764](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4764)) - `opentelemetry-instrumentation-aiohttp-server`: fix span name and `http.route` attribute according to http semantic conventions ([#4768](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4768)) - `opentelemetry-instrumentation-dbapi`: fix suppression for async queries ([#4781](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4781)) - `opentelemetry-instrumentation-redis`: don't fail commands when the connection pool lacks `connection_kwargs` ([#4782](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4782)) - `opentelemetry-util-genai`, `instrumentation-genai/*`: ignore now deprecated GenAI enums ([#4814](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4814)) ## Version 1.43.0/0.64b0 (2026-06-24) ### Added - `opentelemetry-instrumentation-exceptions`: add instrumentation to emit OpenTelemetry logs for uncaught process, thread, and asyncio exceptions. ([#4503](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4503)) - `opentelemetry-instrumentation-botocore`: loosen aiobotocore version constraints to allow for 3.x ([#4601](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4601)) - `opentelemetry-instrumentation-logging`: add optional `inject_trace_context` argument for injecting trace context attributes ([#4626](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4626)) - `opentelemetry-instrumentation-redis`: gracefully handle hook exceptions ([#4696](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4696)) ### Changed - opentelemetry-instrumentation-requests: remove multiple calls to sanitize_method ([#4719](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4719)) ### Fixed - `opentelemetry-instrumentation-django`: Remove duplicate query logging in SQLCommenter middleware that broke Django's `assertNumQueries` ([#4367](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4367)) - `opentelemetry-instrumentation-flask`: wrap wsgi_app call in try/except to prevent active_requests gauge leak ([#4433](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4433)) - `opentelemetry-instrumentation-asyncpg`: instrument prepared statements ([#4529](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4529)) - `opentelemetry-instrumentation-aiokafka`, `opentelemetry-instrumentation-confluent-kafka`, `opentelemetry-instrumentation-kafka-python`: fix malformed RST formatting in module docstrings ([#4613](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4613)) - `opentelemetry-instrumentation-dbapi`: Fix pyodbc DB-API instrumentation examples to wrap `connect`. ([#4657](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4657)) - `opentelemetry-instrumentation-tornado`: reduce cardinality of `http.target` metrics attribute with old semantic conventions ([#4666](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4666)) - `opentelemetry-instrumentation-dbapi`: implement proper handling of t-string queries ([#4697](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4697)) - `opentelemetry-instrumentation-fastapi`: fix `AttributeError` when resolving routes added via `include_router` on FastAPI 0.137+ ([#4700](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4700)) - `opentelemetry-instrumentation-tornado`: sanitize the request http method in server and client spans and metrics ([#4735](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4735)) ## Version 1.42.1/0.63b1 (2026-05-21) No significant changes. ## Version 1.42.0/0.63b0 (2026-05-19) ### Added - `opentelemetry-exporter-richconsole`: Add support for suppressing resource information ([#3898](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3898)) - `opentelemetry-instrumentation`: Add experimental metrics attributes Labeler utility ([#4288](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4288)) - `opentelemetry-instrumentation-logging`: Add `OTEL_PYTHON_LOG_HANDLER_LEVEL` and `OTEL_PYTHON_LOG_FORMAT` environment variables to configure the log level and formatter of the auto-instrumented `LoggingHandler`. ([#4298](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4298)) - `opentelemetry-instrumentation-sqlite3`: Add uninstrument, error status, suppress, and no-op tests ([#4335](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4335)) - Add `BaggageLogProcessor` to `opentelemetry-processor-baggage` ([#4371](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4371)) - `opentelemetry-instrumentation-system-metrics`: Add support for `process.disk.io` metric in system-metrics instrumentation ([#4397](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4397)) - `opentelemetry-instrumentation`: Register `OTEL_SEMCONV_STABILITY_OPT_IN` in `environment_variables.py` so `opentelemetry-instrument` exposes a `--semconv_stability_opt_in` CLI argument ([#4438](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4438)) - Expand `AGENTS.md` with instrumentation/GenAI guidance and add PR review instructions. ([#4457](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4457)) - `opentelemetry-instrumentation`: update auto-instrumentation to re-inject instrumentation path after init ([#4469](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4469)) - `opentelemetry-instrumentation-dbapi`: Add Database client operation duration and returned rows metrics ([#4481](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4481)) ### Changed - Remove redundant `pylint: disable=attribute-defined-outside-init` comments and add rule to global `.pylintrc` disable list ([#3839](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3839)) - Bump `pylint` to `4.0.5` ([#4244](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4244)) - `opentelemetry-instrumentation-logging`: Use `LogRecord.getMessage()` to format and extract each log record's body text to more closely match the expected usage of the logging system. As a result, all OTel log record bodies are now always strings. Previously, if `LogRecord.msg` (which contains the format string) was set to a non-string object (e.g. `logger.warning(some_dict)`), the object was exported as-is to the OTLP body field. Now, `LogRecord.getMessage()` will convert it to to a string. If you are passing in non-strings as the format string argument and your backend is expecting them as-is, you will need to update accordingly. ([#4372](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4372)) - Switch to SPDX license headers and add CI enforcement ([#4533](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4533)) - `opentelemetry-instrumentation-{urllib,urllib3,requests}`: switch http mock library from abandoned httpretty to mocket ([#4583](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4583)) ### Removed - Drop Python 3.9 support ([#4412](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4412)) ### Fixed - `opentelemetry-instrumentation-aiohttp-server`: Use `canonical` attribute of the `Resource` as a span name ([#3896](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3896)) - Refactor unit tests to allow for population of the random trace id flag in the `traceparent` header ([#4030](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4030)) - `opentelemetry-instrumentation-aws-lambda`: fix improper handling of header casing ([#4216](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4216)) - `opentelemetry-instrumentation-flask`: Clean up environ keys in `_teardown_request` to prevent duplicate execution ([#4341](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4341)) - `opentelemetry-instrumentation-celery`: Coerce non-string values to strings in `CeleryGetter.get()` to prevent `TypeError` in `TraceState.from_header()` when Celery request attributes contain ints ([#4360](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4360)) - `opentelemetry-instrumentation-celery`: Coerce timelimit values to strings in `set_attributes_from_context()` to prevent mixed-type span attribute warning ([#4361](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4361)) - `opentelemetry-instrumentation-fastapi`: Fix `FastAPI` instrumentation to correctly trace `BackgroundTasks` by wrapping their execution in a dedicated span, ensuring proper parent-child relationships and accurate trace timing ([#4368](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4368)) - `opentelemetry-instrumentation-flask`: Stop reading the deprecated (from 3.1) `flask.__version__` attribute; resolve the Flask version via `importlib.metadata` ([#4422](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4422)) - `opentelemetry-instrumentation-confluent-kafka`: Populate `server.address` and `server.port` span attributes from the producer/consumer `bootstrap.servers` config; previously `KafkaPropertiesExtractor.extract_bootstrap_servers` was defined but never called ([#4423](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4423)) - `opentelemetry-instrumentation-dbapi` Use `ObjectProxy` instead of `BaseObjectProxy` for `TracedCursorProxy` to restore iterability with wrapt 2.x ([#4427](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4427)) - `opentelemetry-instrumentation-pyramid`: add missing `http.response.status_code` in duration metrics for stable http semantic conventions ([#4448](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4448)) - `opentelemetry-instrumentation-pika` Use `ObjectProxy` instead of `BaseObjectProxy` for `ReadyMessagesDequeProxy` to restore iterability with wrapt 2.x ([#4461](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4461)) - `docker-tests`: Don't require sudo, debian based distro and MS SQL ODBC driver to run locally. Instead require docker and unixodbc ([#4478](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4478)) - `opentelemetry-instrumentation-celery`: clear completed task ids from `task_id_to_start_time` ([#4504](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4504)) - `opentelemetry-instrumentation-celery`: add null guards and type-safe helper handling around Celery context propagation internals ([#4505](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4505)) - `opentelemetry-instrumentation-wsgi`: use `PATH_INFO` and `QUERY_STRING` for URL attributes instead of parsing `RAW_URI` or `REQUEST_URI` ([#4551](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4551)) - `opentelemetry-instrumentation-mysqlclient`: Update unit tests to properly validate trace context trace flag values. ([#4560](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4560)) - `opentelemetry-instrumentation-pika`: pass destination to `_enrich_span` instead of `task_name` ([#4563](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4563)) - `opentelemetry-instrumentation-tornado`: reduce cardinality of span names and metrics attributes. This introduces a breaking change in the metrics attributes for the stable semantic convention by dropping the out of spec `url.query` and `url.path` attributes in favor of in-spec `http.route`. ([#4577](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4577)) - `opentelemetry-instrumentation-confluent-kafka`: Declare `opentelemetry-semantic-conventions` as a direct dependency ([#4590](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4590)) - `opentelemetry-instrumentation-pymssql`: Fix semconv stability migration for connection attributes (host, port, user) set in `wrapped_connection()` to respect `OTEL_SEMCONV_STABILITY_OPT_IN`. Note: `net.peer.port` is now emitted as `int` instead of `string` in default mode, aligning with other DB instrumentations. ([#4592](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4592)) - Declare `opentelemetry-semantic-conventions` as a direct dependency for the aio-pika, logging, pika and system-metrics instrumentations, since each imports `opentelemetry.semconv` directly. ([#4594](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4594)) ## Version 1.41.0/0.62b0 (2026-04-09) ### Added - Enabled the flake8-tidy-import plugins rules for the ruff linter. These rules throw warnings for relative imports in the modules. ([#4395](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4395)) - `opentelemetry-instrumentation-asgi`: Respect `suppress_http_instrumentation` context in ASGI middleware to skip server span creation when HTTP instrumentation is suppressed ([#4375](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4375)) - `opentelemetry-instrumentation-confluent-kafka`: Loosen confluent-kafka upper bound to <3.0.0 ([#4289](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4289)) - `opentelemetry-instrumentation`: Add support for wrapt 2.x ([#4203](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4203)) - `opentelemetry-instrumentation-psycopg2`: Add parameter `capture_parameters` to instrumentor. ([#4212](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4212)) - `opentelemetry-instrumentation-botocore`: Add support for instrumenting `aiobotocore` ([#4049](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4049)) - `opentelemetry-instrumentation-sqlalchemy`: implement new semantic convention opt-in migration ([#4110](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4110)) - `opentelemetry-instrumentation`: Add experimental metrics attributes Labeler utility ([#4288](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4288)) - `opentelemetry-instrumentation-redis`: implement new semantic convention opt-in migration ([#4370](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4370)) ### Fixed - `opentelemetry-docker-tests`: Replace deprecated `SpanAttributes` from `opentelemetry.semconv.trace` with `opentelemetry.semconv._incubating.attributes` ([#4339](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4339)) - `opentelemetry-instrumentation-confluent-kafka`: Skip `recv` span creation when `poll()` returns no message or `consume()` returns an empty list, avoiding empty spans on idle polls ([#4349](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4349)) - Fix intermittent `Core Contrib Test` CI failures caused by GitHub git CDN SHA propagation lag by installing core packages from the already-checked-out local copy instead of a second git clone ([#4305](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4305)) - Don't import module in unwrap if not already imported ([#4321](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4321)) - `opentelemetry-instrumentation-logging`: Map Python `CRITICAL` log level to OTel `FATAL` severity text and `WARNING` to `WARN` ([#4365](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4365)) - `opentelemetry-instrumentation-logging`: Add recursion guard in LoggingHandler.emit to prevent deadlock ([#4302](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4302)) - `opentelemetry-instrumentation-grpc`: Fix bidirectional streaming RPCs raising `AttributeError: 'generator' object has no attribute 'add_done_callback'` ([#4259](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4259)) - `opentelemetry-instrumentation-aiokafka`: fix `Unclosed AIOKafkaProducer` warning and `RuntimeWarning: coroutine was never awaited` in tests ([#4384](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4384)) - `opentelemetry-instrumentation-aiokafka`: Fix compatibility with aiokafka 0.13 by calling `_key_serializer`/`_value_serializer` directly instead of the internal `_serialize` method whose signature changed in 0.13 from `(topic, key, value)` to `(key, value, headers)` ([#4379](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4379)) ### Breaking changes - `opentelemetry-instrumentation-boto`: Remove instrumentation ([#4303](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4303)) ### Added - `opentelemetry-instrumentation-dbapi`: implement new semantic convention opt-in migration ([#4109](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4109)) ## Version 1.40.0/0.61b0 (2026-03-04) ### Added - Add Python 3.14 support ([#4193](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4193)) - `opentelemetry-instrumentation-asgi`: Add exemplars for `http.server.request.duration` and `http.server.duration` metrics ([#3739](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3739)) - `opentelemetry-instrumentation-wsgi`: Add exemplars for `http.server.request.duration` and `http.server.duration` metrics ([#3739](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3739)) - `opentelemetry-instrumentation-aiohttp-client`: add ability to capture custom headers ([#3988](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3988)) - `opentelemetry-instrumentation-requests`: add ability to capture custom headers ([#3987](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3987)) - `opentelemetry-instrumentation-aiohttp-client`: add typechecking for aiohttp-client instrumentation ([#4006](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4006)) - `opentelemetry-instrumentation-flask`: Add support for 3.1+ streaming responses ([#3938](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3938)) - `opentelemetry-instrumentation-aiohttp-server`: Support passing `TracerProvider` when instrumenting. ([#3819](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3819)) - `opentelemetry-instrumentation-system-metrics`: Add support for the `OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS` environment variable ([#3959](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3959)) - `opentelemetry-instrumentation-httpx`: add ability to capture custom headers ([#4047](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4047)) - `opentelemetry-instrumentation-urllib3`: add ability to capture custom headers ([#4050](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4050)) - `opentelemetry-instrumentation-urllib`: add ability to capture custom headers ([#4051](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4051)) - `opentelemetry-instrumentation-confluent-kafka`: Increase confluent-kafka upper bound to support newer versions (2.13.0) ([#4099](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4099)) - `opentelemetry-instrumentation-aiohttp-server` Implement new semantic convention opt-in migration ([#3980](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3980)) - `opentelemetry-instrumentation-falcon`: pass request attributes at span creation ([#4119](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4119)) - `opentelemetry-instrumentation`: add database stability attribute setters in `_semconv` utilities ([#4108](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4108)) - `opentelemetry-instrumentation-aiohttp-server`: pass request attributes at span creation ([#4118](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4118)) - `opentelemetry-instrumentation-tornado`: Implement new semantic convention opt-in migration ([#3993](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3993)) - `opentelemetry-instrumentation-tornado`: pass request attributes at span creation ([#4140](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4140)) - `opentelemetry-instrumentation-pyramid` Implement new semantic convention opt-in migration ([#3982](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3982)) - `opentelemetry-instrumentation-tortoiseorm` Add unit tests for Tortoise ORM instrumentation ([#4141](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4141)) - `opentelemetry-instrumentation-pyramid`: pass request attributes at span creation ([#4139](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4139)) - `opentelemetry-instrumentation-logging`: Move there the SDK LoggingHandler ([#4210](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4210)) - Add stale PR GitHub Action ([#4220](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4220)) ### Fixed - `opentelemetry-instrumentation-flask`: Align `http.server.active_requests` initialization with semantic convention helpers to ensure consistent names, units, and descriptions. ([#4094](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4094)) - `opentelemetry-instrumentation-asyncio`: Fix environment variables not appearing in Read the Docs documentation ([#4256](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/4256)) - `opentelemetry-instrumentation-mysql`: Refactor MySQL integration test mocks to use concrete DBAPI connection attributes, reducing noisy attribute type warnings. ([#4116](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4116)) - `opentelemetry-instrumentation-cassandra`: Use `_instruments_any` instead of `_instruments` for driver dependencies so that having either `cassandra-driver` or `scylla-driver` installed is sufficient ([#4182](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4182)) - `opentelemetry-instrumentation-asyncpg`: Hydrate span attributes before creation so samplers can filter on database details ([#3841](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3841)) - `opentelemetry-instrumentation-django`: Fix exemplars generation for `http.server.(request.)duration` ([#3945](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3945)) - `opentelemetry-util-http`, `opentelemetry-instrumentation-requests`, `opentelemetry-instrumentation-wsgi`, `opentelemetry-instrumentation-asgi`: normalize byte-valued user-agent headers before detecting synthetic sources so attributes are recorded reliably. ([#4001](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/4001)) - `opentelemetry-instrumentation-psycopg`: Fix `instrument_connection` method to use `_new_cursor_async_factory` on async connections. ([#3956](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3956)) - `opentelemetry-instrumentation-dbapi`: Replace SpanAttributes with semconv constants where applicable ([#4058](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4058)) - `opentelemetry-instrumentation-django`: Replace SpanAttributes with semconv constants where applicable ([#4059](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4059)) - `opentelemetry-instrumentation-botocore`: Replace SpanAttributes with semconv constants where applicable ([#4063](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4063)) - `opentelemetry-instrumentation-celery`: Replace SpanAttributes with semconv constants where applicable ([#4056](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4056)) - `opentelemetry-instrumentation-confluent-kafka`: Replace SpanAttributes with semconv constants where applicable ([#4057](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4057)) - `opentelemetry-instrumentation-cassandra`: Replace SpanAttributes with semconv constants for DB attributes ([#4055](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4055)) - `opentelemetry-instrumentation-falcon`: Replace SpanAttributes with semconv constants ([#4066](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4066)) - `opentelemetry-instrumentation-pika`: Replace SpanAttributes with semconv constants for net attributes ([#4068](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4068)) - `opentelemetry-instrumentation-mysqlclient`: Replace SpanAttributes with semconv constants ([#4067](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4067)) - `opentelemetry-instrumentation-pymemcache`: Remove span attributes pymemcache ([#4076](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4076)) - `opentelemetry-instrumentation-pymongo`: Replace SpanAttributes with semconv constants ([#4077](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4077)) - `opentelemetry-instrumentation-pymysql`: Replace SpanAttributes with semconv constants ([#4078](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4078)) - `opentelemetry-instrumentation-pyramid`: Replace SpanAttributes with semconv constants ([#4079](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4079)) - `opentelemetry-instrumentation-tortoiseorm`: Replace SpanAttributes with semconv constants ([#4080](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4080)) - `opentelemetry-instrumentation-asgi`: Replace SpanAttributes with semconv constants ([#4081](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4081)) - `opentelemetry-instrumentation-system-metrics`: Use proper numeric `cpython.gc.generation` attribute in CPython metrics, out of spec `generation` attribute is deprecated and will be removed in the future ([#4092](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4092)) - `opentelemetry-instrumentation-dbapi`: Fix sqlcomment calculation of mysql_client_version field if connection reassignment, with "unknown" fallback ([#3729](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3729)) - `opentelemetry-instrumentation-confluent-kafka`: Fix incorrect number of argument to `_inner_wrap_close` ([#3922](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3922)) - `opentelemetry-instrumentation-urllib3`: fix multiple arguments error ([#4144](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4144)) - `opentelemetry-instrumentation-psycopg`: Fix instrument of typed psycopg sql ([#4078](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4171)) - `opentelemetry-instrumentation-aiohttp-server`: fix HTTP error inconsistencies ([#4175](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4175)) - `opentelemetry-instrumentation-falcon`: Refactor `_handle_exception` to remove pylint disables ([#4207](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4207)) - `opentelemetry-docker-tests` Fix docker-tests assumption by Postgres-Sqlalchemy case about scope of metrics ([#4258](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4258)) - `opentelemetry-instrumentation-threading`: fix AttributeError when Thread is run without starting ([#4246](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4246)) - `opentelemetry-instrumentation-psycopg2`: Fix AttributeError by using instrumented connections weakref, instead of mutating connection object ([#4257](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4257)) ### Breaking changes - `opentelemetry-instrumentation-logging`: Inject span context attributes into logging LogRecord only if configured to do so ([#4112](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4112)) - `opentelemetry-instrumentation-django`: Drop support for Django < 2.0 ([#4083](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4083)) - `opentelemetry-instrumentation-aws-lambda`: Fix improper invocation `Span` name and kind. ## Version 1.39.0/0.60b0 (2025-12-03) ### Added - `opentelemetry-instrumentation-requests`, `opentelemetry-instrumentation-wsgi`, `opentelemetry-instrumentation-asgi` Detect synthetic sources on requests, ASGI, and WSGI. ([#3674](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3674)) - `opentelemetry-instrumentation-aiohttp-client`: add support for url exclusions via `OTEL_PYTHON_EXCLUDED_URLS` / `OTEL_PYTHON_AIOHTTP_CLIENT_EXCLUDED_URLS` ([#3850](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3850)) - `opentelemetry-instrumentation-httpx`: add support for url exclusions via `OTEL_PYTHON_EXCLUDED_URLS` / `OTEL_PYTHON_HTTPX_EXCLUDED_URLS` ([#3837](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3837)) - `opentelemetry-instrumentation-flask`: improve readthedocs for sqlcommenter configuration. ([#3883](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3883)) - `opentelemetry-instrumentation-sqlalchemy`: improve readthedocs for sqlcommenter configuration. ([#3886](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3886)) - `opentelemetry-instrumentation-mysql`, `opentelemetry-instrumentation-mysqlclient`, `opentelemetry-instrumentation-pymysql`: improve readthedocs for sqlcommenter configuration. ([#3885](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3885)) - `opentelemetry-instrumentation-django`: improve readthedocs for sqlcommenter configuration. ([#3884](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3884)) - `opentelemetry-instrumentation-aiohttp-server`: add support for custom header captures via `OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST` and `OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE` ([#3916](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3916)) - `opentelemetry-instrumentation-redis`: add support for `suppress_instrumentation` context manager for both sync and async Redis clients and pipelines - `opentelemetry-instrumentation-django`: improve docs for response_hook with examples of providing attributes from middlewares ([#3923](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3923)) - Update for Log SDK breaking changes. Rename InMemoryLogExporter to InMemoryLogRecordExporter in several tests ([#3589](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3589)) - opentelemetry-instrumentation: allow to skip all instrumentations loading with a wildcard ([#3967](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3967)) - `opentelemetry-instrumentation-redis`: add missing copyright header for opentelemetry-instrumentation-redis ([#3976](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3976)) ### Fixed - `opentelemetry-instrumentation-botocore`: bedrock: Add safety check for bedrock ConverseStream responses ([#3990](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3990)) - `opentelemetry-instrumentation-botocore`: bedrock: only decode JSON input buffer in Anthropic Claude streaming ([#3875](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3875)) - `opentelemetry-instrumentation-aiohttp-client`, `opentelemetry-instrumentation-aiohttp-server`: Fix readme links and text ([#3902](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3902)) - `opentelemetry-instrumentation-aws-lambda`: Fix ImportError with slash-delimited handler paths ([#3894](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3894)) - `opentelemetry-exporter-richconsole`: Prevent deadlock when parent span is not part of the batch ([#3900](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3900)) - `opentelemetry-instrumentation-psycopg2`, `opentelemetry-instrumentation-psycopg`: improve readthedocs for sqlcommenter configuration. ([#3882](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3882)) - `opentelemetry-instrumentation-aiohttp-server`: delay initialization of tracer, meter and excluded urls to instrumentation for testability ([#3836](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3836)) - Replace Python 3.14-deprecated `asyncio.iscoroutinefunction` with `inspect.iscoroutinefunction`. ([#3880](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3880)) - `opentelemetry-instrumentation-elasticsearch`: Enhance elasticsearch query body sanitization ([#3919](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3919)) - `opentelemetry-instrumentation-pymongo`: Fix span error descriptions ([#3904](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3904)) - build: bump ruff to 0.14.1 ([#3842](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3842)) - `opentelemetry-instrumentation-redis`: Add default span name for pipeline operations ([#3941](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3941)) - `opentelemetry-instrumentation-pymongo`: Fix invalid mongodb collection attribute type ([#3942](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3942)) - `opentelemetry-instrumentation-aiohttp-client`: Fix metric attribute leakage ([#3936](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3936)) - `opentelemetry-instrumentation-aiohttp-client`: Update instrumentor to respect suppressing http instrumentation ([#3957](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3957)) ## Version 1.38.0/0.59b0 (2025-10-16) ### Fixed - `opentelemetry-instrumentation-flask`: Do not record `http.server.duration` metrics for excluded URLs. ([#3794](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3794)) - `opentelemetry-instrumentation-botocore`: migrate off the deprecated events API to use the logs API ([#3624](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3624)) - `opentelemetry-instrumentation-dbapi`: fix crash retrieving libpq version when enabling commenter with psycopg ([#3796](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3796)) - `opentelemetry-instrumentation-fastapi`: Fix handling of APIRoute subclasses ([#3681](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3681)) - `opentelemetry-instrumentation-flask`: Fix exemplars generation for `http.server.request.duration` and `http.server.duration` metrics ([#3912](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3912)) ### Added - `opentelemetry-instrumentation-botocore`: Add support for AWS Secrets Manager semantic convention attribute ([#3765](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3765)) - `opentelemetry-instrumentation-dbapi`: Add support for `commenter_options` in `trace_integration` function to control SQLCommenter behavior ([#3743](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3743)) - Add `rstcheck` to pre-commit to stop introducing invalid RST ([#3777](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3777)) - `opentelemetry-exporter-credential-provider-gcp`: create this package which provides support for supplying your machine's Application Default Credentials (https://cloud.google.com/docs/authentication/application-default-credentials) to the OTLP Exporters created automatically by OpenTelemetry Python's auto instrumentation. These credentials authorize OTLP traces to be sent to `telemetry.googleapis.com`. [#3766](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3766). - `opentelemetry-instrumentation-psycopg`: Add missing parameter `capture_parameters` to instrumentor. ([#3676](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3676)) - `opentelemetry-instrumentation-dbapi`: Adds sqlcommenter to documentation. ([#3720](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3720)) ## Version 1.37.0/0.58b0 (2025-09-11) ### Fixed - `opentelemetry-instrumentation-fastapi`: Fix middleware ordering to cover all exception handling use cases. ([#3664](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3664)) - `opentelemetry-instrumentation-asgi`: Make all user hooks failsafe and record exceptions in hooks. ([#3664](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3664)) - `opentelemetry-instrumentation-fastapi`: Fix memory leak in `uninstrument_app()` by properly removing apps from the tracking set ([#3688](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3688)) - `opentelemetry-instrumentation-tornado` Fix server (request) duration metric calculation ([#3679](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3679)) - `opentelemetry-instrumentation-tornado`: Fix to properly skip all server telemetry when URL excluded. ([#3680](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3680)) - `opentelemetry-instrumentation`: Avoid calls to `context.detach` with `None` token. ([#3673](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3673)) - `opentelemetry-instrumentation-starlette`/`opentelemetry-instrumentation-fastapi`: Fixes a crash when host-based routing is used ([#3507](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3507)) - Fix documentation order of sections and headers for Django, Flask, MySQL, mysqlclient, psycopg, psycopg2, pymysql, sqlalchemy instrumentations. ([#3719](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3719)) - `opentelemetry-instrumentation-asgi` Fixed an issue where FastAPI reports IP instead of URL. ([#3670](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3670)) - `opentelemetry-instrumentation-httpx`: fix missing metric response attributes when tracing is disabled ([#3615](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3615)) - `opentelemetry-instrumentation-fastapi`: Don't pass bounded server_request_hook when using `FastAPIInstrumentor.instrument()` ([#3701](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3701)) ### Added - `opentelemetry-instrumentation-confluent-kafka` Add support for confluent-kafka <=2.11.0 ([#3685](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3685)) - `opentelemetry-instrumentation-system-metrics`: Add `cpython.gc.collected_objects` and `cpython.gc.uncollectable_objects` metrics ([#3666](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3666)) - `opentelemetry-sdk-extension-aws` Add AWS X-Ray Remote Sampler with initial Rules Poller implementation ([#3366](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3366)) - `opentelemetry-instrumentation`: add support for `OTEL_PYTHON_AUTO_INSTRUMENTATION_EXPERIMENTAL_GEVENT_PATCH` to inform opentelemetry-instrument about gevent monkeypatching ([#3699](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3699)) - `opentelemetry-instrumentation`: botocore: Add support for AWS Step Functions semantic convention attributes ([#3737](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3737)) - `opentelemetry-instrumentation-botocore`: Add support for SNS semantic convention attribute aws.sns.topic.arn ([#3734](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3734)) - `opentelemetry-instrumentation`: botocore: upgrade moto package from 5.0.9 to 5.1.11 ([#3736](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3736)) ## Version 1.36.0/0.57b0 (2025-07-29) ### Fixed - `opentelemetry-instrumentation`: Fix dependency conflict detection when instrumented packages are not installed by moving check back to before instrumentors are loaded. Add "instruments-any" feature for instrumentations that target multiple packages. ([#3610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3610)) - infra(ci): Fix git pull failures in core contrib test ([#3357](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3357)) ### Added - `opentelemetry-instrumentation-psycopg2` Utilize instruments-any functionality. ([#3610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3610)) - `opentelemetry-instrumentation-kafka-python` Utilize instruments-any functionality. ([#3610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3610)) - `opentelemetry-instrumentation-system-metrics`: Add `cpython.gc.collections` metrics with collection unit is specified in semconv ([3617](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3617)) ## Version 1.35.0/0.56b0 (2025-07-11) ### Added - `opentelemetry-instrumentation-pika` Added instrumentation for All `SelectConnection` adapters ([#3584](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3584)) - `opentelemetry-instrumentation-tornado` Add support for `WebSocketHandler` instrumentation ([#3498](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3498)) - `opentelemetry-util-http` Added support for redacting specific url query string values and url credentials in instrumentations ([#3508](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3508)) - `opentelemetry-instrumentation-pymongo` `aggregate` and `getMore` capture statements support ([#3601](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3601)) ### Fixed - `opentelemetry-instrumentation-asgi`: fix excluded_urls in instrumentation-asgi ([#3567](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3567)) - `opentelemetry-resource-detector-containerid`: make it more quiet on platforms without cgroups ([#3579](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3579)) ## Version 1.34.0/0.55b0 (2025-06-04) ### Fixed - `opentelemetry-instrumentation-system-metrics`: fix loading on Google Cloud Run ([#3533](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3533)) - `opentelemetry-instrumentation-fastapi`: fix wrapping of middlewares ([#3012](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3012)) - `opentelemetry-instrumentation-starlette` Remove max version constraint on starlette ([#3456](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3456)) - `opentelemetry-instrumentation-starlette` Fix memory leak and double middleware ([#3529](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3529)) - `opentelemetry-instrumentation-urllib3`: proper bucket boundaries in stable semconv http duration metrics ([#3518](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3518)) - `opentelemetry-instrumentation-urllib`: proper bucket boundaries in stable semconv http duration metrics ([#3519](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3519)) - `opentelemetry-instrumentation-falcon`: proper bucket boundaries in stable semconv http duration ([#3525](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3525)) - `opentelemetry-instrumentation-wsgi`: add explicit http duration buckets for stable semconv ([#3527](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3527)) - `opentelemetry-instrumentation-asgi`: add explicit http duration buckets for stable semconv ([#3526](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3526)) - `opentelemetry-instrumentation-flask`: proper bucket boundaries in stable semconv http duration ([#3523](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3523)) - `opentelemetry-instrumentation-django`: proper bucket boundaries in stable semconv http duration ([#3524](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3524)) - `opentelemetry-instrumentation-grpc`: support non-list interceptors ([#3520](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3520)) - `opentelemetry-instrumentation-botocore` Ensure spans end on early stream closure for Bedrock Streaming APIs ([#3481](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3481)) - `opentelemetry-instrumentation-sqlalchemy` Respect suppress_instrumentation functionality ([#3477](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3477)) - `opentelemetry-instrumentation-botocore`: fix handling of tool input in Bedrock ConverseStream ([#3544](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3544)) - `opentelemetry-instrumentation-botocore` Add type check when extracting tool use from Bedrock request message content ([#3548](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3548)) - `opentelemetry-instrumentation-dbapi` Respect suppress_instrumentation functionality ([#3460](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3460)) - `opentelemetry-resource-detector-container` Correctly parse container id when using systemd and cgroupsv1 ([#3429](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3429)) ### Breaking changes - `opentelemetry-instrumentation-botocore` Use `cloud.region` instead of `aws.region` span attribute as per semantic conventions. ([#3474](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3474)) - `opentelemetry-instrumentation-fastapi`: Drop support for FastAPI versions earlier than `0.92` ([#3012](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3012)) - `opentelemetry-resource-detector-container`: rename package name to `opentelemetry-resource-detector-containerid` ([#3536](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3536)) ### Added - `opentelemetry-instrumentation-aiohttp-client` Add support for HTTP metrics ([#3517](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3517)) - `opentelemetry-instrumentation-httpx` Add support for HTTP metrics ([#3513](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3513)) - `opentelemetry-instrumentation` Allow re-raising exception when instrumentation fails ([#3545](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3545)) - `opentelemetry-instrumentation-aiokafka` Add instrumentation of `consumer.getmany` (batch) ([#3257](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3257)) ### Deprecated - Drop support for Python 3.8, bump baseline to Python 3.9. ([#3399](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3399)) ## Version 1.33.0/0.54b0 (2025-05-09) ### Added - `opentelemetry-instrumentation-requests` Support explicit_bucket_boundaries_advisory in duration metrics ([#3464](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3464)) - `opentelemetry-instrumentation-redis` Add support for redis client-specific instrumentation. ([#3143](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3143)) ### Fixed - `opentelemetry-instrumentation` Catch `ModuleNotFoundError` when the library is not installed and log as debug instead of exception ([#3423](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3423)) - `opentelemetry-instrumentation-asyncio` Fix duplicate instrumentation ([#3383](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3383)) - `opentelemetry-instrumentation-botocore` Add GenAI instrumentation for additional Bedrock models for InvokeModel API ([#3419](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3419)) - `opentelemetry-instrumentation` don't print duplicated conflict log error message ([#3432](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3432)) - `opentelemetry-instrumentation-grpc` Check for None result in gRPC ([#3380](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3381)) - `opentelemetry-instrumentation-[asynclick/click]` Add missing opentelemetry-instrumentation dep ([#3447](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3447)) - `opentelemetry-instrumentation-botocore` Capture server attributes for botocore API calls ([#3448](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3448)) ## Version 1.32.0/0.53b0 (2025-04-10) ### Added - `opentelemetry-instrumentation-asyncclick`: new instrumentation to trace asyncclick commands ([#3319](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3319)) - `opentelemetry-instrumentation-botocore` Add support for GenAI tool events using Amazon Nova models and `InvokeModel*` APIs ([#3385](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3385)) - `opentelemetry-instrumentation` Make auto instrumentation use the same dependency resolver as manual instrumentation does ([#3202](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3202)) ### Fixed - `opentelemetry-instrumentation` Fix client address is set to server address in new semconv ([#3354](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3354)) - `opentelemetry-instrumentation-dbapi`, `opentelemetry-instrumentation-django`, `opentelemetry-instrumentation-sqlalchemy`: Fix sqlcomment for non string query and composable object. ([#3113](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3113)) - `opentelemetry-instrumentation-grpc` Fix error when using gprc versions <= 1.50.0 with unix sockets. ([[#3393](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3393)]) - `opentelemetry-instrumentation-asyncio` Fix duplicate instrumentation. ([[#3383](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3383)]) - `opentelemetry-instrumentation-aiokafka` Fix send_and_wait method no headers kwargs error. ([[#3332](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3332)]) ## Version 1.31.0/0.52b0 (2025-03-12) ### Added - `opentelemetry-instrumentation-openai-v2` Update doc for OpenAI Instrumentation to support OpenAI Compatible Platforms ([#3279](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3279)) - `opentelemetry-instrumentation-system-metrics` Add `process` metrics and deprecated `process.runtime` prefixed ones ([#3250](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3250)) - `opentelemetry-instrumentation-botocore` Add support for GenAI user events and lazy initialize tracer ([#3258](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3258)) - `opentelemetry-instrumentation-botocore` Add support for GenAI system events ([#3266](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3266)) - `opentelemetry-instrumentation-botocore` Add support for GenAI choice events ([#3275](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3275)) - `opentelemetry-instrumentation-botocore` Add support for GenAI tool events ([#3302](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3302)) - `opentelemetry-instrumentation-botocore` Add support for GenAI metrics ([#3326](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3326)) - `opentelemetry-instrumentation` make it simpler to initialize auto-instrumentation programmatically ([#3273](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3273)) - Add `opentelemetry-instrumentation-vertexai>=2.0b0` to `opentelemetry-bootstrap` ([#3307](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3307)) - Loosen `opentelemetry-instrumentation-starlette[instruments]` specifier ([#3304](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3304)) ### Fixed - `opentelemetry-instrumentation-redis` Add missing entry in doc string for `def _instrument` ([#3247](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3247)) - `opentelemetry-instrumentation-botocore` sns-extension: Change destination name attribute to match topic ARN and redact phone number from attributes ([#3249](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3249)) - `opentelemetry-instrumentation-asyncpg` Fix fallback for empty queries. ([#3253](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3253)) - `opentelemetry-instrumentation` Fix a traceback in sqlcommenter when psycopg connection pooling is enabled. ([#3309](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3309)) - `opentelemetry-instrumentation-threading` Fix broken context typehints ([#3322](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3322)) - `opentelemetry-instrumentation-requests` always record span status code in duration metric ([#3323](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3323)) ## Version 1.30.0/0.51b0 (2025-02-03) ### Added - `opentelemetry-instrumentation-confluent-kafka` Add support for confluent-kafka <=2.7.0 ([#3100](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3100)) - Add support to database stability opt-in in `_semconv` utilities and add tests ([#3111](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3111)) - `opentelemetry-instrumentation-urllib` Add `py.typed` file to enable PEP 561 ([#3131](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3131)) - `opentelemetry-opentelemetry-pymongo` Add `py.typed` file to enable PEP 561 ([#3136](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3136)) - `opentelemetry-opentelemetry-requests` Add `py.typed` file to enable PEP 561 ([#3135](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3135)) - `opentelemetry-instrumentation-system-metrics` Add `py.typed` file to enable PEP 561 ([#3132](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3132)) - `opentelemetry-opentelemetry-sqlite3` Add `py.typed` file to enable PEP 561 ([#3133](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3133)) - `opentelemetry-instrumentation-falcon` add support version to v4 ([#3086](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3086)) - `opentelemetry-instrumentation-falcon` Implement new HTTP semantic convention opt-in for Falcon ([#2790](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2790)) - `opentelemetry-instrumentation-wsgi` always record span status code to have it available in metrics ([#3148](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3148)) - add support to Python 3.13 ([#3134](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3134)) - `opentelemetry-opentelemetry-wsgi` Add `py.typed` file to enable PEP 561 ([#3129](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3129)) - `opentelemetry-util-http` Add `py.typed` file to enable PEP 561 ([#3127](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3127)) - `opentelemetry-instrumentation-psycopg2` Add support for psycopg2-binary ([#3186](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3186)) - `opentelemetry-opentelemetry-botocore` Add basic support for GenAI attributes for AWS Bedrock Converse API ([#3161](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3161)) - `opentelemetry-opentelemetry-botocore` Add basic support for GenAI attributes for AWS Bedrock InvokeModel API ([#3200](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3200)) - `opentelemetry-opentelemetry-botocore` Add basic support for GenAI attributes for AWS Bedrock ConverseStream API ([#3204](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3204)) - `opentelemetry-opentelemetry-botocore` Add basic support for GenAI attributes for AWS Bedrock InvokeModelWithStreamResponse API ([#3206](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3206)) - `opentelemetry-instrumentation-pymssql` Add pymssql instrumentation ([#394](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/394)) - `opentelemetry-instrumentation-mysql` Add sqlcommenter support ([#3163](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3163)) ### Fixed - `opentelemetry-instrumentation-httpx` Fix `RequestInfo`/`ResponseInfo` type hints ([#3105](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3105)) - `opentelemetry-instrumentation-dbapi` Move `TracedCursorProxy` and `TracedConnectionProxy` to the module level ([#3068](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3068)) - `opentelemetry-instrumentation-click` Disable tracing of well-known server click commands ([#3174](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3174)) - `opentelemetry-instrumentation` Fix `get_dist_dependency_conflicts` if no distribution requires ([#3168](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3168)) ### Breaking changes - `opentelemetry-exporter-prometheus-remote-write` updated protobuf required version from 4.21 to 5.26 and regenerated protobufs ([#3219](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3219)) - `opentelemetry-instrumentation-sqlalchemy` including sqlcomment in `db.statement` span attribute value is now opt-in ([#3112](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3112)) - `opentelemetry-instrumentation-dbapi` including sqlcomment in `db.statement` span attribute value is now opt-in ([#3115](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3115)) - `opentelemetry-instrumentation-psycopg2`, `opentelemetry-instrumentation-psycopg`, `opentelemetry-instrumentation-mysqlclient`, `opentelemetry-instrumentation-pymysql`: including sqlcomment in `db.statement` span attribute value is now opt-in ([#3121](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3121)) ## Version 1.29.0/0.50b0 (2024-12-11) ### Added - `opentelemetry-instrumentation-starlette` Add type hints to the instrumentation ([#3045](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3045)) - `opentelemetry-distro` default to OTLP log exporter. ([#3042](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3042)) - `opentelemetry-instrumentation-sqlalchemy` Update unit tests to run with SQLALchemy 2 ([#2976](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2976)) - Add `opentelemetry-instrumentation-openai-v2` to `opentelemetry-bootstrap` ([#2996](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2996)) - `opentelemetry-instrumentation-sqlalchemy` Add sqlcomment to `db.statement` attribute ([#2937](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2937)) - `opentelemetry-instrumentation-dbapi` Add sqlcomment to `db.statement` attribute ([#2935](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2935)) - `opentelemetry-instrumentation-dbapi` instrument_connection accepts optional connect_module ([#3027](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3027)) - `opentelemetry-instrumentation-mysqlclient` Add sqlcommenter support ([#2941](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2941)) - `opentelemetry-instrumentation-pymysql` Add sqlcommenter support ([#2942](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2942)) - `opentelemetry-instrumentation-click`: new instrumentation to trace click commands ([#2994](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2994)) ### Fixed - `opentelemetry-instrumentation-starlette`: Retrieve `meter_provider` key instead of `_meter_provider` on `_instrument` ([#3048](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3048)) - `opentelemetry-instrumentation-httpx`: instrument_client is a static method again ([#3003](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3003)) - `opentelemetry-instrumentation-system_metrics`: fix callbacks reading wrong config ([#3025](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3025)) - `opentelemetry-instrumentation-httpx`: Check if mount transport is none before wrap it ([#3022](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3022)) - Replace all instrumentor unit test `assertEqualSpanInstrumentationInfo` calls with `assertEqualSpanInstrumentationScope` calls ([#3037](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3037)) - `opentelemetry-instrumentation-sqlalchemy` Fixes engines from `sqlalchemy.engine_from_config` not being fully instrumented ([#2816](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2816)) - `opentelemetry-instrumentation-sqlalchemy`: Fix a remaining memory leak in EngineTracer ([#3053](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3053)) - `opentelemetry-instrumentation-sqlite3`: Update documentation on explicit cursor support of tracing ([#3088](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3088)) ### Breaking changes - `opentelemetry-instrumentation-sqlalchemy` teach instruments version ([#2971](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2971)) - Drop `opentelemetry-instrumentation-test` package from default instrumentation list ([#2969](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2969)) - `opentelemetry-instrumentation-httpx`: remove private unused `_InstrumentedClient` and `_InstrumentedAsyncClient` classes ([#3036](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3036)) ## Version 1.28.0/0.49b0 (2024-11-05) ### Added - `opentelemetry-instrumentation-openai-v2` Instrumentation for OpenAI >= 0.27.0 ([#2759](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2759)) - `opentelemetry-instrumentation-fastapi` Add autoinstrumentation mechanism tests. ([#2860](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2860)) - `opentelemetry-instrumentation-aiokafka` Add instrumentor and auto instrumentation support for aiokafka ([#2082](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2082)) - `opentelemetry-instrumentation-redis` Add additional attributes for methods create_index and search, rename those spans ([#2635](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2635)) - `opentelemetry-instrumentation` Add support for string based dotted module paths in unwrap ([#2919](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2919)) ### Fixed - `opentelemetry-instrumentation-aiokafka` Wrap `AIOKafkaConsumer.getone()` instead of `AIOKafkaConsumer.__anext__` ([#2874](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2874)) - `opentelemetry-instrumentation-confluent-kafka` Fix to allow `topic` to be extracted from `kwargs` in `produce()` ([#2901])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2901) - `opentelemetry-instrumentation-system-metrics` Update metric units to conform to UCUM conventions. ([#2922](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2922)) - `opentelemetry-instrumentation-celery` Don't detach context without a None token ([#2927](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2927)) - `opentelemetry-exporter-prometheus-remote-write`: sort labels before exporting ([#2940](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2940)) - `opentelemetry-instrumentation-dbapi` sqlcommenter key values created from PostgreSQL, MySQL systems ([#2897](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2897)) - `opentelemetry-instrumentation-system-metrics`: don't report open file descriptors on Windows ([#2946](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2946)) ### Breaking changes - Deprecation of pkg_resource in favor of importlib.metadata ([#2871](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2871)) - `opentelemetry-instrumentation` Don't fail distro loading if instrumentor raises ImportError, instead skip them ([#2923](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2923)) - `opentelemetry-instrumentation-httpx` Rewrote instrumentation to use wrapt instead of subclassing ([#2909](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2909)) ## Version 1.27.0/0.48b0 (2024-08-28) ### Added - `opentelemetry-instrumentation-kafka-python` Instrument temporary fork, kafka-python-ng inside kafka-python's instrumentation ([#2537](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2537)) - `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-fastapi` Add ability to disable internal HTTP send and receive spans ([#2802](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2802)) - `opentelemetry-instrumentation-asgi` Add fallback decoding for ASGI headers ([#2837](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2837)) ### Breaking changes - `opentelemetry-bootstrap` Remove `opentelemetry-instrumentation-aws-lambda` from the defaults instrumentations ([#2786](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2786)) ### Fixed - `opentelemetry-instrumentation-httpx` fix handling of async hooks ([#2823](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2823)) - `opentelemetry-instrumentation-system-metrics` fix `process.runtime.cpu.utilization` values to be shown in range of 0 to 1 ([#2812](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2812)) - `opentelemetry-instrumentation-fastapi` fix `fastapi` auto-instrumentation by removing `fastapi-slim` support, `fastapi-slim` itself is discontinued from maintainers ([#2783](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2783)) - `opentelemetry-instrumentation-aws-lambda` Avoid exception when a handler is not present. ([#2750](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2750)) - `opentelemetry-instrumentation-django` Fix regression - `http.target` re-added back to old semconv duration metrics ([#2746](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2746)) - `opentelemetry-instrumentation-asgi` do not set `url.full` attribute for server spans ([#2735](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2735)) - `opentelemetry-instrumentation-grpc` Fixes the issue with the gRPC instrumentation not working with the 1.63.0 and higher version of gRPC ([#2483](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2484)) - `opentelemetry-instrumentation-aws-lambda` Fixing w3c baggage support ([#2589](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2589)) - `opentelemetry-instrumentation-celery` propagates baggage ([#2385](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2385)) - `opentelemetry-instrumentation-asyncio` Fixes async generator coroutines not being awaited ([#2792](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2792)) - `opentelemetry-instrumentation-tornado` Handle http client exception and record exception info into span ([#2563](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2563)) - `opentelemetry-instrumentation` fix `http.host` new http semantic convention mapping to depend on `kind` of span ([#2814](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2814)) - `opentelemetry-instrumentation` Fix the description of `http.server.duration` and `http.server.request.duration` ([#2753](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2753)) - `opentelemetry-instrumentation-grpc` Fix grpc supported version ([#2845](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2845)) - `opentelemetry-instrumentation-asyncio` fix `AttributeError` in `AsyncioInstrumentor.trace_to_thread` when `func` is a `functools.partial` instance ([#2911](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2911)) ## Version 1.26.0/0.47b0 (2024-07-23) ### Added - `opentelemetry-instrumentation-flask` Add `http.route` and `http.target` to metric attributes ([#2621](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2621)) - `opentelemetry-instrumentation-aws-lambda` Enable global propagator for AWS instrumentation ([#2708](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2708)) - `opentelemetry-instrumentation-sklearn` Deprecated the sklearn instrumentation ([#2708](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2708)) - `opentelemetry-instrumentation-pyramid` Record exceptions raised when serving a request ([#2622](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2622)) - `opentelemetry-sdk-extension-aws` Add AwsXrayLambdaPropagator ([#2573](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2573)) - `opentelemetry-instrumentation-confluent-kafka` Add support for version 2.4.0 of confluent_kafka ([#2616](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2616)) - `opentelemetry-instrumentation-asyncpg` Add instrumentation to cursor based queries ([#2501](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2501)) - `opentelemetry-instrumentation-confluent-kafka` Add support for produce purge ([#2638](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2638)) - `opentelemetry-instrumentation-asgi` Implement new semantic convention opt-in with stable http semantic conventions ([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610)) - `opentelemetry-instrumentation-fastapi` Implement new semantic convention opt-in with stable http semantic conventions ([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682)) - `opentelemetry-instrumentation-httpx` Implement new semantic convention opt-in migration with stable http semantic conventions ([#2631](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2631)) - `opentelemetry-instrumentation-system-metrics` Permit to use psutil 6.0+. ([#2630](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2630)) - `opentelemetry-instrumentation-system-metrics` Add support for capture open file descriptors ([#2652](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2652)) - `opentelemetry-instrumentation-httpx` Add support for instrument client with proxy ([#2664](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2664)) - `opentelemetry-instrumentation-aiohttp-client` Implement new semantic convention opt-in migration ([#2673](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2673)) - `opentelemetry-instrumentation-django` Add `http.target` to Django duration metric attributes ([#2624](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2624)) - `opentelemetry-instrumentation-urllib3` Implement new semantic convention opt-in migration ([#2715](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2715)) - `opentelemetry-instrumentation-django` Implement new semantic convention opt-in with stable http semantic conventions ([#2714](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2714)) - `opentelemetry-instrumentation-urllib` Implement new semantic convention opt-in migration ([#2736](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2736)) ### Breaking changes - `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-fastapi`, `opentelemetry-instrumentation-starlette` Use `tracer` and `meter` of originating components instead of one from `asgi` middleware ([#2580](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2580)) - Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `asgi` middleware ([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610)) - Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `fastapi` middleware ([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682)) - `opentelemetry-instrumentation-urllib3` Populate `{method}` as `HTTP` on `_OTHER` methods for span name ([#2715](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2715)) - Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `fastapi` instrumentation ([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682)) - Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `django` middleware ([#2714](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2714)) - Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `urllib` instrumentation ([#2736](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2736)) - `opentelemetry-instrumentation-httpx`, `opentelemetry-instrumentation-aiohttp-client`, `opentelemetry-instrumentation-requests` Populate `{method}` as `HTTP` on `_OTHER` methods ([#2726](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2726)) - `opentelemetry-instrumentation-fastapi` Add dependency support for fastapi-slim ([#2702](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2702)) - `opentelemetry-instrumentation-urllib3` improve request_hook, replacing `headers` and `body` parameters with a single `request_info: RequestInfo` parameter that now contains the `method` and `url` ([#2711](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2711)) ### Fixed - Handle `redis.exceptions.WatchError` as a non-error event in redis instrumentation ([#2668](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2668)) - `opentelemetry-instrumentation-httpx` Ensure httpx.get or httpx.request like methods are instrumented ([#2538](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2538)) - Add Python 3.12 support ([#2572](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2572)) - `opentelemetry-instrumentation-aiohttp-server`, `opentelemetry-instrumentation-httpx` Ensure consistently use of suppress_instrumentation utils ([#2590](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2590)) - Reference symbols from generated semantic conventions ([#2611](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2611)) - `opentelemetry-instrumentation-psycopg` Bugfix: Handle empty statement. ([#2644](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2644)) - `opentelemetry-instrumentation-confluent-kafka` Confluent Kafka: Ensure consume span is ended when consumer is closed ([#2640](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2640)) - `opentelemetry-instrumentation-asgi` Fix generation of `http.target` and `http.url` attributes for ASGI apps using sub apps ([#2477](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2477)) - `opentelemetry-instrumentation-aws-lambda` Bugfix: AWS Lambda event source key incorrect for SNS in instrumentation library. ([#2612](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2612)) - `opentelemetry-instrumentation-asyncio` instrumented `asyncio.wait_for` properly raises `asyncio.TimeoutError` as expected ([#2637](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2637)) - `opentelemetry-instrumentation-django` Handle exceptions from request/response hooks ([#2153](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2153)) - `opentelemetry-instrumentation-asgi` Removed `NET_HOST_NAME` AND `NET_HOST_PORT` from active requests count attribute ([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610)) - `opentelemetry-instrumentation-asgi` Bugfix: Middleware did not set status code attribute on duration metrics for non-recording spans. ([#2627](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2627)) - `opentelemetry-instrumentation-mysql` Add support for `mysql-connector-python` v9 ([#2751](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2751)) ## Version 1.25.0/0.46b0 (2024-05-31) ### Breaking changes - Add return statement to Confluent kafka Producer poll() and flush() calls when instrumented by ConfluentKafkaInstrumentor().instrument_producer() ([#2527](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2527)) - Rename `type` attribute to `asgi.event.type` in `opentelemetry-instrumentation-asgi` ([#2300](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2300)) - Rename AwsLambdaInstrumentor span attributes `faas.id` to `cloud.resource_id`, `faas.execution` to `faas.invocation_id` ([#2372](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2372)) - Drop support for instrumenting elasticsearch client < 6 ([#2422](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2422)) - `opentelemetry-instrumentation-wsgi` Add `http.method` to `span.name` ([#2425](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2425)) - `opentelemetry-instrumentation-flask` Add `http.method` to `span.name` ([#2454](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2454)) - Record repeated HTTP headers in lists, rather than a comma separate strings for ASGI based web frameworks ([#2361](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2361)) - ASGI, FastAPI, Starlette: provide both send and receive hooks with `scope` and `message` for internal spans - ([#2546](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2546)) ### Added - `opentelemetry-sdk-extension-aws` Register AWS resource detectors under the `opentelemetry_resource_detector` entry point ([#2382](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2382)) - `opentelemetry-instrumentation-wsgi` Implement new semantic convention opt-in with stable http semantic conventions ([#2425](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2425)) - `opentelemetry-instrumentation-flask` Implement new semantic convention opt-in with stable http semantic conventions ([#2454](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2454)) - `opentelemetry-instrumentation-threading` Initial release for threading ([#2253](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2253)) - `opentelemetry-instrumentation-pika` Instrumentation for `channel.consume()` (supported only for global, non channel specific instrumentation) ([#2397](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2397)) - `opentelemetry-processor-baggage` Initial release ([#2436](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2436)) - `opentelemetry-processor-baggage` Add baggage key predicate ([#2535](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2535)) ### Fixed - `opentelemetry-instrumentation-dbapi` Fix compatibility with Psycopg3 to extract libpq build version ([#2500](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2500)) - `opentelemetry-instrumentation-grpc` AioClientInterceptor should propagate with a Metadata object ([#2363](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2363)) - `opentelemetry-instrumentation-boto3sqs` Instrument Session and resource ([#2161](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2161)) - `opentelemetry-instrumentation-aws-lambda` Fix exception handling for events with requestContext ([#2418](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2418)) - Use sqlalchemy version in sqlalchemy commenter instead of opentelemetry library version ([#2404](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2404)) - `opentelemetry-instrumentation-asyncio` Check for cancelledException in the future ([#2461](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2461)) - Remove SDK dependency from opentelemetry-instrumentation-grpc ([#2474](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2474)) - `opentelemetry-instrumentation-elasticsearch` Improved support for version 8 ([#2420](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2420)) - `opentelemetry-instrumentation-elasticsearch` Disabling instrumentation with native OTel support enabled ([#2524](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2524)) - `opentelemetry-instrumentation-asyncio` Check for **name** attribute in the coroutine ([#2521](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2521)) - `opentelemetry-instrumentation-requests` Fix wrong time unit for duration histogram ([#2553](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2553)) - `opentelemetry-util-http` Preserve brackets around literal IPv6 hosts ([#2552](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2552)) - `opentelemetry-util-redis` Fix net peer attribute for unix socket connection ([#2493](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2493)) ## Version 1.24.0/0.45b0 (2024-03-28) ### Added - `opentelemetry-instrumentation-psycopg` Async Instrumentation for psycopg 3.x ([#2146](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2146)) ### Fixed - `opentelemetry-instrumentation-celery` Allow Celery instrumentation to be installed multiple times ([#2342](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2342)) - Align gRPC span status codes to OTEL specification ([#1756](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1756)) - `opentelemetry-instrumentation-flask` Add importlib metadata default for deprecation warning flask version ([#2297](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/2297)) - Ensure all http.server.duration metrics have the same description ([#2151](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/2298)) - Fix regression in httpx `request.url` not being of type `httpx.URL` after `0.44b0` ([#2359](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2359)) - Avoid losing repeated HTTP headers ([#2266](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2266)) - `opentelemetry-instrumentation-elasticsearch` Don't send bulk request body as db statement ([#2355](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2355)) - AwsLambdaInstrumentor sets `cloud.account.id` span attribute ([#2367](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2367)) ### Added - `opentelemetry-instrumentation-fastapi` Add support for configuring header extraction via runtime constructor parameters ([#2241](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2241)) ## Version 1.23.0/0.44b0 (2024-02-23) - Drop support for 3.7 ([#2151](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2151)) - `opentelemetry-resource-detector-azure` Added 10s timeout to VM Resource Detector ([#2119](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2119)) - `opentelemetry-instrumentation-asyncpg` Allow AsyncPGInstrumentor to be instantiated multiple times ([#1791](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1791)) - `opentelemetry-instrumentation-confluent-kafka` Add support for higher versions until 2.3.0 of confluent_kafka ([#2132](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2132)) - `opentelemetry-resource-detector-azure` Changed timeout to 4 seconds due to [timeout bug](https://github.com/open-telemetry/opentelemetry-python/issues/3644) ([#2136](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2136)) - `opentelemetry-resource-detector-azure` Suppress instrumentation for `urllib` call ([#2178](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2178)) - AwsLambdaInstrumentor handles and re-raises function exception ([#2245](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2245)) ### Added - `opentelemetry-instrumentation-psycopg` Initial release for psycopg 3.x - `opentelemetry-instrumentation-asgi` Add support for configuring ASGI middleware header extraction via runtime constructor parameters ([#2026](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2026)) ## Version 1.22.0/0.43b0 (2023-12-14) ### Added - `opentelemetry-instrumentation-asyncio` Add support for asyncio ([#1919](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1943)) - `opentelemetry-instrumentation` Added Otel semantic convention opt-in mechanism ([#1987](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1987)) - `opentelemetry-instrumentation-httpx` Fix mixing async and non async hooks ([#1920](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1920)) - `opentelemetry-instrumentation-requests` Implement new semantic convention opt-in with stable http semantic conventions ([#2002](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2002)) - `opentelemetry-instrument-grpc` Fix arity of context.abort for AIO RPCs ([#2066](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2066)) - Consolidate instrumentation suppression mechanisms and fix bug in httpx instrumentation ([#2061](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2061)) ### Fixed - `opentelemetry-instrumentation-httpx` Remove URL credentials ([#2020](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2020)) - `opentelemetry-instrumentation-urllib`/`opentelemetry-instrumentation-urllib3` Fix metric descriptions to match semantic conventions ([#1959](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1959)) - `opentelemetry-resource-detector-azure` Added dependency for Cloud Resource ID attribute ([#2072](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2072)) ## Version 1.21.0/0.42b0 (2023-11-01) ### Added - `opentelemetry-instrumentation-aiohttp-server` Add instrumentor and auto instrumentation support for aiohttp-server ([#1800](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1800)) - `opentelemetry-instrumentation-botocore` Include SNS topic ARN as a span attribute with name `messaging.destination.name` to uniquely identify the SNS topic ([#1995](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1995)) - `opentelemetry-instrumentation-system-metrics` Add support for collecting process metrics ([#1948](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1948)) - Added schema_url (`"https://opentelemetry.io/schemas/1.11.0"`) to all metrics and traces ([#1977](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1977)) ### Fixed - `opentelemetry-instrumentation-aio-pika` and `opentelemetry-instrumentation-pika` Fix missing trace context propagation when trace not recording. ([#1969](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1969)) - Fix version of Flask dependency `werkzeug` ([#1980](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1980)) - `opentelemetry-resource-detector-azure` Using new Cloud Resource ID attribute. ([#1976](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1976)) - Do not collect `system.network.connections` by default on macOS which was causing exceptions in metrics collection. ([#2008](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2008)) ## Version 1.20.0/0.41b0 (2023-09-01) ### Fixed - `opentelemetry-instrumentation-asgi` Fix UnboundLocalError local variable 'start' referenced before assignment ([#1889](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1889)) - Fixed union typing error not compatible with Python 3.7 introduced in `opentelemetry-util-http`, fix tests introduced by patch related to sanitize method for wsgi ([#1913](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1913)) - `opentelemetry-instrumentation-celery` Unwrap Celery's `ExceptionInfo` errors and report the actual exception that was raised. ([#1863](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1863)) ### Added - `opentelemetry-resource-detector-azure` Add resource detectors for Azure App Service and VM ([#1901](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1901)) - `opentelemetry-instrumentation-flask` Add support for Flask 3.0.0 ([#152](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2013)) ## Version 1.19.0/0.40b0 (2023-07-13) - `opentelemetry-instrumentation-asgi` Add `http.server.request.size` metric ([#1867](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1867)) ### Fixed - `opentelemetry-instrumentation-django` Fix empty span name when using `path("", ...)` ([#1788](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1788) - Fix elastic-search instrumentation sanitization to support bulk queries ([#1870](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1870)) - Update falcon instrumentation to follow semantic conventions ([#1824](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1824)) - Fix sqlalchemy instrumentation wrap methods to accept sqlcommenter options ([#1873](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1873)) - Exclude background task execution from root server span in ASGI middleware ([#1952](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1952)) ### Added - Add instrumentor support for cassandra and scylla ([#1902](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1902)) - Add instrumentor support for mysqlclient ([#1744](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1744)) - Fix async redis clients not being traced correctly ([#1830](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1830)) - Make Flask request span attributes available for `start_span`. ([#1784](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1784)) - Fix falcon instrumentation's usage of Span Status to only set the description if the status code is ERROR. ([#1840](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1840)) - Instrument all httpx versions >= 0.18. ([#1748](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1748)) - Fix `Invalid type NoneType for attribute X (opentelemetry-instrumentation-aws-lambda)` error when some attributes do not exist ([#1780](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1780)) - Add metric instrumentation for celery ([#1679](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1679)) - `opentelemetry-instrumentation-asgi` Add `http.server.response.size` metric ([#1789](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1789)) - `opentelemetry-instrumentation-grpc` Allow gRPC connections via Unix socket ([#1833](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1833)) - Fix elasticsearch `Transport.perform_request` instrument wrap for elasticsearch >= 8 ([#1810](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1810)) - `opentelemetry-instrumentation-urllib3` Add support for urllib3 version 2 ([#1879](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1879)) - Add optional distro and configurator selection for auto-instrumentation ([#1823](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1823)) - `opentelemetry-instrumentation-django` - Add option to add Opentelemetry middleware at specific position in middleware chain ([#2912]https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2912) ### Added - `opentelemetry-instrumentation-kafka-python` Add instrumentation to `consume` method ([#1786](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1786)) ## Version 1.18.0/0.39b0 (2023-05-10) - Update runtime metrics to follow semantic conventions ([#1735](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1735)) - Add request and response hooks for GRPC instrumentation (client only) ([#1706](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1706)) - Fix memory leak in SQLAlchemy instrumentation where disposed `Engine` does not get garbage collected ([#1771](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1771)) - `opentelemetry-instrumentation-pymemcache` Update instrumentation to support pymemcache >4 ([#1764](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1764)) - `opentelemetry-instrumentation-confluent-kafka` Add support for higher versions of confluent_kafka ([#1815](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1815)) ### Added - Expand sqlalchemy pool.name to follow the semantic conventions ([#1778](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1778)) - Add `excluded_urls` functionality to `urllib` and `urllib3` instrumentations ([#1733](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1733)) - Make Django request span attributes available for `start_span`. ([#1730](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1730)) - Make ASGI request span attributes available for `start_span`. ([#1762](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1762)) - `opentelemetry-instrumentation-celery` Add support for anonymous tasks. ([#1407](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1407)) - `opentelemetry-instrumentation-logging` Add `otelTraceSampled` to instrumetation-logging ([#1773](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1773)) ### Changed - `opentelemetry-instrumentation-botocore` now uses the AWS X-Ray propagator by default ([#1741](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1741)) ### Fixed - Fix redis db.statements to be sanitized by default ([#1778](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1778)) - Fix elasticsearch db.statement attribute to be sanitized by default ([#1758](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1758)) - Fix `AttributeError` when AWS Lambda handler receives a list event ([#1738](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1738)) - Fix `None does not implement middleware` error when there are no middlewares registered ([#1766](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1766)) - Fix Flask instrumentation to only close the span if it was created by the same request context. ([#1692](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1692)) ### Changed - Update HTTP server/client instrumentation span names to comply with spec ([#1759](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1759)) ## Version 1.17.0/0.38b0 (2023-03-22) ### Added - Add connection attributes to sqlalchemy connect span ([#1608](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1608)) - Add support for enabling Redis sanitization from environment variable ([#1690](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1690)) - Add metrics instrumentation for sqlalchemy ([#1645](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1645)) ### Fixed - Fix Flask instrumentation to only close the span if it was created by the same thread. ([#1654](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1654)) - Fix confluent-kafka instrumentation by allowing Producer headers to be dict or list ([#1655](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1655)) - `opentelemetry-instrumentation-system-metrics` Fix initialization of the instrumentation class when configuration is provided ([#1438](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1439)) - Fix exception in Urllib3 when dealing with filelike body. ([#1399](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1399)) - Fix httpx resource warnings ([#1695](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1695)) ### Changed - `opentelemetry-instrumentation-requests` Replace `name_callback` and `span_callback` with standard `response_hook` and `request_hook` callbacks ([#670](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/670)) ## Version 1.16.0/0.37b0 (2023-02-17) ### Added - Support `aio_pika` 9.x ([#1670](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1670)) - `opentelemetry-instrumentation-redis` Add `sanitize_query` config option to allow query sanitization. ([#1572](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1572)) - `opentelemetry-instrumentation-elasticsearch` Add optional db.statement query sanitization. ([#1598](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1598)) - `opentelemetry-instrumentation-celery` Record exceptions as events on the span. ([#1573](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1573)) - Add metric instrumentation for urllib ([#1553](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1553)) - `opentelemetry/sdk/extension/aws` Implement [`aws.ecs.*`](https://opentelemetry.io/docs/specs/semconv/resource/cloud-provider/aws/ecs/) and [`aws.logs.*`](https://opentelemetry.io/docs/specs/semconv/resource/cloud-provider/aws/logs/) resource attributes in the `AwsEcsResourceDetector` detector when the ECS Metadata v4 is available ([#1212](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1212)) - `opentelemetry-instrumentation-aio-pika` Support `aio_pika` 8.x ([#1481](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1481)) - `opentelemetry-instrumentation-aws-lambda` Flush `MeterProvider` at end of function invocation. ([#1613](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1613)) - Fix aiohttp bug with unset `trace_configs` ([#1592](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1592)) - `opentelemetry-instrumentation-django` Allow explicit `excluded_urls` configuration through `instrument()` ([#1618](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1618)) ### Fixed - Fix TortoiseORM instrumentation `AttributeError: type object 'Config' has no attribute 'title'` ([#1575](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1575)) - Fix SQLAlchemy uninstrumentation ([#1581](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1581)) - `opentelemetry-instrumentation-grpc` Fix code()/details() of \_OpentelemetryServicerContext. ([#1578](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1578)) - Fix aiopg instrumentation to work with aiopg < 2.0.0 ([#1473](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1473)) - `opentelemetry-instrumentation-aws-lambda` Adds an option to configure `disable_aws_context_propagation` by environment variable: `OTEL_LAMBDA_DISABLE_AWS_CONTEXT_PROPAGATION` ([#1507](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1507)) - Fix pymongo to collect the property DB_MONGODB_COLLECTION ([#1555](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1555)) - `opentelemetry-instrumentation-asgi` Fix keys() in class ASGIGetter to correctly fetch values from carrier headers. ([#1435](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1435)) - mongo db - fix db statement capturing ([#1512](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1512)) - Add commit method for ConfluentKafkaInstrumentor's ProxiedConsumer ([#1656](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1656)) ## Version 1.15.0/0.36b0 (2022-12-10) - Add uninstrument test for sqlalchemy ([#1471](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1471)) - `opentelemetry-instrumentation-tortoiseorm` Initial release ([#685](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/685)) - Add metric instrumentation for tornado ([#1252](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1252)) - `opentelemetry-instrumentation-aws-lambda` Add option to disable aws context propagation ([#1466](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1466)) ### Added - `opentelemetry-resource-detector-container` Add support resource detection of container properties. ([#1584](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1584)) - `opentelemetry-instrumentation-pymysql` Add tests for commit() and rollback(). ([#1424](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1424)) - `opentelemetry-instrumentation-fastapi` Add support for regular expression matching and sanitization of HTTP headers. ([#1403](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1403)) - `opentelemetry-instrumentation-botocore` add support for `messaging.*` in the sqs extension. ([#1350](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1350)) - `opentelemetry-instrumentation-starlette` Add support for regular expression matching and sanitization of HTTP headers. ([#1404](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1404)) - `opentelemetry-instrumentation-botocore` Add support for SNS `publish` and `publish_batch`. ([#1409](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1409)) - Strip leading comments from SQL queries when generating the span name. ([#1434](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1434)) - `opentelemetry-instrumentation-confluent-kafka` Add support for the latest versions of the library. ([#1468](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1468)) ### Fixed - Fix bug in Urllib instrumentation - add status code to span attributes only if the status code is not None. ([#1430](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1430)) - `opentelemetry-instrumentation-aiohttp-client` Allow overriding of status in response hook. ([#1394](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1394)) - `opentelemetry-instrumentation-pymysql` Fix dbapi connection instrument wrapper has no \_sock member. ([#1424](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1424)) - `opentelemetry-instrumentation-dbapi` Fix the check for the connection already being instrumented in instrument_connection(). ([#1424](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1424)) - Remove db.name attribute from Redis instrumentation ([#1427](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1427)) - `opentelemetry-instrumentation-asgi` Fix target extraction for duration metric ([#1461](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1461)) - Add grpc.aio instrumentation to package entry points ([#1442](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1442)) - Fix a bug in SQLAlchemy instrumentation - support disabling enable_commenter variable ([#1440](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1440)) ## Version 1.14.0/0.35b0 (2022-11-03) ### Deprecated - `opentelemetry-distro` Deprecate `otlp_proto_grpc` and `otlp_proto_http` in favor of using `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` as according to specifications ([#1250](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1250)) ### Added - Capture common HTTP attributes from API Gateway proxy events in `opentelemetry-instrumentation-aws-lambda` ([#1233](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1233)) - Add metric instrumentation for tornado ([#1252](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1252)) - `opentelemetry-instrumentation-django` Fixed bug where auto-instrumentation fails when django is installed and settings are not configured. ([#1369](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1369)) - `opentelemetry-instrumentation-system-metrics` add supports to collect system thread count. ([#1339](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1339)) - `opentelemetry-exporter-richconsole` Fixing RichConsoleExpoter to allow multiple traces, fixing duplicate spans and include resources ([#1336](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1336)) - `opentelemetry-instrumentation-asgi` Add support for regular expression matching and sanitization of HTTP headers. ([#1333](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1333)) - `opentelemetry-instrumentation-asgi` metrics record target attribute (FastAPI only) ([#1323](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1323)) - `opentelemetry-instrumentation-wsgi` Add support for regular expression matching and sanitization of HTTP headers. ([#1402](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1402)) - Add support for py3.11 ([#1415](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1415)) - `opentelemetry-instrumentation-django` Add support for regular expression matching and sanitization of HTTP headers. ([#1411](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1411)) - `opentelemetry-instrumentation-falcon` Add support for regular expression matching and sanitization of HTTP headers. ([#1412](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1412)) - `opentelemetry-instrumentation-flask` Add support for regular expression matching and sanitization of HTTP headers. ([#1413](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1413)) - `opentelemetry-instrumentation-pyramid` Add support for regular expression matching and sanitization of HTTP headers. ([#1414](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1414)) - `opentelemetry-instrumentation-grpc` Add support for grpc.aio Clients and Servers ([#1245](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1245)) - Add metric exporter for Prometheus Remote Write ([#1359](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1359)) ### Fixed - Fix bug in Falcon instrumentation ([#1377](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1377)) - `opentelemetry-instrumentation-asgi` Fix keys() in class ASGIGetter so it decodes the keys before returning them. ([#1333](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1333)) - `opentelemetry-instrumentation-asgi` Make ASGIGetter.get() compare all keys in a case insensitive manner. ([#1333](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1333)) - Use resp.text instead of resp.body for Falcon 3 to avoid a deprecation warning. ([#1412](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1412)) ## Version 1.13.0/0.34b0 (2022-09-26) - `opentelemetry-instrumentation-asyncpg` Fix high cardinality in the span name ([#1324](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1324)) ### Added - `opentelemetry-instrumentation-grpc` add supports to filter requests to instrument. ([#1241](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1241)) - Flask sqlalchemy psycopg2 integration ([#1224](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1224)) - Add metric instrumentation in Falcon ([#1230](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1230)) - Add metric instrumentation in fastapi ([#1199](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1199)) - Add metric instrumentation in Pyramid ([#1242](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1242)) - `opentelemetry-util-http` Add support for sanitizing HTTP header values. ([#1253](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1253)) - Add metric instrumentation in starlette ([#1327](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1327)) ### Fixed - `opentelemetry-instrumentation-kafka-python`: wait for metadata ([#1260](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1260)) - `opentelemetry-instrumentation-boto3sqs` Make propagation compatible with other SQS instrumentations, add 'messaging.url' span attribute, and fix missing package dependencies. ([#1234](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1234)) - `opentelemetry-instrumentation-pymongo` Change span names to not contain queries but only database name and command name ([#1247](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1247)) - restoring metrics in django framework ([#1208](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1208)) - `opentelemetry-instrumentation-aiohttp-client` Fix producing additional spans with each newly created ClientSession - ([#1246](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1246)) - Add \_is_opentelemetry_instrumented check in \_InstrumentedFastAPI class ([#1313](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1313)) - Fix uninstrumentation of existing app instances in FastAPI ([#1258](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1258)) - Fix uninstrumentation of existing app instances in falcon ([#1341]https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1341) ## Version 1.12.0/0.33b0 (2022-08-08) - Adding multiple db connections support for django-instrumentation's sqlcommenter ([#1187](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1187)) - SQLCommenter semicolon bug fix ([#1200](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1200/files)) - Adding sqlalchemy native tags in sqlalchemy commenter ([#1206](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1206)) - Add psycopg2 native tags to sqlcommenter ([#1203](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1203)) ### Added - `opentelemetry-instrumentation-redis` add support to instrument RedisCluster clients ([#1177](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1177)) - `opentelemetry-instrumentation-sqlalchemy` Added span for the connection phase ([#1133](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1133)) - Add metric instrumentation in asgi ([#1197](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1197)) - Add metric instrumentation for flask ([#1186](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1186)) - Add a test for asgi using NoOpTracerProvider ([#1367](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1367)) ## [1.12.0rc2-0.32b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc2) - 2022-07-01 - Pyramid: Only categorize 500s server exceptions as errors ([#1037](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1037)) ### Fixed - Fix bug in system metrics by checking their configuration ([#1129](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1129)) - Adding escape call to fix [auto-instrumentation not producing spans on Windows](https://github.com/open-telemetry/opentelemetry-python/issues/2703). ([#1100](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1100)) - `opentelemetry-instrumentation-grpc` narrow protobuf dependency to exclude protobuf >= 4 ([#1109](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1109)) - cleanup type hints for textmap `Getter` and `Setter` classes - Suppressing downstream HTTP instrumentation to avoid [extra spans](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/930) ([#1116](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1116)) - fixed typo in `system.network.io` metric configuration ([#1135](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1135)) ### Added - `opentelemetry-instrumentation-aiohttp-client` Add support for optional custom trace_configs argument. ([1079](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1079)) - `opentelemetry-instrumentation-sqlalchemy` add support to instrument multiple engines ([#1132](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1132)) - `opentelemetry-instrumentation-logging` add log hook support ([#1117](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1117)) - `opentelemetry-instrumentation-remoulade` Initial release ([#1082](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1082)) - Added `opentelemetry-instrumention-confluent-kafka` ([#1111](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1111)) - Set otlp-proto-grpc as the default metrics exporter for auto-instrumentation ([#1127](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1127)) - Add metric instrumentation for WSGI ([#1128](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1128)) - Add metric instrumentation for Urllib3 ([#1198](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1198)) - `opentelemetry-instrumentation-aio-pika` added RabbitMQ aio-pika module instrumentation. ([#1095](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1095)) - `opentelemetry-instrumentation-requests` Restoring metrics in requests ([#1110](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1110)) - Integrated sqlcommenter plugin into opentelemetry-instrumentation-django ([#896](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/896)) ## Version 1.12.0rc1/0.31b0 (2022-05-17) ### Fixed - `opentelemetry-instrumentation-aiohttp-client` make span attributes available to sampler ([#1072](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1072)) - `opentelemetry-instrumentation-aws-lambda` Fixed an issue - in some rare cases (API GW proxy integration test) headers are set to None, breaking context propagators. ([#1055](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1055)) - Refactoring custom header collection API for consistency ([#1064](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1064)) - `opentelemetry-instrumentation-sqlalchemy` will correctly report `otel.library.name` ([#1086](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1086)) - `opentelemetry-sdk-extension-aws` change timeout for AWS EC2 and EKS metadata requests from 1000 seconds and 2000 seconds to 1 second ### Added - `opentelemetry-instrument` and `opentelemetry-bootstrap` now include a `--version` flag ([#1065](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1065)) - `opentelemetry-instrumentation-redis` now instruments asynchronous Redis clients, if the installed redis-py includes async support (>=4.2.0). ([#1076](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1076)) - `opentelemetry-instrumentation-boto3sqs` added AWS's SQS instrumentation. ([#1081](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1081)) ## Version 1.11.1/0.30b1 (2022-04-21) ### Added - `opentelemetry-instrumentation-starlette` Capture custom request/response headers in span attributes ([#1046](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1046)) ### Fixed - Prune autoinstrumentation sitecustomize module directory from PYTHONPATH immediately ([#1066](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1066)) ## Version 1.11.0/0.30b0 (2022-04-18) ### Fixed - `opentelemetry-instrumentation-pyramid` Fixed which package is the correct caller in \_traced_init. ([#830](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/830)) - `opentelemetry-instrumentation-tornado` Fix Tornado errors mapping to 500 ([#1048](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1048)) - `opentelemetry-instrumentation-urllib` make span attributes available to sampler ([1014](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1014)) - `opentelemetry-instrumentation-flask` Fix non-recording span bug ([#999](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/999)) - `opentelemetry-instrumentation-tornado` Fix non-recording span bug ([#999](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/999)) ### Added - `opentelemetry-instrumentation-fastapi` Capture custom request/response headers in span attributes ([#1032](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1032)) - `opentelemetry-instrumentation-django` Capture custom request/response headers in span attributes ([#1024](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1024)) - `opentelemetry-instrumentation-asgi` Capture custom request/response headers in span attributes ([#1004](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1004)) - `opentelemetry-instrumentation-psycopg2` extended the sql commenter support of dbapi into psycopg2 ([#940](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/940)) - `opentelemetry-instrumentation-falcon` Add support for falcon==1.4.1 ([#1000](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1000)) - `opentelemetry-instrumentation-falcon` Falcon: Capture custom request/response headers in span attributes ([#1003](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1003)) - `opentelemetry-instrumentation-elasticsearch` no longer creates unique span names by including search target, replaces them with `` and puts the value in attribute `elasticsearch.target` ([#1018](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1018)) - `opentelemetry-instrumentation-pyramid` Handle non-HTTPException exceptions ([#1001](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1001)) - `opentelemetry-instrumentation-system-metrics` restore `SystemMetrics` instrumentation as `SystemMetricsInstrumentor` ([#1012](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1012)) - `opentelemetry-instrumentation-pyramid` Pyramid: Capture custom request/response headers in span attributes ([#1022](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1022)) ## Version 1.10.0/0.29b0 (2022-03-10) - `opentelemetry-instrumentation-wsgi` Capture custom request/response headers in span attributes ([#925](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/925)) - `opentelemetry-instrumentation-flask` Flask: Capture custom request/response headers in span attributes ([#952](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/952)) - `opentelemetry-instrumentation-tornado` Tornado: Capture custom request/response headers in span attributes ([#950](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/950)) ### Added - `opentelemetry-instrumentation-aws-lambda` `SpanKind.SERVER` by default, add more cases for `SpanKind.CONSUMER` services. ([#926](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/926)) - `opentelemetry-instrumentation-sqlalchemy` added experimental sql commenter capability ([#924](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/924)) - `opentelemetry-contrib-instrumentations` added new meta-package that installs all contrib instrumentations. ([#681](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/681)) - `opentelemetry-instrumentation-dbapi` add experimental sql commenter capability ([#908](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/908)) - `opentelemetry-instrumentation-requests` make span attribute available to samplers ([#931](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/931)) - `opentelemetry-datadog-exporter` add deprecation note to example. ([#900](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/900)) ### Fixed - `opentelemetry-instrumentation-dbapi` Changed the format of traceparent id. ([#941](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/941)) - `opentelemetry-instrumentation-logging` retrieves service name defensively. ([#890](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/890)) - `opentelemetry-instrumentation-wsgi` WSGI: Conditionally create SERVER spans ([#903](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/903)) - `opentelemetry-instrumentation-falcon` Safer patching mechanism ([#895](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/895)) - `opentelemetry-instrumentation-kafka-python` Fix topic extraction ([#949](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/949)) ### Changed - `opentelemetry-instrumentation-pymemcache` should run against newer versions of pymemcache. ([#935](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/935)) ## Version 1.9.1/0.28b1 (2022-01-29) ### Fixed - `opentelemetry-instrumentation-pika` requires `packaging` dependency - `opentelemetry-instrumentation-tornado` Tornado: Conditionally create SERVER spans ([#889](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/889)) ## Version 1.9.0/0.28b0 (2022-01-26) ### Added - `opentelemetry-instrumentation-pyramid` Pyramid: Conditionally create SERVER spans ([#869](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/869)) - `opentelemetry-instrumentation-grpc` added `trailing_metadata` to \_OpenTelemetryServicerContext. ([#871](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/871)) - `opentelemetry-instrumentation-asgi` now returns a `traceresponse` response header. ([#817](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/817)) - `opentelemetry-instrumentation-kafka-python` added kafka-python module instrumentation. ([#814](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/814)) - `opentelemetry-instrumentation-falcon` Falcon: Conditionally create SERVER spans ([#867](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/867)) - `opentelemetry-instrumentation-pymongo` now supports `pymongo v4` ([#876](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/876)) - `opentelemetry-instrumentation-httpx` now supports versions higher than `0.19.0`. ([#866](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/866)) ### Fixed - `opentelemetry-instrumentation-django` Django: Conditionally create SERVER spans ([#832](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/832)) - `opentelemetry-instrumentation-flask` Flask: Conditionally create SERVER spans ([#828](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/828)) - `opentelemetry-instrumentation-celery` Celery: Support partial task time limit ([#846](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/846)) - `opentelemetry-instrumentation-asgi` ASGI: Conditionally create SERVER spans ([#843](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/843)) - `opentelemetry-instrumentation-django` Django: fix issue preventing detection of MIDDLEWARE_CLASSES - `opentelemetry-instrumentation-sqlite3` Instrumentation now works with `dbapi2.connect` - `opentelemetry-instrumentation-kafka` Kafka: safe kafka partition extraction ([#872](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/872)) - `opentelemetry-instrumentation-aiohttp-client` aiohttp: Correct url filter input type ([#843](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/864)) - `opentelemetry-instrumentation-aiohttp-client` aiohttp: Remove `span_name` from docs ([#857](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/857)) ## Version 1.8.0/0.27b0 (2021-12-17) ### Added - `opentelemetry-instrumentation-aws-lambda` Adds support for configurable flush timeout via `OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT` property. ([#825](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/825)) - `opentelemetry-instrumentation-pika` Adds support for versions between `0.12.0` to `1.0.0`. ([#837](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/837)) ### Fixed - `opentelemetry-instrumentation-urllib` Fixed an error on unexpected status values. ([#823](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/823)) - `opentelemetry-exporter-richconsole` Fixed attribute error on parentless spans. ([#782](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/782)) - `opentelemetry-instrumentation-tornado` Add support instrumentation for Tornado 5.1.1 ([#812](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/812)) ## Version 1.7.1/0.26b1 (2021-11-11) ### Added - `opentelemetry-instrumentation-aws-lambda` Add instrumentation for AWS Lambda Service - pkg metadata files (Part 1/2) ([#739](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/739)) - Add support for Python 3.10 ([#742](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/742)) - Pass in auto-instrumentation version to configurator ([#783](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/783)) - `opentelemetry-instrumentation` Add `setuptools` to `install_requires` ([#781](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/781)) - `opentelemetry-instrumentation-aws-lambda` Add instrumentation for AWS Lambda Service - Implementation (Part 2/2) ([#777](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/777)) - `opentelemetry-instrumentation-pymongo` Add `request_hook`, `response_hook` and `failed_hook` callbacks passed as arguments to the instrument method ([#793](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/793)) - `opentelemetry-instrumentation-pymysql` Add support for PyMySQL 1.x series ([#792](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/792)) - Add support for generic OTEL_PYTHON_EXCLUDED_URLS variable ([#790](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/790)) ### Fixed - `opentelemetry-instrumentation-asgi` now explicitly depends on asgiref as it uses the package instead of instrumenting it. ([#765](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/765)) - `opentelemetry-instrumentation-pika` now propagates context to basic_consume callback ([#766](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/766)) - `opentelemetry-instrumentation-falcon` Dropped broken support for Python 3.4. ([#774](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/774)) - `opentelemetry-instrumentation-django` Fixed carrier usage on ASGI requests. ([#767](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/767)) - Don't set Span Status on 4xx http status code for SpanKind.SERVER spans ([#776](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/776)) - `opentelemetry-instrumentation-django` Fixed instrumentation and tests for all Django major versions. ([#780](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/780)) ## Version 1.6.2/0.25b2 (2021-10-19) - `opentelemetry-instrumentation-sqlalchemy` Fix PostgreSQL instrumentation for Unix sockets ([#761](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/761)) ### Changed - `opentelemetry-sdk-extension-aws` & `opentelemetry-propagator-aws` Release AWS Python SDK Extension as 2.0.1 and AWS Propagator as 1.0.1 ([#753](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/753)) - `opentelemetry-instrumentation-pika` Add `_decorate_basic_consume` to ensure post instrumentation `basic_consume` calls are also instrumented. ([#759](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/759)) - Consolidate instrumentation documentation in docstrings ([#754](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/754)) ### Fixed - `opentelemetry-distro` uses the correct entrypoint name which was updated in the core release of 1.6.0 but the distro was not updated with it ([#755](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/755)) ### Added - `opentelemetry-instrumentation-pika` Add `publish_hook` and `consume_hook` callbacks passed as arguments to the instrument method ([#763](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/763)) ## Version 1.6.1/0.25b1 (2021-10-18) ### Changed - `opentelemetry-util-http` no longer contains an instrumentation entrypoint and will not be loaded automatically by the auto instrumentor. ([#745](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/745)) - `opentelemetry-instrumentation-pika` Bugfix use properties.headers. It will prevent the header injection from raising. ([#740](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/740)) - `opentelemetry-instrumentation-botocore` Add extension for DynamoDB ([#735](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/735)) - `opentelemetry-sdk-extension-aws` & `opentelemetry-propagator-aws` Remove unnecessary dependencies on `opentelemetry-test` ([#752](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/752)) - `opentelemetry-instrumentation-botocore` Add Lambda extension ([#760](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/760)) ## Version 1.6.0/0.25b0 (2021-10-13) ### Added - `opentelemetry-sdk-extension-aws` Release AWS Python SDK Extension as 1.0.0 ([#667](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/667)) - `opentelemetry-instrumentation-urllib3`, `opentelemetry-instrumentation-requests` The `net.peer.ip` attribute is set to the IP of the connected HTTP server or proxy using a new instrumentor in `opententelemetry-util-http` ([#661](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/661)) - `opentelemetry-instrumentation-pymongo` Add check for suppression key in PyMongo. ([#736](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/736)) - `opentelemetry-instrumentation-elasticsearch` Added `response_hook` and `request_hook` callbacks ([#670](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/670)) - `opentelemetry-instrumentation-redis` added request_hook and response_hook callbacks passed as arguments to the instrument method. ([#669](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/669)) - `opentelemetry-instrumentation-botocore` add `request_hook` and `response_hook` callbacks ([679](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/679)) - `opentelemetry-exporter-richconsole` Initial release ([#686](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/686)) - `opentelemetry-instrumentation-elasticsearch` no longer creates unique span names by including document IDs, replaces them with `:id` and puts the value in attribute `elasticsearch.id` ([#705](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/705)) - `opentelemetry-instrumentation-tornado` now sets `http.client_ip` and `tornado.handler` attributes ([#706](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/706)) - `opentelemetry-instrumentation-requests` added exclude urls functionality ([#714](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/714)) - `opentelemetry-instrumentation-django` Add ASGI support ([#391](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/391)) ### Changed - `opentelemetry-instrumentation-flask` Fix `RuntimeError: Working outside of request context` ([#734](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/734)) - `opentelemetry-propagators-aws-xray` Rename `AwsXRayFormat` to `AwsXRayPropagator` ([#729](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/729)) - `opentelemetry-instrumentation-sqlalchemy` Respect provided tracer provider when instrumenting SQLAlchemy ([#728](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/728)) - `opentelemetry-sdk-extension-aws` Move AWS X-Ray Propagator into its own `opentelemetry-propagators-aws` package ([#720](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/720)) - `opentelemetry-instrumentation-sqlalchemy` Added `packaging` dependency ([#713](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/713)) - `opentelemetry-instrumentation-jinja2` Allow instrumentation of newer Jinja2 versions. ([#712](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/712)) - `opentelemetry-instrumentation-botocore` Make common span attributes compliant with semantic conventions ([#674](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/674)) - `opentelemetry-sdk-extension-aws` Release AWS Python SDK Extension as 1.0.0 ([#667](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/667)) - `opentelemetry-instrumentation-botocore` Unpatch botocore Endpoint.prepare_request on uninstrument ([#664](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/664)) - `opentelemetry-instrumentation-botocore` Fix span injection for lambda invoke ([#663](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/663)) - `opentelemetry-instrumentation-botocore` Introduce instrumentation extensions ([#718](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/718)) - `opentelemetry-instrumentation-urllib3` Updated `_RequestHookT` with two additional fields - the request body and the request headers ([#660](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/660)) - Tests for Falcon 3 support ([#644](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/644)) ## Version 1.5.0/0.24b0 (2021-08-26) ### Added - `opentelemetry-sdk-extension-aws` Add AWS resource detectors to extension package ([#586](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/586)) - `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-aiohttp-client`, `openetelemetry-instrumentation-fastapi`, `opentelemetry-instrumentation-starlette`, `opentelemetry-instrumentation-urllib`, `opentelemetry-instrumentation-urllib3` Added `request_hook` and `response_hook` callbacks ([#576](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/576)) - `opentelemetry-instrumentation-pika` added RabbitMQ's pika module instrumentation. ([#680](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/680)) ### Changed - `opentelemetry-instrumentation-fastapi` Allow instrumentation of newer FastAPI versions. ([#602](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/602)) - Enable explicit `excluded_urls` argument in `opentelemetry-instrumentation-flask` ([#604](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/604)) ## Version 1.4.0/0.23b0 (2021-07-21) ### Removed - Move `opentelemetry-instrumentation` to the core repo. ([#595](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/595)) ### Changed - `opentelemetry-instrumentation-falcon` added support for Falcon 3. ([#607](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/607)) - `opentelemetry-instrumentation-tornado` properly instrument work done in tornado on_finish method. ([#499](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/499)) - `opentelemetry-instrumentation` Fixed cases where trying to use an instrumentation package without the target library was crashing auto instrumentation agent. ([#530](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/530)) - Fix weak reference error for pyodbc cursor in SQLAlchemy instrumentation. ([#469](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/469)) - Implemented specification that HTTP span attributes must not contain username and password. ([#538](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/538)) - Changed the psycopg2-binary to psycopg2 as dependency in production ([#543](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/543)) - Implement consistent way of checking if instrumentation is already active ([#549](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/549)) - Require aiopg to be less than 1.3.0 ([#560](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/560)) - `opentelemetry-instrumentation-django` Migrated Django middleware to new-style. ([#533](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/533)) - Updating dependency for opentelemetry api/sdk packages to support major version instead of pinning to specific versions. ([#567](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/567)) - `opentelemetry-instrumentation-grpc` Respect the suppress instrumentation in gRPC client instrumentor ([#559](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/559)) - `opentelemetry-instrumentation-grpc` Fixed asynchronous unary call traces ([#536](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/536)) - `opentelemetry-sdk-extension-aws` Update AWS entry points to match spec ([#566](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/566)) - Include Flask 2.0 as compatible with existing flask instrumentation ([#545](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/545)) - `openelemetry-sdk-extension-aws` Take a dependency on `opentelemetry-sdk` ([#558](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/558)) - Change `opentelemetry-instrumentation-httpx` to replace `client` classes with instrumented versions. ([#577](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/577)) - `opentelemetry-instrumentation-requests` Fix potential `AttributeError` when `requests` is used with a custom transport adapter. ([#562](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/562)) - `opentelemetry-instrumentation-django` Fix AttributeError: ResolverMatch object has no attribute route ([#581](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/581)) - `opentelemetry-instrumentation-botocore` Suppress botocore downstream instrumentation like urllib3 ([#563](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/563)) - `opentelemetry-exporter-datadog` Datadog exporter should not use `unknown_service` as fallback resource service name. ([#570](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/570)) - Add support for the async extension of SQLAlchemy (>= 1.4) ([#568](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/568)) ### Added - `opentelemetry-instrumentation-httpx` Add `httpx` instrumentation ([#461](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/461)) ## Version 1.3.0/0.22b0 (2021-06-01) ### Changed - `opentelemetry-bootstrap` not longer forcibly removes and re-installs libraries and their instrumentations. This means running bootstrap will not auto-upgrade existing dependencies and as a result not cause dependency conflicts. ([#514](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/514)) - `opentelemetry-instrumentation-asgi` Set the response status code on the server span ([#478](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/478)) - `opentelemetry-instrumentation-tornado` Fixed cases where description was used with non- error status code when creating Status objects. ([#504](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/504)) - `opentelemetry-instrumentation-asgi` Fix instrumentation default span name. ([#418](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/418)) - Propagators use the root context as default for `extract` and do not modify the context if extracting from carrier does not work. ([#488](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/488)) ### Added - `opentelemetry-instrumentation-botocore` now supports context propagation for lambda invoke via Payload embedded headers. ([#458](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/458)) - Added support for CreateKey functionality. ([#502](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/502)) ## Version 1.2.0/0.21b0 (2021-05-11) ### Changed - Instrumentation packages don't specify the libraries they instrument as dependencies anymore. Instead, they verify the correct version of libraries are installed at runtime. ([#475](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/475)) - `opentelemetry-propagator-ot-trace` Use `TraceFlags` object in `extract` ([#472](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/472)) - Set the `traced_request_attrs` of FalconInstrumentor by an argument correctly. ([#473](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/473)) - Enable passing explicit urls to exclude in instrumentation in FastAPI ([#486](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/486)) - Distros can now implement `load_instrumentor(EntryPoint)` method to customize instrumentor loading behaviour. ([#480](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/480)) - Fix entrypoint for ottrace propagator ([#492](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/492)) ### Added - Move `opentelemetry-instrumentation` from core repository ([#465](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/465)) ## Version 0.20b0 (2021-04-20) ### Changed - Restrict DataDog exporter's `ddtrace` dependency to known working versions. ([#400](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/400)) - GRPC instrumentation now correctly injects trace context into outgoing requests. ([#392](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/39)) - Publish `opentelemetry-propagator-ot-trace` package as a part of the release process ([#387](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/387)) - Update redis instrumentation to follow semantic conventions ([#403](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/403)) - Update instrumentations to use tracer_provider for creating tracer if given, otherwise use global tracer provider ([#402](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/402)) - `opentelemetry-instrumentation-wsgi` Replaced `name_callback` with `request_hook` and `response_hook` callbacks. ([#424](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/424)) - Update gRPC instrumentation to better wrap server context ([#420](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/420)) - `opentelemetry-instrumentation-redis` Fix default port KeyError and Wrong Attribute name (net.peer.ip -> net.peer.port) ([#265](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/265)) - `opentelemetry-instrumentation-asyncpg` Fix default port KeyError and Wrong Attribute name (net.peer.ip -> net.peer.port) ([#265](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/265)) ### Added - `opentelemetry-instrumentation-urllib3` Add urllib3 instrumentation ([#299](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/299)) - `opentelemetry-instrumentation-flask` Added `request_hook` and `response_hook` callbacks. ([#416](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/416)) - `opentelemetry-instrumenation-django` now supports request and response hooks. ([#407](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/407)) - `opentelemetry-instrumentation-falcon` FalconInstrumentor now supports request/response hooks. ([#415](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/415)) - `opentelemetry-instrumentation-tornado` Add request/response hooks. ([#426](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/426)) - `opentelemetry-exporter-datadog` Add parsing exception events for error tags. ([#459](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/459)) - `opentelemetry-instrumenation-django` now supports trace response headers. ([#436](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/436)) - `opentelemetry-instrumenation-tornado` now supports trace response headers. ([#436](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/436)) - `opentelemetry-instrumenation-pyramid` now supports trace response headers. ([#436](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/436)) - `opentelemetry-instrumenation-falcon` now supports trace response headers. ([#436](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/436)) - `opentelemetry-instrumenation-flask` now supports trace response headers. ([#436](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/436)) - `opentelemetry-instrumentation-grpc` Keep client interceptor in sync with grpc client interceptors. ([#442](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/442)) ### Removed - Remove `http.status_text` from span attributes ([#406](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/406)) ## Version 0.19b0 (2021-03-26) - Implement context methods for `_InterceptorChannel` ([#363](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/363)) ### Changed - Rename `IdsGenerator` to `IdGenerator` ([#350](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/350)) - `opentelemetry-exporter-datadog` Fix warning when DatadogFormat encounters a request with no DD_ORIGIN headers ([#368](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/368)). - `opentelemetry-instrumentation-aiopg` Fix multiple nested spans when `aiopg.pool` is used ([#336](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/381)). - Updated instrumentations to use `opentelemetry.trace.use_span` instead of `Tracer.use_span()` ([#364](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/364)) - `opentelemetry-propagator-ot-trace` Do not throw an exception when headers are not present ([#378](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/378)) - `opentelemetry-instrumentation-wsgi` Reimplement `keys` method to return actual keys from the carrier instead of an empty list. ([#379](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/379)) - `opentelemetry-instrumentation-sqlalchemy` Fix multithreading issues in recording spans from SQLAlchemy ([#315](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/315)) - Make getters and setters optional ([#372](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/372)) ### Removed - Removing support for Python 3.5 ([#374](https://github.com/open-telemetry/opentelemetry-python/pull/374)) ## Version 0.18b0 (2021-02-16) ### Added - `opentelemetry-propagator-ot-trace` Add OT Trace Propagator ([#302](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/302)) - `opentelemetry-instrumentation-logging` Added logging instrumentation to enable log - trace correlation. ([#345](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/345)) ### Removed - Remove `component` span attribute in instrumentations. `opentelemetry-instrumentation-aiopg`, `opentelemetry-instrumentation-dbapi` Remove unused `database_type` parameter from `trace_integration` function. ([#301](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/301)) - `opentelemetry-instrumentation-asgi` Return header values using case insensitive keys ([#308](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/308)) - Remove metrics from all instrumentations ([#312](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/312)) - `opentelemetry-instrumentation-boto` updated to set span attributes instead of overriding the resource. ([#310](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/310)) - `opentelemetry-instrumentation-grpc` Fix issue tracking child spans in streaming responses ([#260](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/260)) - `opentelemetry-instrumentation-grpc` Updated client attributes, added tests, fixed examples, docs ([#269](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/269)) ## Version 0.17b0 (2021-01-20) ### Added - `opentelemetry-instrumentation-sqlalchemy` Ensure spans have kind set to "CLIENT" ([#278](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/278)) - `opentelemetry-instrumentation-celery` Add support for Celery version 5.x ([#266](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/266)) - `opentelemetry-instrumentation-urllib` Add urllib instrumentation ([#222](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/222)) - `opentelemetry-exporter-datadog` Add fields method ([#226](https://github.com/open-telemetry/opentelemetry-python/pull/226)) - `opentelemetry-sdk-extension-aws` Add method to return fields injected by propagator ([#226](https://github.com/open-telemetry/opentelemetry-python/pull/226)) - `opentelemetry-exporter-prometheus-remote-write` Prometheus Remote Write Exporter Setup ([#180](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/180)) - `opentelemetry-exporter-prometheus-remote-write` Add Exporter constructor validation methods in Prometheus Remote Write Exporter ([#206](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/206)) - `opentelemetry-exporter-prometheus-remote-write` Add conversion to TimeSeries methods in Prometheus Remote Write Exporter ([#207](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/207)) - `opentelemetry-exporter-prometheus-remote-write` Add request methods to Prometheus Remote Write Exporter ([#212](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/212)) - `opentelemetry-instrumentation-fastapi` Added support for excluding some routes with env var `OTEL_PYTHON_FASTAPI_EXCLUDED_URLS` ([#237](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/237)) - `opentelemetry-instrumentation-starlette` Added support for excluding some routes with env var `OTEL_PYTHON_STARLETTE_EXCLUDED_URLS` ([#237](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/237)) - Add Prometheus Remote Write Exporter integration tests in opentelemetry-docker-tests ([#216](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/216)) - `opentelemetry-instrumentation-grpc` Add tests for grpc span attributes, grpc `abort()` conditions ([#236](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/236)) - Add README and example app for Prometheus Remote Write Exporter ([#227](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/227)) - `opentelemetry-instrumentation-botocore` Adds a field to report the number of retries it take to complete an API call ([#275](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/275)) - `opentelemetry-instrumentation-requests` Use instanceof to check if responses are valid Response objects ([#273](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/273)) ### Changed - Fix broken links to project ([#413](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/413)) - `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-wsgi` Return `None` for `CarrierGetter` if key not found ([#233](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/233)) - `opentelemetry-instrumentation-grpc` Comply with updated spec, rework tests ([#236](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/236)) - `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-falcon`, `opentelemetry-instrumentation-flask`, `opentelemetry-instrumentation-pyramid`, `opentelemetry-instrumentation-wsgi` Renamed `host.port` attribute to `net.host.port` ([#242](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/242)) - `opentelemetry-instrumentation-flask` Do not emit a warning message for request contexts created with `app.test_request_context` ([#253](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/253)) - `opentelemetry-instrumentation-requests`, `opentelemetry-instrumentation-urllib` Fix span name callback parameters ([#259](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/259)) - `opentelemetry-exporter-datadog` Fix unintentional type change of span trace flags ([#261](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/261)) - `opentelemetry-instrumentation-aiopg` Fix AttributeError `__aexit__` when `aiopg.connect` and `aio[g].create_pool` used with async context manager ([#235](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/235)) - `opentelemetry-exporter-datadog` `opentelemetry-sdk-extension-aws` Fix reference to ids_generator in sdk ([#283](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/283)) - `opentelemetry-instrumentation-sqlalchemy` Use SQL operation and DB name as span name. ([#254](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/254)) - `opentelemetry-instrumentation-dbapi`, `TracedCursor` replaced by `CursorTracer` ([#246](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/246)) - `opentelemetry-instrumentation-psycopg2`, Added support for psycopg2 registered types. ([#246](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/246)) - `opentelemetry-instrumentation-dbapi`, `opentelemetry-instrumentation-psycopg2`, `opentelemetry-instrumentation-mysql`, `opentelemetry-instrumentation-pymysql`, `opentelemetry-instrumentation-aiopg` Use SQL command name as the span operation name instead of the entire query. ([#246](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/246)) - Update TraceState to adhere to specs ([#276](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/276)) ### Removed - Remove Configuration ([#285](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/285)) ## Version 0.16b1 (2020-11-26) ## Version 0.16b0 (2020-11-25) ### Added - `opentelemetry-instrumentation-flask` Add span name callback ([#152](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/152)) - `opentelemetry-sdk-extension-aws` Add AWS X-Ray Ids Generator Entry Point ([#201](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/201)) - `opentelemetry-sdk-extension-aws` Fix typo for installing OTel SDK in docs ([#200](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/200)) - `opentelemetry-sdk-extension-aws` Import missing components for docs ([#198](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/198)) - `opentelemetry-sdk-extension-aws` Provide components needed to Configure OTel SDK for Tracing with AWS X-Ray ([#130](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/130)) - `opentelemetry-instrumentation-sklearn` Initial release ([#151](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/151)) - `opentelemetry-instrumentation-requests` Add span name callback ([#158](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/158)) - `opentelemetry-instrumentation-botocore` Add propagator injection for botocore calls ([#181](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/181)) ### Changed - `opentelemetry-instrumentation-pymemcache` Update pymemcache instrumentation to follow semantic conventions ([#183](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/183)) - `opentelemetry-instrumentation-redis` Update redis instrumentation to follow semantic conventions ([#184](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/184)) - `opentelemetry-instrumentation-pymongo` Update pymongo instrumentation to follow semantic conventions ([#203](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/203)) - `opentelemetry-instrumentation-sqlalchemy` Update sqlalchemy instrumentation to follow semantic conventions ([#202](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/202)) - `opentelemetry-instrumentation-botocore` Make botocore instrumentation check if instrumentation has been suppressed ([#182](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/182)) - `opentelemetry-instrumentation-botocore` Botocore SpanKind as CLIENT and modify existing traced attributes ([#150](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/150)) - `opentelemetry-instrumentation-dbapi` Update dbapi and its dependent instrumentations to follow semantic conventions ([#195](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/195)) - `opentelemetry-instrumentation-dbapi` Stop capturing query parameters by default ([#156](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/156)) - `opentelemetry-instrumentation-asyncpg` Update asyncpg instrumentation to follow semantic conventions ([#188](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/188)) - `opentelemetry-instrumentation-grpc` Update protobuf versions ([#1356](https://github.com/open-telemetry/opentelemetry-python/pull/1356)) ## Version 0.15b0 (2020-11-02) ### Added - `opentelemetry-instrumentation-requests` Add support for tracking http metrics ([#1230](https://github.com/open-telemetry/opentelemetry-python/pull/1230)) - `opentelemetry-instrumentation-django` Added capture of http.route ([#1226](https://github.com/open-telemetry/opentelemetry-python/issues/1226)) - `opentelemetry-instrumentation-django` Add support for tracking http metrics ([#1230](https://github.com/open-telemetry/opentelemetry-python/pull/1230)) ### Changed - `opentelemetry-exporter-datadog` Make `SpanProcessor.on_start` accept parent Context ([#1251](https://github.com/open-telemetry/opentelemetry-python/pull/1251)) - `opentelemetry-instrumentation-flask` Use `url.rule` instead of `request.endpoint` for span name ([#1260](https://github.com/open-telemetry/opentelemetry-python/pull/1260)) - `opentelemetry-instrumentation-django` Django instrumentation is now enabled by default but can be disabled by setting `OTEL_PYTHON_DJANGO_INSTRUMENT` to `False` ([#1239](https://github.com/open-telemetry/opentelemetry-python/pull/1239)) - `opentelemetry-instrumentation-django` Bugfix use request.path replace request.get_full_path(). It will get correct span name ([#1309](https://github.com/open-telemetry/opentelemetry-python/pull/1309#)) - `opentelemetry-instrumentation-django` Record span status and http.status_code attribute on exception ([#1257](https://github.com/open-telemetry/opentelemetry-python/pull/1257)) - `opentelemetry-instrumentation-grpc` Rewrite gRPC server interceptor ([#1171](https://github.com/open-telemetry/opentelemetry-python/pull/1171)) ## Version 0.14b0 (2020-10-13) ### Added - `opentelemetry-exporter-datadog` Add support for span resource labels and service name - `opentelemetry-instrumentation-celery` Span operation names now include the task type. ([#1135](https://github.com/open-telemetry/opentelemetry-python/pull/1135)) - `opentelemetry-instrumentation-celery` Added automatic context propagation. ([#1135](https://github.com/open-telemetry/opentelemetry-python/pull/1135)) - `opentelemetry-instrumentation-falcon` Added support for `OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS` ([#1158](https://github.com/open-telemetry/opentelemetry-python/pull/1158)) - `opentelemetry-instrumentation-tornado` Added support for `OTEL_PYTHON_TORNADO_TRACED_REQUEST_ATTRS` ([#1178](https://github.com/open-telemetry/opentelemetry-python/pull/1178)) - `opentelemetry-instrumentation-django` Added support for `OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS` ([#1154](https://github.com/open-telemetry/opentelemetry-python/pull/1154)) ### Changed - `opentelemetry-instrumentation-pymongo` Cast PyMongo commands as strings ([#1132](https://github.com/open-telemetry/opentelemetry-python/pull/1132)) - `opentelemetry-instrumentation-system-metrics` Fix issue when specific metrics are not available in certain OS ([#1207](https://github.com/open-telemetry/opentelemetry-python/pull/1207)) - `opentelemetry-instrumentation-pymysql` Bumped version from 0.9.3 to 0.10.1 ([#1228](https://github.com/open-telemetry/opentelemetry-python/pull/1228)) - `opentelemetry-instrumentation-django` Changed span name extraction from request to comply semantic convention ([#992](https://github.com/open-telemetry/opentelemetry-python/pull/992)) ## Version 0.13b0 (2020-09-17) ### Added - `opentelemetry-instrumentation-falcon` Initial release. Added instrumentation for Falcon 2.0+ - `opentelemetry-instrumentation-tornado` Initial release. Supports Tornado 6.x on Python 3.5 and newer. - `opentelemetry-instrumentation-aiohttp-client` Add instrumentor and auto instrumentation support for aiohttp ([#1075](https://github.com/open-telemetry/opentelemetry-python/pull/1075)) - `opentelemetry-instrumentation-requests` Add support for instrumenting prepared requests ([#1040](https://github.com/open-telemetry/opentelemetry-python/pull/1040)) - `opentelemetry-instrumentation-requests` Add support for http metrics ([#1116](https://github.com/open-telemetry/opentelemetry-python/pull/1116)) ### Changed - `opentelemetry-instrumentation-aiohttp-client` Updating span name to match semantic conventions ([#972](https://github.com/open-telemetry/opentelemetry-python/pull/972)) - `opentelemetry-instrumentation-dbapi` cursors and connections now produce spans when used with context managers ([#1028](https://github.com/open-telemetry/opentelemetry-python/pull/1028)) ### Removed - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) ## Version 0.12b0 (2020-08-14) ### Changed - `opentelemetry-ext-pymemcache` Change package name to opentelemetry-instrumentation-pymemcache ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - `opentelemetry-ext-redis` Update default SpanKind to `SpanKind.CLIENT` ([#965](https://github.com/open-telemetry/opentelemetry-python/pull/965)) - `opentelemetry-ext-redis` Change package name to opentelemetry-instrumentation-redis ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - `opentelemetry-ext-datadog` Change package name to opentelemetry-exporter-datadog ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) - `opentelemetry-ext-jinja2` Change package name to opentelemetry-instrumentation-jinja2 ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) - `opentelemetry-ext-elasticsearch` Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) - `opentelemetry-ext-elasticsearch` Change package name to opentelemetry-instrumentation-elasticsearch ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) - `opentelemetry-ext-celery` Change package name to opentelemetry-instrumentation-celery ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) - `opentelemetry-ext-pyramid` Change package name to opentelemetry-instrumentation-pyramid ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - `opentelemetry-ext-pyramid` Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) - `opentelemetry-ext-pymongo` Change package name to opentelemetry-instrumentation-pymongo ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - `opentelemetry-ext-sqlite3` Change package name to opentelemetry-instrumentation-sqlite3 ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - `opentelemetry-ext-sqlalchemy` Change package name to opentelemetry-instrumentation-sqlalchemy ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - `opentelemetry-ext-psycopg2` Change package name to opentelemetry-instrumentation-psycopg2 ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - `opentelemetry-ext-aiohttp-client` Change package name to opentelemetry-instrumentation-aiohttp-client ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) - `opentelemetry-ext-boto` Change package name to opentelemetry-instrumentation-boto ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) - `opentelemetry-ext-system-metrics` Change package name to opentelemetry-instrumentation-system-metrics ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) - `opentelemetry-ext-asgi` Change package name to opentelemetry-instrumentation-asgi ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) - `opentelemetry-ext-wsgi` Change package name to opentelemetry-instrumentation-wsgi ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) - `opentelemetry-ext-pymysql` Change package name to opentelemetry-instrumentation-pymysql ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - `opentelemetry-ext-requests` Change package name to opentelemetry-instrumentation-requests ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) - `opentelemetry-ext-requests` Span name reported updated to follow semantic conventions to reduce cardinality ([#972](https://github.com/open-telemetry/opentelemetry-python/pull/972)) - `opentelemetry-ext-botocore` Change package name to opentelemetry-instrumentation-botocore ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) - `opentelemetry-ext-dbapi` Change package name to opentelemetry-instrumentation-dbapi ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - `opentelemetry-ext-flask` Change package name to opentelemetry-instrumentation-flask ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) - `opentelemetry-ext-flask` Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) - `opentelemetry-ext-django` Change package name to opentelemetry-instrumentation-django ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) - `opentelemetry-ext-django` Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) - `opentelemetry-ext-asyncpg` Change package name to opentelemetry-instrumentation-asyncpg ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - `opentelemetry-ext-mysql` Change package name to opentelemetry-instrumentation-mysql ([#966](https://github.com/open-telemetry/opentelemetry-python/pull/966)) - `opentelemetry-ext-grpc` Change package name to opentelemetry-instrumentation-grpc ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) ## Version 0.11b0 (2020-07-28) ### Added - `opentelemetry-instrumentation-aiopg` Initial release - `opentelemetry-instrumentation-fastapi` Initial release ([#890](https://github.com/open-telemetry/opentelemetry-python/pull/890)) - `opentelemetry-ext-grpc` Add status code to gRPC client spans ([896](https://github.com/open-telemetry/opentelemetry-python/pull/896)) - `opentelemetry-ext-grpc` Add gRPC client and server instrumentors ([788](https://github.com/open-telemetry/opentelemetry-python/pull/788)) - `opentelemetry-ext-grpc` Add metric recording (bytes in/out, errors, latency) to gRPC client ### Changed - `opentelemetry-ext-pyramid` Use one general exclude list instead of two ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/872)) - `opentelemetry-ext-boto` fails to export spans via jaeger ([#866](https://github.com/open-telemetry/opentelemetry-python/pull/866)) - `opentelemetry-ext-botocore` fails to export spans via jaeger ([#866](https://github.com/open-telemetry/opentelemetry-python/pull/866)) - `opentelemetry-ext-wsgi` Set span status on wsgi errors ([#864](https://github.com/open-telemetry/opentelemetry-python/pull/864)) - `opentelemetry-ext-flask` Use one general exclude list instead of two ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/872)) - `opentelemetry-ext-django` Use one general exclude list instead of two ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/872)) - `opentelemetry-ext-asyncpg` Shouldn't capture query parameters by default ([#854](https://github.com/open-telemetry/opentelemetry-python/pull/854)) - `opentelemetry-ext-mysql` bugfix: Fix auto-instrumentation entry point for mysql ([#858](https://github.com/open-telemetry/opentelemetry-python/pull/858)) ## Version 0.10b0 (2020-06-23) ### Added - `opentelemetry-ext-pymemcache` Initial release - `opentelemetry-ext-elasticsearch` Initial release - `opentelemetry-ext-celery` Add instrumentation for Celery ([#780](https://github.com/open-telemetry/opentelemetry-python/pull/780)) - `opentelemetry-instrumentation-starlette` Initial release ([#777](https://github.com/open-telemetry/opentelemetry-python/pull/777)) - `opentelemetry-ext-asyncpg` Initial Release ([#814](https://github.com/open-telemetry/opentelemetry-python/pull/814)) ## Version 0.9b0 (2020-06-10) ### Added - `opentelemetry-ext-pyramid` Initial release - `opentelemetry-ext-boto` Initial release - `opentelemetry-ext-botocore` Initial release - `opentelemetry-ext-system-metrics` Initial release (https://github.com/open-telemetry/opentelemetry-python/pull/652) ## Version 0.8b0 (2020-05-27) ### Added - `opentelemetry-ext-datadog` Add exporter to Datadog ([#572](https://github.com/open-telemetry/opentelemetry-python/pull/572)) - `opentelemetry-ext-sqlite3` Initial release - `opentelemetry-ext-psycopg2` Implement instrumentor interface, enabling auto-instrumentation ([#694](https://github.com/open-telemetry/opentelemetry-python/pull/694)) - `opentelemetry-ext-asgi` Add ASGI middleware ([#716](https://github.com/open-telemetry/opentelemetry-python/pull/716)) - `opentelemetry-ext-django` Add exclude list for paths and hosts to prevent from tracing ([#670](https://github.com/open-telemetry/opentelemetry-python/pull/670)) - `opentelemetry-ext-django` Add support for django >= 1.10 (#717) ### Changed - `opentelemetry-ext-grpc` lint: version of grpc causes lint issues ([#696](https://github.com/open-telemetry/opentelemetry-python/pull/696)) ## Version 0.7b1 (2020-05-12) ### Added - `opentelemetry-ext-redis` Initial release - `opentelemetry-ext-jinja2` Add jinja2 instrumentation ([#643](https://github.com/open-telemetry/opentelemetry-python/pull/643)) - `opentelemetry-ext-pymongo` Implement instrumentor interface ([#612](https://github.com/open-telemetry/opentelemetry-python/pull/612)) - `opentelemetry-ext-sqlalchemy` Initial release - `opentelemetry-ext-aiohttp-client` Initial release - `opentelemetry-ext-pymysql` Initial release - `opentelemetry-ext-http-requests` Implement instrumentor interface, enabling auto-instrumentation ([#597](https://github.com/open-telemetry/opentelemetry-python/pull/597)) - `opentelemetry-ext-http-requests` Adding disable_session for more granular instrumentation control ([#573](https://github.com/open-telemetry/opentelemetry-python/pull/573)) - `opentelemetry-ext-http-requests` Add a callback for custom attributes ([#656](https://github.com/open-telemetry/opentelemetry-python/pull/656)) - `opentelemetry-ext-dbapi` Implement instrument_connection and uninstrument_connection ([#624](https://github.com/open-telemetry/opentelemetry-python/pull/624)) - `opentelemetry-ext-flask` Add exclude list for paths and hosts ([#630](https://github.com/open-telemetry/opentelemetry-python/pull/630)) - `opentelemetry-ext-django` Initial release - `opentelemetry-ext-mysql` Implement instrumentor interface ([#654](https://github.com/open-telemetry/opentelemetry-python/pull/654)) ### Changed - `opentelemetry-ext-http-requests` Rename package to opentelemetry-ext-requests ([#619](https://github.com/open-telemetry/opentelemetry-python/pull/619)) ## Version 0.6b0 (2020-03-30) ### Added - `opentelemetry-ext-flask` Add an entry_point to be usable in auto-instrumentation ([#327](https://github.com/open-telemetry/opentelemetry-python/pull/327)) - `opentelemetry-ext-grpc` Add gRPC integration ([#476](https://github.com/open-telemetry/opentelemetry-python/pull/476)) ## Version 0.5b0 (2020-03-16) ## Version 0.4a0 (2020-02-21) ### Added - `opentelemetry-ext-psycopg2` Initial release - `opentelemetry-ext-dbapi` Initial release - `opentelemetry-ext-mysql` Initial release ### Changed - `opentelemetry-ext-pymongo` Updating network connection attribute names ([#350](https://github.com/open-telemetry/opentelemetry-python/pull/350)) - `opentelemetry-ext-wsgi` Updating network connection attribute names ([#350](https://github.com/open-telemetry/opentelemetry-python/pull/350)) - `opentelemetry-ext-flask` Use string keys for WSGI environ values ([#366](https://github.com/open-telemetry/opentelemetry-python/pull/366)) ## Version 0.3a0 (2019-12-11) ### Added - `opentelemetry-ext-flask` Initial release - `opentelemetry-ext-pymongo` Initial release ### Changed - `opentelemetry-ext-wsgi` Support new semantic conventions ([#299](https://github.com/open-telemetry/opentelemetry-python/pull/299)) - `opentelemetry-ext-wsgi` Updates for core library changes ## Version 0.2a0 (2019-10-29) ### Changed - `opentelemetry-ext-wsgi` Updates for core library changes - `opentelemetry-ext-http-requests` Updates for core library changes - `Added support for PyPy3` Initial release ([#1033](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1033)) ## Version 0.1a0 (2019-09-30) ### Added - `opentelemetry-ext-wsgi` Initial release - `opentelemetry-ext-http-requests` Initial release - Drop support for 3.7 ([#2151](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2151)) - `opentelemetry-resource-detector-azure` Added 10s timeout to VM Resource Detector ([#2119](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2119)) - `opentelemetry-instrumentation-asyncpg` Allow AsyncPGInstrumentor to be instantiated multiple times ([#1791](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1791)) - `opentelemetry-instrumentation-confluent-kafka` Add support for higher versions until 2.3.0 of confluent_kafka ([#2132](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2132)) - `opentelemetry-resource-detector-azure` Changed timeout to 4 seconds due to [timeout bug](https://github.com/open-telemetry/opentelemetry-python/issues/3644) ([#2136](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2136)) - `opentelemetry-resource-detector-azure` Suppress instrumentation for `urllib` call ([#2178](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2178)) - AwsLambdaInstrumentor handles and re-raises function exception ([#2245](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2245)) opentelemetry-python-contrib-0.66b2/CLAUDE.md000066400000000000000000000000141527070072300210170ustar00rootroot00000000000000@AGENTS.md opentelemetry-python-contrib-0.66b2/CONTRIBUTING.md000066400000000000000000000730411527070072300220030ustar00rootroot00000000000000# Contributing to opentelemetry-python-contrib The Python special interest group (SIG) meets regularly. See the OpenTelemetry [community](https://github.com/open-telemetry/community#python-sdk) repo for information on this and other language SIGs. See the [public meeting notes](https://docs.google.com/document/d/18w8zOBm_mbety0OqlPwxc7dvnfu641EgmrO4AdJef0U/edit?tab=t.0) for a summary description of past meetings. To request edit access, join the meeting or get in touch on [Slack](https://cloud-native.slack.com/archives/C01PD4HUVBL). See the [community membership document](https://github.com/open-telemetry/community/blob/main/community-membership.md) on how to become a [**Member**](https://github.com/open-telemetry/community/blob/main/community-membership.md#member), [**Approver**](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver) and [**Maintainer**](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer). Before you can contribute, you will need to sign the [Contributor License Agreement](https://docs.linuxfoundation.org/lfx/easycla/contributors). Please also read the [OpenTelemetry Contributor Guide](https://github.com/open-telemetry/community/blob/main/guides/contributor/README.md). If you are using AI agents to assist with contributions, please read [AGENTS.md](AGENTS.md) for guidance on how to use them responsibly in this project. ## Index - [Contributing to opentelemetry-python-contrib](#contributing-to-opentelemetry-python-contrib) - [Index](#index) - [Find a Buddy and get Started Quickly](#find-a-buddy-and-get-started-quickly) - [Development](#development) - [Virtual Environment](#virtual-environment) - [Troubleshooting](#troubleshooting) - [Benchmarks](#benchmarks) - [Pull Requests](#pull-requests) - [How to Send Pull Requests](#how-to-send-pull-requests) - [How to Receive Comments](#how-to-receive-comments) - [How to Get PRs Reviewed](#how-to-get-prs-reviewed) - [How to Get PRs Merged](#how-to-get-prs-merged) - [Changelog](#changelog) - [Stale PRs](#stale-prs) - [Design Choices](#design-choices) - [Focus on Capabilities, Not Structure Compliance](#focus-on-capabilities-not-structure-compliance) - [Running Tests Locally](#running-tests-locally) - [Testing against a different Core repo branch/commit](#testing-against-a-different-core-repo-branchcommit) - [Style Guide](#style-guide) - [Guideline for instrumentations](#guideline-for-instrumentations) - [Update supported instrumentation package versions](#update-supported-instrumentation-package-versions) - [Guideline for GenAI instrumentations](#guideline-for-genai-instrumentations) - [Get Involved](#get-involved) - [Expectations from contributors](#expectations-from-contributors) - [Use of AI coding assistants](#use-of-ai-coding-assistants) - [Guidelines for native OpenTelemetry instrumentation](#guidelines-for-native-opentelemetry-instrumentation) - [Updating supported Python versions](#updating-supported-python-versions) - [Bumping the Python baseline](#bumping-the-python-baseline) - [Adding support for a new Python release](#adding-support-for-a-new-python-release) ## Find a Buddy and get Started Quickly If you are looking for someone to help you find a starting point and be a resource for your first contribution, join our Slack and find a buddy! 1. Join [Slack](https://slack.cncf.io/) and join our [chat room](https://cloud-native.slack.com/archives/C01PD4HUVBL). 2. Post in the room with an introduction to yourself, what area you are interested in (check issues marked "Help Wanted"), and say you are looking for a buddy. We will match you with someone who has experience in that area. Your OpenTelemetry buddy is your resource to talk to directly on all aspects of contributing to OpenTelemetry: providing context, reviewing PRs, and helping those get merged. Buddies will not be available 24/7, but is committed to responding during their normal contribution hours. ## Development This project uses [tox](https://tox.readthedocs.io) to automate some aspects of development, including testing against multiple Python versions. To install `tox`, run: ```sh pip install tox ``` You can also run tox with `uv` support. By default [tox.ini](./tox.ini) will automatically create a provisioned tox environment with `tox-uv`, but you can install it at host level: ```sh pip install tox-uv ``` You can run `tox` with the following arguments: * `tox` to run all existing tox commands, including unit tests for all packages under multiple Python versions * `tox -e docs` to regenerate all docs * `tox -e py314-test-instrumentation-aiopg` to e.g. run the aiopg instrumentation unit tests under a specific Python version * `tox -e spellcheck` to run a spellcheck on all the code * `tox -e lint-some-package` to run lint checks on `some-package` * `tox -e generate-workflows` to run creation of new CI workflows if tox environments have been updated * `tox -e ruff` to run ruff linter and formatter checks against the entire codebase * `tox -e precommit` to run all `pre-commit` actions `ruff check` and `ruff format` are executed when `tox -e ruff` is run. We strongly recommend you to configure [pre-commit](https://pre-commit.com/) locally to run `ruff` and `rstcheck` automatically before each commit by installing it as git hooks. You just need to [install pre-commit](https://pre-commit.com/#install) in your environment: ```console pip install pre-commit -c dev-requirements.txt ``` and run this command inside the git repository: ```console pre-commit install ``` See [`tox.ini`](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/tox.ini) for more detail on available tox commands. ### Virtual Environment You can also create a single virtual environment to make it easier to run local tests. For that, you'll need to install [`uv`](https://docs.astral.sh/uv/getting-started/installation/). After installing `uv`, you can run the following command: ```sh uv sync ``` This will create a virtual environment in the `.venv` directory and install all the necessary dependencies. ### Troubleshooting Some packages may require additional system-wide dependencies to be installed. For example, you may need to install `libpq-dev` to run the postgresql client libraries instrumentation tests or `libsnappy-dev` to run the prometheus exporter tests. If you encounter a build error, please check the installation instructions for the package you are trying to run tests for. For `docs` building, you may need to install `mysql-client` and other required dependencies as necessary. Ensure the Python version used in your local setup matches the version used in the [CI](./.github/workflows/) to maintain compatibility when building the documentation. If you are using `tox-uv` for tests and have issues with resolving OpenTelemetry dependencies try: ```sh uv sync --refresh ``` ### Benchmarks Some packages have benchmark tests. To run them, run `tox -f benchmark`. Benchmark tests use `pytest-benchmark` and they output a table with results to the console. To write benchmarks, simply use the [pytest benchmark fixture](https://pytest-benchmark.readthedocs.io/en/latest/usage.html#usage) like the following: ```python def test_simple_start_span(benchmark): def benchmark_start_as_current_span(span_name, attribute_num): span = tracer.start_span( span_name, attributes={"count": attribute_num}, ) span.end() benchmark(benchmark_start_as_current_span, "benchmarkedSpan", 42) ``` Make sure the test file is under the `benchmarks/` folder of the package it is benchmarking and further has a path that corresponds to the file in the package it is testing. Make sure that the file name begins with `test_benchmark_`. (e.g. `propagator/opentelemetry-propagator-aws-xray/benchmarks/trace/propagation/test_benchmark_aws_xray_propagator.py`) ## Pull Requests ### How to Send Pull Requests Everyone is welcome to contribute code to `opentelemetry-python-contrib` via GitHub pull requests (PRs). To create a new PR, fork the project in GitHub and clone the upstream repo: ```sh git clone https://github.com/open-telemetry/opentelemetry-python-contrib.git cd opentelemetry-python-contrib ``` Add your fork as an origin: ```sh git remote add fork https://github.com/YOUR_GITHUB_USERNAME/opentelemetry-python-contrib.git ``` make sure you have all supported versions of Python installed, install `tox` only for the first time: ```sh pip install tox tox-uv ``` Run tests in the root of the repository (this will run all tox environments and may take some time): ```sh tox ``` Check out a new branch, make modifications and push the branch to your fork: ```sh git checkout -b feature ``` After you edit the files, stage changes in the current directory: ```sh git add . ``` Then run the following to commit the changes: ```sh git commit git push fork feature ``` Open a pull request (PR) against the main `opentelemetry-python-contrib` repo. A descriptive PR title will help the community better triage and review your changes. Make sure to prefix with the name(s) of the package/subdirectory/domain that your PR updates. Following any of these examples will help: * "opentelemetry-instrumentation-dbapi: add client operation duration metrics" * "scripts/build.sh: ignore opamp packages" * "docs(dbapi): fix pyodbc connect method example " ### How to Receive Comments * If the PR is not ready for review, please put `[WIP]` in the title, tag it as `work-in-progress`, or mark it as [`draft`](https://github.blog/2019-02-14-introducing-draft-pull-requests/). * Make sure tests and lint are passing locally before requesting a review. * Make sure CLA is signed and CI is clear. ### How to Get PRs Reviewed The maintainers and approvers of this repository are not experts in every instrumentation there is here. In fact, each one of us knows enough about them to only review a few. Unfortunately, it can be hard to find enough experts in every instrumentation to quickly review every instrumentation PR. The instrumentation experts are listed in `.github/component_owners.yml` with their corresponding files or directories that they own. The owners listed there will be notified when PRs that modify their files are opened. If you are not getting reviews, please contact the respective owners directly. > [!TIP] > Even if you’re new here, your review counts —and it’s valuable to the project. Feel free to jump into any open PR: check the docs, run the tests, ask questions, or give a +1 when things look good. The OpenTelemetry-Python community is intentionally flexible: anyone can review PRs and help them get merged. Every comment moves the project forward, so don’t hesitate if you have expertise to review a PR. ### How to Get PRs Merged A PR is considered to be **ready to merge** when: * It has received two approvals from [Approvers](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver) / [Maintainers](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer) (at different companies). * Major feedbacks are resolved. * It has been open for review for at least one working day. This gives people reasonable time to review. * Trivial change (typo, cosmetic, doc, etc.) doesn't have to wait for one day. * Urgent fix can take exception as long as it has been actively communicated. * A changelog fragment is added (see [Changelog](#changelog) below), if there is any impact on behavior. e.g. doc entries are not required, but small bug entries are. Any Approver / Maintainer can merge the PR once it is **ready to merge**. ### Changelog This project uses [towncrier](https://towncrier.readthedocs.io/) to manage changelogs. Instead of editing `CHANGELOG.md` directly, each PR should include a changelog fragment file. **Where to add fragments:** - **Coordinated packages** (most instrumentations, exporters, propagators): add to the root `.changelog/` directory - **Independently released packages** (`opentelemetry-opamp-client`, `opentelemetry-propagator-aws-xray`, `opentelemetry-resource-detector-azure`, `opentelemetry-resourcedetector-gcp`, `opentelemetry-sdk-extension-aws`, `opentelemetry-instrumentation-valkey-py`): add to `/.changelog/` **Creating a changelog fragment:** Create a file named `.changelog/.` where `TYPE` is one of: `added`, `changed`, `deprecated`, `removed`, `fixed`. The file should contain a one-line description of the change. For example, `.changelog/1234.fixed`: ``` `opentelemetry-instrumentation-flask`: fix request hook not being called for websocket connections ``` **Writing a good changelog entry:** - Write in imperative tone, as if completing the phrase "This change will..." - Keep entries concise — ideally under 80 characters - Prefix with the affected package name (e.g. `` `opentelemetry-instrumentation-flask`: ... ``) - Don't include the PR number — towncrier adds it automatically **Preview the changelog:** ```console towncrier build --draft --version Unreleased ``` The CI will verify that a changelog fragment exists and that `CHANGELOG.md` files are not directly modified. If your change does not need a changelog entry, add the "Skip Changelog" label to the PR. ### Stale PRs PRs with no activity for 14 days will be automatically marked as stale and closed after a further 14 days of inactivity. To prevent a PR from being marked stale, ensure there is regular activity (commits, comments, reviews, etc). Project managers can also exempt a PR from this by applying one of the following labels: `hold`, `WIP`, `blocked-by-spec`, `do not merge`. ## Design Choices As with other OpenTelemetry clients, opentelemetry-python follows the [opentelemetry-specification](https://github.com/open-telemetry/opentelemetry-specification). It's especially valuable to read through the [library guidelines](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/library-guidelines.md). ### Focus on Capabilities, Not Structure Compliance OpenTelemetry is an evolving specification, one where the desires and use cases are clear, but the method to satisfy those uses cases are not. As such, contributions should provide functionality and behavior that conforms to the specification, but the interface and structure is flexible. It is preferable to have contributions follow the idioms of the language rather than conform to specific API names or argument patterns in the spec. For a deeper discussion, see: https://github.com/open-telemetry/opentelemetry-specification/issues/165 ## Running Tests Locally 1. Go to your Contrib repo directory. `git clone git@github.com:open-telemetry/opentelemetry-python-contrib.git && cd opentelemetry-python-contrib`. 2. Make sure you have `tox` installed. `pip install tox`. 3. Run `tox` without any arguments to run tests for all the packages. Read more about [tox](https://tox.readthedocs.io/en/latest/). Some tests can be slow due to pre-steps that do dependencies installs. To help with that, you can run tox a first time, and after that run the tests using previous installed dependencies in toxdir as following: 1. First time run (e.g., opentelemetry-instrumentation-aiopg) ```console tox -e py314-test-instrumentation-aiopg ``` 2. Run tests again without pre-steps: ```console .tox/py314-test-instrumentation-aiopg/bin/pytest instrumentation/opentelemetry-instrumentation-aiopg ``` ### Testing against a different Core repo branch/commit Some of the tox targets install packages from the [OpenTelemetry Python Core Repository](https://github.com/open-telemetry/opentelemetry-python) via pip. The version of the packages installed defaults to the main branch in that repository when tox is run locally. It is possible to install packages tagged with a specific git commit hash by setting an environment variable before running tox as per the following example: ```sh CORE_REPO_SHA=c49ad57bfe35cfc69bfa863d74058ca9bec55fc3 tox ``` The continuous integration overrides that environment variable with as per the configuration [here](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/.github/workflows/test.yml#L17). ## Style Guide * All Python files must include the following SPDX license header as the first two lines (or immediately after a shebang line): ```python # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 ``` This is enforced by CI via `tox -e lint-license-header-check`. * docstrings should adhere to the [Google Python Style Guide](http://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) as specified with the [napoleon extension](http://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#google-vs-numpy) extension in [Sphinx](http://www.sphinx-doc.org/en/master/index.html). ## Guideline for instrumentations Below is a checklist of things to be mindful of when implementing a new instrumentation or working on a specific instrumentation. It is one of our goals as a community to keep the implementation specific details of instrumentations as similar across the board as possible for ease of testing and feature parity. It is also good to abstract as much common functionality as possible. - Find or create a new [Issue](https://github.com/open-telemetry/opentelemetry-python-contrib/issues) describing the tool or framework to instrument and its use cases to support with OpenTelemetry. - Be familiar with the [expectations from contributors](#expectations-from-contributors) that apply. - If you're a tool or framework maintainer, please consider using the OpenTelemetry API directly to support [native instrumentation](#guidelines-for-native-opentelemetry-instrumentation) instead of adding a new community instrumentation library. - Follow semantic conventions - The instrumentation should follow the semantic conventions defined [here](https://github.com/open-telemetry/semantic-conventions/tree/main/docs). - To ensure consistency, we encourage contributions that align with [STABLE](https://opentelemetry.io/docs/specs/otel/document-status/#lifecycle-status) semantic conventions if available. This approach helps us avoid potential confusion and reduces the need to support multiple outdated versions of semantic conventions. However, we are still open to considering exceptional cases where changes are well justified. - Contributions related to outdated HTTP semantic conventions (conventions prior to becoming [stable](https://github.com/open-telemetry/semantic-conventions/tree/v1.23.0)) will likely be discouraged, as they increase complexity and the potential for misconceptions. - Contains a name that is not already claimed in [Pypi](https://pypi.org/). Contact a maintainer, bring the issue up in the weekly Python SIG or create a ticket in Pypi if a desired name has already been taken. - Extends from [BaseInstrumentor](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/2518a4ac07cb62ad6587dd8f6cbb5f8663a7e179/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py#L35) - Supports auto-instrumentation - Add an entry point (ex. ) - Run `python scripts/generate_instrumentation_bootstrap.py` after adding a new instrumentation package. - Functionality that is common amongst other instrumentation and can be abstracted [here](https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/opentelemetry-instrumentation/src/opentelemetry/instrumentation) - Request/response [hooks](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/408) for http instrumentations - `suppress_instrumentation` functionality - ex. - Suppress propagation functionality - https://github.com/open-telemetry/opentelemetry-python-contrib/issues/344 for more context - `exclude_urls` functionality - ex. - `url_filter` functionality - ex. - `is_recording()` optimization on non-sampled spans - ex. - Appropriate error handling - ex. - Isolate sync and async test - For synchronous tests, the typical test case class is inherited from `opentelemetry.test.test_base.TestBase`. However, if you want to write asynchronous tests, the test case class should inherit also from `IsolatedAsyncioTestCase`. Adding asynchronous tests to a common test class can lead to tests passing without actually running, which can be misleading. - ex. - Most of the instrumentations have the same version. If you are going to develop a new instrumentation it would probably have `X.Y.dev` version and depends on `opentelemetry-instrumentation` and `opentelemetry-semantic-conventions` for a [compatible version](https://peps.python.org/pep-0440/#compatible-release). That means that you may need to install the instrumentation dependencies from this repo and the core repo from git. - Documentation - When adding a new instrumentation remember to add an entry in `docs/instrumentation/` named `/.rst` to have the instrumentation documentation referenced from the index. You can use the entry template available [here](./_template/autodoc_entry.rst) - Testing - When adding a new instrumentation remember to update `tox.ini` adding appropriate rules in `envlist`, `command_pre` and `commands` sections ### Update supported instrumentation package versions - Navigate to the **instrumentation package directory:** - Update **`pyproject.toml`** file by modifying `instruments` or `instruments-any` entry in the `[project.optional-dependencies]` section with the new version constraint - Update `_instruments` or `_instruments_any` variable in instrumentation **`package.py`** file with the new version constraint - At the **root of the project directory**, run `tox -e generate` to regenerate necessary files Please note that `instruments-any` is an optional field that can be used instead of or in addition to `instruments`. While `instruments` is a list of dependencies, _all_ of which are expected by the instrumentation, `instruments-any` is a list _any_ of which but not all are expected. For example, the following entry requires both `util` and `common` plus either `foo` or `bar` to be present for the instrumentation to occur: ``` [project.optional-dependencies] instruments = [ "util ~= 1.0" "common ~= 2.0" ] instruments-any = [ "foo ~= 3.0" "bar ~= 4.0" ] ``` If you're adding support for a new version of the instrumentation package, follow these additional steps: - At the **instrumentation package directory:** Add new test-requirements.txt file with the respective package version required for testing - At the **root of the project directory**: Add a new test environment entry for the package version in [tox.ini](./tox.ini) and run `tox -e generate-workflows` to regenerate new workflows accordingly. In the same [tox.ini](./tox.ini) file, search for `opentelemetry-instrumentation-{package}/test-requirements` and add a new line to point to the new test-requirements.txt you created in the previous step so tox can install the correct requirements. Example PRs: [#2976](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2976), [#2845](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2845) ## Guideline for GenAI instrumentations Instrumentations that relate to [Generative AI](https://opentelemetry.io/docs/specs/semconv/gen-ai/) systems should be housed in the dedicated repository [opentelemetry-python-genai](https://github.com/open-telemetry/opentelemetry-python-genai/). Please read [those guidelines](https://github.com/open-telemetry/opentelemetry-python-genai/blob/main/CONTRIBUTING.md). ## Get Involved * Reviewing PRs: If you would like to be tagged as reviewer in new PRs related to these instrumentations, please submit a PR to add your GitHub handle to [component_owners.yml](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/.github/component_owners.yml) under the corresponding instrumentation folder(s). * Approving PRs: If you would like to be able to approve PRs related to these instrumentations, you must join [opentelemetry-python-contrib-approvers](https://github.com/orgs/open-telemetry/teams/opentelemetry-python-contrib-approvers) team. Please ask one of the [Python contrib maintainers](https://github.com/orgs/open-telemetry/teams/opentelemetry-python-contrib-maintainers) to be accepted into the team. * Tracking and Creating Issues: Create or search for issues [here](https://github.com/open-telemetry/opentelemetry-python-contrib/issues). If you do not see an issue related to an instrumentation you would like to contribute to, please create a new tracking issue so the community is aware of its progress. ## Expectations from contributors OpenTelemetry is an open source community, and as such, greatly encourages contributions from anyone interested in the project. With that being said, there is a certain level of expectation from contributors even after a pull request is merged, specifically pertaining to instrumentations. The OpenTelemetry Python community expects contributors to maintain a level of support and interest in the instrumentations they contribute. This is to ensure that the instrumentation does not become stale and still functions the way the original contributor intended. Some instrumentations also pertain to libraries that the current members of the community are not so familiar with, so it is necessary to rely on the expertise of the original contributing parties. ### Use of AI coding assistants AI coding assistants can be valuable tools for contributors. They may help with understanding the codebase, drafting changes, writing tests and improving documentation. Contributors are welcome to use AI tools as a part of their contribution workflow. However, contributors remain responsible for the quality and correctness of their contributions. AI generated material must be carefully reviewed, validated and adapted by the contributor before being submitted to the project. Maintainers reserve the right to close pull requests or other contributions that appear to have been primarily generated by AI with little or no meaningful review from the contributor. This includes, but is not limited to: - Opening a large number of low quality pull requests in a short period of time. - Submitting changes that are incomplete, incorrect, unrelated to the issue or not aligned with project conventions. - Posting review replies, issue comments or other responses that appear to be AI generated and/or do not meaningfully address maintainer feedback. - Submitting pull requests that require maintainers to spend significant time correcting or explaining basic issues that should have been caught by the contributor before submission. This policy is intended to reduce unnecessary burden on maintainers and ensure that AI tools are used responsibly in support of high quality contributions. ### Guidelines for native OpenTelemetry instrumentation The preferred approach to supporting instrumentation of a tool or framework is native OpenTelemetry instrumentation. Compared to adding a new instrumentation library to this community repository, native instrumentation is better for: * continued support by framework experts long-term * access to more context for critical paths and correlations * granularity and performance To support native instrumentation, the tool or framework should use the [OpenTelemetry API](https://opentelemetry-python.readthedocs.io/en/latest/) directly to emit traces, metrics, and logs. ## Updating supported Python versions ### Bumping the Python baseline When updating the minimum supported Python version remember to: - Remove the version in `pyproject.toml` trove classifiers - Remove the version from `tox.ini` - Update github workflows accordingly with `tox -e generate-workflows` - Search for `sys.version_info` usage and remove code for unsupported versions - Bump `py-version` in `.pylintrc` for Python version dependent checks ### Adding support for a new Python release When adding support for a new Python release remember to: - Add the version in `tox.ini` - Add the version in `pyproject.toml` trove classifiers - Update github workflows accordingly with `tox -e generate-workflows`; lint and benchmarks use the latest supported version - Update `.pre-commit-config.yaml` - Update tox examples in the documentation opentelemetry-python-contrib-0.66b2/LICENSE000066400000000000000000000261351527070072300205610ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/LICENSE.Apache000066400000000000000000000261351527070072300217410ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/LICENSE.BSD3000066400000000000000000000027251527070072300212520ustar00rootroot00000000000000Copyright (c) 2016, Datadog All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Datadog nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DATADOG BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. opentelemetry-python-contrib-0.66b2/README.md000066400000000000000000000276301527070072300210340ustar00rootroot00000000000000---

Getting Started   •   API Documentation   •   Getting In Touch (GitHub Discussions)

GitHub release (latest by date including pre-releases) Codecov Status license
Build Status Beta

Contributing   •   Instrumentations

--- # OpenTelemetry Python Contrib The Python auto-instrumentation libraries for [OpenTelemetry](https://opentelemetry.io/) (per [OTEP 0001](https://github.com/open-telemetry/oteps/blob/main/text/0001-telemetry-without-manual-instrumentation.md)) ## Index * [Installation](#installation) * [Releasing](#releasing) * [Releasing a package as `1.0` stable](#releasing-a-package-as-10-stable) * [Semantic Convention status of instrumentations](#semantic-convention-status-of-instrumentations) * [Contributing](#contributing) * [Thanks to all the people who already contributed](#thanks-to-all-the-people-who-already-contributed) ## Installation This repository includes installable packages for each instrumented library. Libraries that produce telemetry data should only depend on `opentelemetry-api`, and defer the choice of the SDK to the application developer. Applications may depend on `opentelemetry-sdk` or another package that implements the API. **Please note** that these libraries are currently in _beta_, and shouldn't generally be used in production environments. Unless explicitly stated otherwise, any instrumentation here for a particular library is not developed or maintained by the authors of such library. The [`instrumentation/`](https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation) directory includes OpenTelemetry instrumentation packages, which can be installed separately as: ```sh pip install opentelemetry-instrumentation-{integration} ``` To install the development versions of these packages instead, clone or fork this repo and do an [editable install](https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs): ```sh pip install -e ./instrumentation/opentelemetry-instrumentation-{integration} ``` ## Releasing Maintainers release new versions of the packages in `opentelemetry-python-contrib` on a monthly cadence. See [releases](https://github.com/open-telemetry/opentelemetry-python-contrib/releases) for all previous releases. Contributions that enhance OTel for Python are welcome to be hosted upstream for the benefit of group collaboration. Maintainers will look for things like good documentation, good unit tests, and in general their own confidence when deciding to release a package with the stability guarantees that are implied with a `1.0` release. To resolve this, members of the community are encouraged to commit to becoming a CODEOWNER for packages in `-contrib` that they feel experienced enough to maintain. CODEOWNERS can then follow the checklist below to release `-contrib` packages as 1.0 stable: ### Releasing a package as `1.0` stable To release a package as `1.0` stable, the package: - SHOULD have a CODEOWNER. To become one, submit an issue and explain why you meet the responsibilities found in [CODEOWNERS](.github/CODEOWNERS). - MUST have unit tests that cover all supported versions of the instrumented library. - e.g. Instrumentation packages might use different techniques to instrument different major versions of python packages - MUST have clear documentation for non-obvious usages of the package - e.g. If an instrumentation package uses flags, a token as context, or parameters that are not typical of the `BaseInstrumentor` class, these are documented - After the release of `1.0`, a CODEOWNER may no longer feel like they have the bandwidth to meet the responsibilities of maintaining the package. That's not a problem at all, life happens! However, if that is the case, we ask that the CODEOWNER please raise an issue indicating that they would like to be removed as a CODEOWNER so that they don't get pinged on future PRs. Ultimately, we hope to use that issue to find a new CODEOWNER. ## Semantic Convention status of instrumentations In our efforts to maintain optimal user experience and prevent breaking changes for transitioning into stable semantic conventions, OpenTelemetry Python is adopting the semantic convention migration plan for several instrumentations. Currently this plan is only being adopted for [HTTP-related instrumentations](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/non-normative/http-migration.md), but will eventually cover all types. Please refer to the `semconv status` column of the [instrumentation README](instrumentation/README.md) of the current status of instrumentations' semantic conventions. The possible values are `development`, `stable` and `migration` referring to [status](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.31.0/specification/document-status.md#lifecycle-status) of that particular semantic convention. `Migration` refers to an instrumentation that currently supports the migration plan. ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) We meet weekly on Thursday at 9AM PT. The meeting is subject to change depending on contributors' availability. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=c_2bf73e3b6b530da4babd444e72b76a6ad893a5c3f43cf40467abc7a9a897f977%40group.calendar.google.com) for specific dates and for the Zoom link. Meeting notes are available as a public [Google doc](https://docs.google.com/document/d/18w8zOBm_mbety0OqlPwxc7dvnfu641EgmrO4AdJef0U/edit?tab=t.0). The meeting is open for all to join. We invite everyone to join our meeting, regardless of your experience level. Whether you're a seasoned OpenTelemetry developer, just starting your journey, or simply curious about the work we do, you're more than welcome to participate! ### Maintainers - [Aaron Abbott](https://github.com/aabmass), Google - [Emídio Neto](https://github.com/emdneto), Independent - [Leighton Chen](https://github.com/lzchen), Microsoft - [Lukas Hering](https://github.com/herin049), Oracle - [Riccardo Magliocchetti](https://github.com/xrmx), Elastic For more information about the maintainer role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#maintainer). ### Approvers - [Diego Hurtado](https://github.com/ocelotl), Dash0 - [Dylan Russell](https://github.com/dylanrussell), Google - [Héctor Hernández](https://github.com/hectorhdzg), Microsoft - [Keith Decker](https://github.com/keith-decker), Cisco/Splunk - [Liudmila Molkova](https://github.com/lmolkova), Google - [Mike Goldsmith](https://github.com/MikeGoldsmith), Honeycomb - [Pablo Collins](https://github.com/pmcollins), Splunk - [Srikanth Chekuri](https://github.com/srikanthccv), signoz.io - [Tammy Baylis](https://github.com/tammy-baylis-swi), SolarWinds For more information about the approver role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#approver). ### Emeritus - [Alex Boten](https://github.com/codeboten), Maintainer - [Ashutosh Goel](https://github.com/ashu658), Approver - [Jeremy Voss](https://github.com/jeremydvoss), Approver - [Nathaniel Ruiz Nowell](https://github.com/NathanielRN), Approver - [Nikolay Sokolik](https://github.com/nikosokolik), Approver - [Nikolay Sokolik](https://github.com/oxeye-nikolay), Approver - [Owais Lone](https://github.com/owais), Maintainer - [Owais Lone](https://github.com/owais), Approver - [Sanket Mehta](https://github.com/sanketmehta28), Approver - [Shalev Roda](https://github.com/shalevr), Maintainer - [Shalev Roda](https://github.com/shalevr), Approver - [Yusuke Tsutsumi](https://github.com/toumorokoshi), Maintainer For more information about the emeritus role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#emeritus-maintainerapprovertriager). ### Thanks to all of our contributors! Repo contributors opentelemetry-python-contrib-0.66b2/RELEASING.md000066400000000000000000000234331527070072300214050ustar00rootroot00000000000000# Release instructions ## Preparing a new major or minor release * Run the [Prepare release branch workflow](https://github.com/open-telemetry/opentelemetry-python-contrib/actions/workflows/prepare-release-branch.yml). * Press the "Run workflow" button, and leave the default branch `main` selected. * If making a pre-release of stable components (e.g. release candidate), enter the pre-release version number, e.g. `1.9.0rc2`. (otherwise the workflow will pick up the version from `main` and just remove the `.dev` suffix). * Review the two pull requests that it creates. (one is targeted to the release branch and one is targeted to `main`). * The builds will fail for the release PR because of validation rules. Follow the [release workflow](https://github.com/open-telemetry/opentelemetry-python/blob/main/RELEASING.md) for the core repo up until this same point. * Close and reopen the PR so that the workflow will take into account the label automation we have in place * Merge the release PR. * Merge the PR to main (this can be done separately from [making the release](#making-the-release)) ### Preparing a major or minor release for individual package > [!NOTE] > Per-package major or minor release is supported for the following packages only: > - opentelemetry-opamp-client > - opentelemetry-propagator-aws-xray > - opentelemetry-resource-detector-azure > - opentelemetry-resourcedetector-gcp > - opentelemetry-sdk-extension-aws > - opentelemetry-instrumentation-valkey-py > - opentelemetry-util-genai > > These libraries are also excluded from the general release. > > The deprecated GenAI instrumentations listed under [Releasing individual package](#releasing-individual-package) > are intentionally absent here: they no longer get major or minor releases from this repository, only > patch releases from their existing `package-release/` branches. Package release preparation is handled by the [`[Package] Prepare release`](./.github/workflows/package-prepare-release.yml) workflow that allows to pick a specific package to release. It follows the same versioning strategy and process as the general release. Long-term package release branch follows `package-release/{package-name}/v{major}.{minor}.x` (or `package-release/{package-name}/v{major}.{minor}bx`) naming pattern. The workflow will create two pull requests, one against the `main` and one against the `package-release/` branch; both should be merged in order to proceed with the release. To keep the process lightweight, it's OK to approve the PRs you generate and merge without additional reviews. ## Preparing a new patch release ### Backporting Creating manual backports of pull request(s) requires the `backport` label to be added in order to have a green CI. To use the workflow to backport pull request(s) to the release branch: * Run the [Backport workflow](https://github.com/open-telemetry/opentelemetry-python-contrib/actions/workflows/backport.yml). * Press the "Run workflow" button, then select the release branch from the dropdown list, e.g. `release/v1.9.x`, then enter the pull request number that you want to backport, then click the "Run workflow" button below that. * Add the label `backport` to the generated pull request. * In case label automation doesn't work, just close and reopen the PR so that the workflow will take into account the label automation we have in place. * Review and merge the backport pull request that it generates. ### Preparing a patch release * Run the [Prepare patch release workflow](https://github.com/open-telemetry/opentelemetry-python-contrib/actions/workflows/prepare-patch-release.yml). * Press the "Run workflow" button, then select the release branch from the dropdown list, e.g. `release/v1.9.x`, and click the "Run workflow" button below that. * Review and merge the pull request that it creates for updating the version. * Note: If you are doing a patch release in `-contrib` repo, you should also do an equivalent patch release in `-core` repo (even if there's no fix to release), otherwise tests in CI will fail. ### Preparing a patch release for individual package > [!NOTE] > Per-package release is supported only for packages included in the corresponding workflow. Libraries that support per-package release are currently > excluded from the general patch release. Per-package patch release preparation is handled by the [`[Package] Prepare patch release`](./.github/workflows/package-prepare-patch-release.yml) workflow that allows to pick a specific package to release. The workflow can only be run against long-term release branch such as `package-release/{package-name}/v{major}.{minor}.x` or `package-release/{package-name}/v{major}.{minor}bx`. The workflow will create a pull request that should be merged in order to proceed with the release. ## Making the release * Run the [Release workflow](https://github.com/open-telemetry/opentelemetry-python-contrib/actions/workflows/release.yml). * Press the "Run workflow" button, then select the release branch from the dropdown list, e.g. `release/v1.9.x`, and click the "Run workflow" button below that. * This workflow will publish the artifacts and publish a GitHub release with release notes based on the change log. * Review and merge the pull request that it creates for updating the change log in main (note that if this is not a patch release then the change log on main may already be up-to-date, in which case no pull request will be created). * Verify that a new [Github release](https://github.com/open-telemetry/opentelemetry-python-contrib/releases) has been created and that the CHANGELOGs look correct. ### Releasing individual package > [!NOTE] > Per-package patch release is supported for the following packages only: > - opentelemetry-opamp-client > - opentelemetry-propagator-aws-xray > - opentelemetry-resource-detector-azure > - opentelemetry-resourcedetector-gcp > - opentelemetry-sdk-extension-aws > - opentelemetry-instrumentation-openai-v2 > - opentelemetry-instrumentation-openai-agents-v2 > - opentelemetry-instrumentation-vertexai > - opentelemetry-instrumentation-valkey-py > - opentelemetry-util-genai > > These libraries are also excluded from the general patch release. Per-package release is handled by the [`[Package] Release`](./.github/workflows/package-release.yml) workflow that allows to pick a specific package to release. The workflow can only be run against long-term release branch such as `package-release/{package-name}/v{major}.{minor}.x` or `package-release/{package-name}/v{major}.{minor}bx`. ## After the release * Check PyPI * This should be handled automatically on release by the [publish action](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/.github/workflows/release.yml). * Check the [action logs](https://github.com/open-telemetry/opentelemetry-python-contrib/actions/workflows/release.yml) to make sure packages have been uploaded to PyPI * Check the release history (e.g. https://pypi.org/project/opentelemetry-instrumentation/#history) on PyPI * If for some reason the action failed, see [Publish failed](#publish-failed) below ## Notes about version numbering for stable components * The version number for stable components in the `main` branch is always `X.Y.0.dev`, where `X.Y.0` represents the next minor release. * When the release branch is created, you can opt to make a "pre-release", e.g. `X.Y.0rc2`. * If you ARE NOT making a "pre-release": * A "long-term" release branch will be created, e.g. `release/v1.9.x-0.21bx` (notice the wildcard x's). Later on, after the initial release, you can backport PRs to a "long-term" release branch and make patch releases from it. * The version number for stable components in the release branch will be bumped to remove the `.dev`, e.g. `X.Y.0`. * The version number for stable components in the `main` branch will be bumped to the next version, e.g. `X.{Y+1}.0.dev`. * If you ARE making a "pre-release": * A "short-term" release branch will be created, e.g. `release/v1.9.0rc2-0.21b0` (notice the precise version with no wildcard x's). "Short-term" release branches do not support backports or patch releases after the initial release. * The version number for stable components in the `main` branch will not be bumped, e.g. it will remain `X.Y.0.dev` since the next minor release will still be `X.Y.0`. ## Notes about version numbering for unstable components * The version number for unstable components in the `main` branch is always `0.Yb0.dev`, where `0.Yb0` represents the next minor release. * _Question: Is "b" (beta) redundant on "0." releases, or is this a python thing? I'm wondering if we can change it to `0.Y.0` to match up with the practice in js and go repos._ * Unstable components do not need "pre-releases", and so whether or not you are making a "pre-release" of stable components: * The version number for unstable components in the release branch will be bumped to remove the `.dev`, e.g. `0.Yb0`. * The version number for unstable components in the `main` branch will be bumped to the next version, e.g. `0.{Y+1}b0.dev`. ## Releasing dev version of new packages to claim namespace When a contribution introduces a new package, in order to mitigate name-squatting incidents, release the current development version of the new package under the `opentelemetry` user to simply claim the namespace. This should be done shortly after the PR that introduced this package has been merged into `main`. ## Troubleshooting ### Publish failed If for some reason the action failed, do it manually: - Switch to the release branch (important so we don't publish packages with "dev" versions) - Build distributions with `./scripts/build.sh` - Delete distributions we don't want to push (e.g. `testutil`) - Push to PyPI as `twine upload --skip-existing --verbose dist/*` - Double check PyPI! opentelemetry-python-contrib-0.66b2/_template/000077500000000000000000000000001527070072300215175ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/_template/README.rst000066400000000000000000000012201527070072300232010ustar00rootroot00000000000000OpenTelemetry Instrumentation ========================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-.svg :target: https://pypi.org/project/opentelemetry-instrumentation-/ This library allows tracing requests made by the library. Installation ------------ :: pip install opentelemetry-instrumentation- References ---------- * `OpenTelemetry REPLACE ME/ Tracing `_ * `OpenTelemetry Project `_ opentelemetry-python-contrib-0.66b2/_template/autodoc_entry.rst000066400000000000000000000003551527070072300251330ustar00rootroot00000000000000.. include:: ../../../instrumentation/opentelemetry-instrumentation-/README.rst :end-before: References .. automodule:: opentelemetry.instrumentation. :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/_template/pyproject.toml000066400000000000000000000036071527070072300244410ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] # opentelemetry-instrumentation plus the name of the library being instrument e.g # name = "opentelemetry-instrumentation-sqlalchemy" name = "" dynamic = ["version"] # a description of the instrumentation e.g # description = "SQLAlchemy tracing for OpenTelemetry" description = "" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", ] [project.entry-points.opentelemetry_instrumentor] # REPLACE ME: the entrypoint for the instrumentor e.g # sqlalchemy = "opentelemetry.instrumentation.sqlalchemy:SQLAlchemyInstrumentor" REPLACE_ME = "opentelemetry.instrumentation." [project.urls] # url of the instrumentation e.g # Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-sqlalchemy" Homepage = "" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] # REPLACE ME: the path to the version file e.g # "src/opentelemetry/instrumentation/sqlalchemy/version.py" path = "src/opentelemetry/instrumentation//version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/_template/version.py000066400000000000000000000001441527070072300235550ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" opentelemetry-python-contrib-0.66b2/dev-requirements.txt000066400000000000000000000005161527070072300236070ustar00rootroot00000000000000pylint==4.0.5 pyright==v1.1.404 sphinx==7.1.2 sphinx-rtd-theme==2.0.0rc4 sphinx-autodoc-typehints==1.25.2 pytest==7.4.4 pytest-cov==4.1.0 readme-renderer==42.0 bleach==4.1.0 # transient dependency for readme-renderer markupsafe>=2.0.1 codespell==2.1.0 requests==2.33.1 ruamel.yaml==0.17.21 flaky==3.7.0 pre-commit==3.7.0 ruff==0.16.1 opentelemetry-python-contrib-0.66b2/docs-requirements.txt000066400000000000000000000021601527070072300237560ustar00rootroot00000000000000sphinx==7.1.2 sphinx-rtd-theme==2.0.0rc4 sphinx-autodoc-typehints==1.25.2 # Required to avoid breakages for pyramid and cassandra packages setuptools<82.0.0 # Required by opentelemetry-instrumentation fastapi>=0.65.2 pymemcache~=1.3 # Required by conf django>=2.2 # Require by opentelemetry-util-genai fsspec>=2025.9.0 # Required by instrumentation and exporter packages aio_pika~=7.2.0 aiohttp~=3.13 aiokafka~=0.11.0 aiopg>=0.13.0,<1.3.0 asyncpg>=0.12.0 asyncclick~=8.0 botocore~=1.0 boto3~=1.0 cassandra-driver~=3.25 celery>=4.0 confluent-kafka>= 1.8.2,< 3.0.0 flask~=2.0 falcon~=2.0 grpcio~=1.27 httpx>=0.18.0 httpx2~=2.12 kafka-python>=2.0,<3.0 mysql-connector-python~=8.0 mysqlclient~=2.1.1 openai >= 1.26.0 psutil>=5 psycopg~=3.1.17 pika>=0.12.0 pymongo~=4.6.3 PyMySQL~=1.1.1 pymssql~=2.3.2 pyramid>=1.7 redis>=2.6 remoulade[limits]>=5.0.0 ; python_version >= "3.12" remoulade>=0.50.0 ; python_version < "3.12" sqlalchemy>=1.0 starlette~=0.50 structlog>=21.1 tornado>=5.1.1 tortoise-orm>=0.17.0 wrapt~=2.1 # required by opamp uuid_utils protobuf>=5.0,< 7.0 # indirect dependency pins markupsafe==2.0.1 itsdangerous==2.0.1 opentelemetry-python-contrib-0.66b2/docs/000077500000000000000000000000001527070072300204755ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/Makefile000066400000000000000000000011051527070072300221320ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) opentelemetry-python-contrib-0.66b2/docs/conf.py000066400000000000000000000155241527070072300220030ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. import os import sys from configparser import ConfigParser from os import listdir from os.path import isdir, join # configure django to avoid the following exception: # django.core.exceptions.ImproperlyConfigured: Requested settings, but settings # are not configured. You must either define the environment variable # DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings. from django.conf import settings settings.configure() source_dirs = [] exp = "../exporter" exp_dirs = [os.path.abspath("/".join([exp, f, "src"])) for f in listdir(exp) if isdir(join(exp, f))] instr = "../instrumentation" instr_dirs = [os.path.abspath("/".join([instr, f, "src"])) for f in listdir(instr) if isdir(join(instr, f))] instr_genai = "../instrumentation-genai" instr_genai_dirs = [ os.path.abspath("/".join([instr_genai, f, "src"])) for f in listdir(instr_genai) if isdir(join(instr_genai, f)) ] prop = "../propagator" prop_dirs = [os.path.abspath("/".join([prop, f, "src"])) for f in listdir(prop) if isdir(join(prop, f))] sdk_ext = "../sdk-extension" sdk_ext_dirs = [os.path.abspath("/".join([sdk_ext, f, "src"])) for f in listdir(sdk_ext) if isdir(join(sdk_ext, f))] resource = "../resource" resource_dirs = [os.path.abspath("/".join([resource, f, "src"])) for f in listdir(resource) if isdir(join(resource, f))] util = "../util" util_dirs = [os.path.abspath("/".join([util, f, "src"])) for f in listdir(util) if isdir(join(util, f))] opamp = "../opamp" opamp_dirs = [os.path.abspath("/".join([opamp, f, "src"])) for f in listdir(opamp) if isdir(join(opamp, f))] sys.path[:0] = ( exp_dirs + instr_dirs + instr_genai_dirs + sdk_ext_dirs + prop_dirs + resource_dirs + util_dirs + opamp_dirs ) # -- Project information ----------------------------------------------------- project = "OpenTelemetry Python Contrib" copyright = "OpenTelemetry Authors" # pylint: disable=redefined-builtin author = "OpenTelemetry Authors" # -- General configuration --------------------------------------------------- # Easy automatic cross-references for `code in backticks` default_role = "any" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ # API doc generation "sphinx.ext.autodoc", # Support for google-style docstrings "sphinx.ext.napoleon", # Infer types from hints instead of docstrings "sphinx_autodoc_typehints", # Add links to source from generated docs "sphinx.ext.viewcode", # Link to other sphinx docs "sphinx.ext.intersphinx", # Add a .nojekyll file to the generated HTML docs # https://help.github.com/en/articles/files-that-start-with-an-underscore-are-missing "sphinx.ext.githubpages", # Support external links to different versions in the Github repo "sphinx.ext.extlinks", ] intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), "psycopg": ("https://www.psycopg.org/psycopg3/docs/", None), "psycopg2": ("https://www.psycopg.org/docs/", None), "opentracing": ( "https://opentracing-python.readthedocs.io/en/latest/", None, ), "aiohttp": ("https://docs.aiohttp.org/en/stable/", None), "wrapt": ("https://wrapt.readthedocs.io/en/latest/", None), "pymongo": ("https://pymongo.readthedocs.io/en/stable/", None), "opentelemetry": ( "https://opentelemetry-python.readthedocs.io/en/latest/", None, ), "redis": ("https://redis.readthedocs.io/en/latest/", None), "fsspec": ("https://filesystem-spec.readthedocs.io/en/latest/", None), } # http://www.sphinx-doc.org/en/master/config.html#confval-nitpicky # Sphinx will warn about all references where the target cannot be found. nitpicky = True # Sphinx does not recognize generic type TypeVars # Container supposedly were fixed, but does not work # https://github.com/sphinx-doc/sphinx/pull/3744 nitpick_ignore = [ ( "py:class", "opamp_pb2.RemoteConfigStatus", ), ( "py:class", "opamp_pb2.EffectiveConfig", ), ( "py:class", "opamp_pb2.AgentRemoteConfig", ), ] cfg = ConfigParser() cfg.read("./nitpick-exceptions.ini") mcfg = cfg["default"] def getlistcfg(strval): return [val.strip() for line in strval.split("\n") for val in line.split(",") if val.strip()] ignore_categories = [ "py-class", "py-func", "py-exc", "py-obj", "py-data", "any", ] for category in ignore_categories: if category in mcfg: items = getlistcfg(mcfg[category]) for item in items: nitpick_ignore.append((category.replace("-", ":"), item)) # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] autodoc_default_options = { "members": True, "undoc-members": True, "show-inheritance": True, "member-order": "bysource", } # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] # Support external links to specific versions of the files in the Github repo branch = os.environ.get("READTHEDOCS_VERSION") if branch is None or branch == "latest": branch = "main" REPO = "open-telemetry/opentelemetry-python-contrib/" scm_raw_web = "https://raw.githubusercontent.com/" + REPO + branch scm_web = "https://github.com/" + REPO + "blob/" + branch # Store variables in the epilogue so they are globally available. rst_epilog = f""" .. |SCM_WEB| replace:: {scm_web} .. |SCM_RAW_WEB| replace:: {scm_raw_web} .. |SCM_BRANCH| replace:: {branch} """ # used to have links to repo files extlinks = { "scm_raw_web": (scm_raw_web + "/%s", "scm_raw_web"), "scm_web": (scm_web + "/%s", "scm_web"), } opentelemetry-python-contrib-0.66b2/docs/index.rst000066400000000000000000000061211527070072300223360ustar00rootroot00000000000000OpenTelemetry-Python-Contrib ============================ Complimentary instrumentation and vendor-specific packages for use with the Python `OpenTelemetry `_ client. .. image:: https://img.shields.io/badge/slack-chat-green.svg :target: https://cloud-native.slack.com/archives/C01PD4HUVBL :alt: Slack Chat **Please note** that this library is currently in _beta_, and shouldn't generally be used in production environments. Installation ------------ There are several complimentary packages available on PyPI which can be installed separately via pip: .. code-block:: sh pip install opentelemetry-exporter-{exporter} pip install opentelemetry-instrumentation-{instrumentation} pip install opentelemetry-sdk-extension-{sdk-extension} A complete list of packages can be found at the `Contrib repo instrumentation `_ and `Contrib repo exporter `_ directories. Extensions ---------- Visit `OpenTelemetry Registry `_ to find a lot of related projects like exporters, instrumentation libraries, tracer implementations, resource, etc. Installing Cutting Edge Packages ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ While the project is pre-1.0, there may be significant functionality that has not yet been released to PyPI. In that situation, you may want to install the packages directly from the repo. This can be done by cloning the repository and doing an `editable install `_: .. code-block:: sh git clone https://github.com/open-telemetry/opentelemetry-python-contrib.git cd opentelemetry-python-contrib pip install -e ./instrumentation/opentelemetry-instrumentation-flask pip install -e ./instrumentation/opentelemetry-instrumentation-botocore pip install -e ./sdk-extension/opentelemetry-sdk-extension-aws pip install -e ./resource/opentelemetry-resource-detector-containerid .. toctree:: :maxdepth: 2 :caption: OpenTelemetry Instrumentations :name: Instrumentations :glob: instrumentation/** .. toctree:: :maxdepth: 2 :caption: OpenTelemetry Generative AI Instrumentations :name: Generative AI Instrumentations :glob: instrumentation-genai/** .. toctree:: :maxdepth: 2 :caption: OpenTelemetry Propagators :name: Propagators :glob: propagator/** .. toctree:: :maxdepth: 2 :caption: OpenTelemetry Performance :name: Performance :glob: performance/** .. toctree:: :maxdepth: 2 :caption: OpenTelemetry SDK Extensions :name: SDK Extensions :glob: sdk-extension/** .. toctree:: :maxdepth: 2 :caption: OpenTelemetry Resource Detectors :name: Resource Detectors :glob: resource/** .. toctree:: :maxdepth: 2 :caption: OpAMP :name: OpAMP :glob: opamp/** Indices and tables ------------------ * :ref:`genindex` * :ref:`modindex` * :ref:`search` opentelemetry-python-contrib-0.66b2/docs/instrumentation-genai/000077500000000000000000000000001527070072300250215ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation-genai/openai.rst000066400000000000000000000003161527070072300270260ustar00rootroot00000000000000OpenTelemetry Python - OpenAI Instrumentation ============================================= .. automodule:: opentelemetry.instrumentation.openai_v2 :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation-genai/util.rst000066400000000000000000000012741527070072300265340ustar00rootroot00000000000000OpenTelemetry Python - GenAI Util ================================= .. automodule:: opentelemetry.util.genai :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.util.genai.utils :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.util.genai.types :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.util.genai.environment_variables :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.util.genai.completion_hook :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.util.genai._upload :members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/000077500000000000000000000000001527070072300237405ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/aio_pika/000077500000000000000000000000001527070072300255145ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/aio_pika/aio_pika.rst000066400000000000000000000003051527070072300300200ustar00rootroot00000000000000OpenTelemetry aio pika Instrumentation ============================================ .. automodule:: opentelemetry.instrumentation.aio_pika :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/aiohttp_client/000077500000000000000000000000001527070072300267465ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/aiohttp_client/aiohttp_client.rst000066400000000000000000000003211527070072300325020ustar00rootroot00000000000000OpenTelemetry aiohttp client Instrumentation ============================================ .. automodule:: opentelemetry.instrumentation.aiohttp_client :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/aiohttp_server/000077500000000000000000000000001527070072300267765ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/aiohttp_server/aiohttp_server.rst000066400000000000000000000003611527070072300325660ustar00rootroot00000000000000.. include:: ../../../instrumentation/opentelemetry-instrumentation-aiohttp-server/README.rst :end-before: References .. automodule:: opentelemetry.instrumentation.aiohttp_server :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/aiokafka/000077500000000000000000000000001527070072300255065ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/aiokafka/aiokafka.rst000066400000000000000000000003561527070072300300120ustar00rootroot00000000000000.. include:: ../../../instrumentation/opentelemetry-instrumentation-aiokafka/README.rst :end-before: References API --- .. automodule:: opentelemetry.instrumentation.aiokafka :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/aiopg/000077500000000000000000000000001527070072300250375ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/aiopg/aiopg.rst000066400000000000000000000002661527070072300266740ustar00rootroot00000000000000OpenTelemetry aiopg Instrumentation =================================== .. automodule:: opentelemetry.instrumentation.aiopg :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/asgi/000077500000000000000000000000001527070072300246635ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/asgi/asgi.rst000066400000000000000000000003121527070072300263340ustar00rootroot00000000000000.. include:: ../../../instrumentation/opentelemetry-instrumentation-asgi/README.rst API --- .. automodule:: opentelemetry.instrumentation.asgi :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/asyncclick/000077500000000000000000000000001527070072300260635ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/asyncclick/asyncclick.rst000066400000000000000000000003511527070072300307370ustar00rootroot00000000000000.. include:: ../../../instrumentation/opentelemetry-instrumentation-asyncclick/README.rst :end-before: References .. automodule:: opentelemetry.instrumentation.asyncclick :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/asyncio/000077500000000000000000000000001527070072300254055ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/asyncio/asyncio.rst000066400000000000000000000005131527070072300276030ustar00rootroot00000000000000OpenTelemetry asyncio Instrumentation ============================================== .. automodule:: opentelemetry.instrumentation.asyncio :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.instrumentation.asyncio.environment_variables :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/asyncpg/000077500000000000000000000000001527070072300254045ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/asyncpg/asyncpg.rst000066400000000000000000000003351527070072300276030ustar00rootroot00000000000000OpenTelemetry asyncpg Instrumentation ===================================== Module contents --------------- .. automodule:: opentelemetry.instrumentation.asyncpg :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/aws_lambda/000077500000000000000000000000001527070072300260325ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/aws_lambda/aws_lambda.rst000066400000000000000000000003051527070072300306540ustar00rootroot00000000000000OpenTelemetry AWS lambda Instrumentation ========================================== .. automodule:: opentelemetry.instrumentation.aws_lambda :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/base/000077500000000000000000000000001527070072300246525ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/base/instrumentation.rst000066400000000000000000000004241527070072300306470ustar00rootroot00000000000000.. include:: ../../../opentelemetry-instrumentation/README.rst :end-before: References API --- .. automodule:: opentelemetry.instrumentation :members: :undoc-members: :show-inheritance: Submodules ---------- .. toctree:: :maxdepth: 1 instrumentor opentelemetry-python-contrib-0.66b2/docs/instrumentation/base/instrumentor.rst000066400000000000000000000003331527070072300301540ustar00rootroot00000000000000opentelemetry.instrumentation.instrumentor package ================================================== .. automodule:: opentelemetry.instrumentation.instrumentor :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/boto3sqs/000077500000000000000000000000001527070072300255155ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/boto3sqs/boto3sqs.rst000066400000000000000000000003111527070072300300170ustar00rootroot00000000000000.. include:: ../../../instrumentation/opentelemetry-instrumentation-boto3sqs/README.rst .. automodule:: opentelemetry.instrumentation.boto3sqs :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/botocore/000077500000000000000000000000001527070072300255545ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/botocore/botocore.rst000066400000000000000000000002771527070072300301300ustar00rootroot00000000000000OpenTelemetry Botocore Instrumentation ====================================== .. automodule:: opentelemetry.instrumentation.botocore :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/cassandra/000077500000000000000000000000001527070072300256775ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/cassandra/cassandra.rst000066400000000000000000000003021527070072300303630ustar00rootroot00000000000000OpenTelemetry Cassandra Instrumentation ======================================= .. automodule:: opentelemetry.instrumentation.cassandra :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/celery/000077500000000000000000000000001527070072300252235ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/celery/celery.rst000066400000000000000000000002711527070072300272400ustar00rootroot00000000000000OpenTelemetry Celery Instrumentation ==================================== .. automodule:: opentelemetry.instrumentation.celery :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/click/000077500000000000000000000000001527070072300250255ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/click/click.rst000066400000000000000000000003371527070072300266470ustar00rootroot00000000000000.. include:: ../../../instrumentation/opentelemetry-instrumentation-click/README.rst :end-before: References .. automodule:: opentelemetry.instrumentation.click :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/confluent_kafka/000077500000000000000000000000001527070072300270725ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/confluent_kafka/confluent_kafka.rst000066400000000000000000000003451527070072300327600ustar00rootroot00000000000000.. include:: ../../../instrumentation/opentelemetry-instrumentation-confluent-kafka/README.rst .. automodule:: opentelemetry.instrumentation.confluent_kafka :members: :undoc-members: :show-inheritance: :noindex: opentelemetry-python-contrib-0.66b2/docs/instrumentation/dbapi/000077500000000000000000000000001527070072300250175ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/dbapi/dbapi.rst000066400000000000000000000003041527070072300266250ustar00rootroot00000000000000OpenTelemetry Database API Instrumentation ========================================== .. automodule:: opentelemetry.instrumentation.dbapi :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/django/000077500000000000000000000000001527070072300252025ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/django/django.rst000066400000000000000000000002711527070072300271760ustar00rootroot00000000000000OpenTelemetry Django Instrumentation ==================================== .. automodule:: opentelemetry.instrumentation.django :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/exceptions/000077500000000000000000000000001527070072300261215ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/exceptions/exceptions.rst000066400000000000000000000003051527070072300310320ustar00rootroot00000000000000OpenTelemetry Exceptions Instrumentation ======================================== .. automodule:: opentelemetry.instrumentation.exceptions :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/falcon/000077500000000000000000000000001527070072300252025ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/falcon/falcon.rst000066400000000000000000000002711527070072300271760ustar00rootroot00000000000000OpenTelemetry Falcon Instrumentation ==================================== .. automodule:: opentelemetry.instrumentation.falcon :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/fastapi/000077500000000000000000000000001527070072300253675ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/fastapi/fastapi.rst000066400000000000000000000003171527070072300275510ustar00rootroot00000000000000.. include:: ../../../instrumentation/opentelemetry-instrumentation-fastapi/README.rst API --- .. automodule:: opentelemetry.instrumentation.fastapi :members: :undoc-members: :show-inheritance:opentelemetry-python-contrib-0.66b2/docs/instrumentation/flask/000077500000000000000000000000001527070072300250405ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/flask/flask.rst000066400000000000000000000002661527070072300266760ustar00rootroot00000000000000OpenTelemetry Flask Instrumentation =================================== .. automodule:: opentelemetry.instrumentation.flask :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/grpc/000077500000000000000000000000001527070072300246735ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/grpc/grpc.rst000066400000000000000000000003211527070072300263540ustar00rootroot00000000000000OpenTelemetry gRPC Instrumentation ================================== Module contents --------------- .. automodule:: opentelemetry.instrumentation.grpc :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/httpx/000077500000000000000000000000001527070072300251075ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/httpx/httpx.rst000066400000000000000000000004001527070072300270020ustar00rootroot00000000000000.. include:: ../../../instrumentation/opentelemetry-instrumentation-httpx/README.rst :end-before: References API --- .. automodule:: opentelemetry.instrumentation.httpx :members: :undoc-members: :show-inheritance: :inherited-members: opentelemetry-python-contrib-0.66b2/docs/instrumentation/jinja2/000077500000000000000000000000001527070072300251155ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/jinja2/jinja2.rst000066400000000000000000000002711527070072300270240ustar00rootroot00000000000000OpenTelemetry Jinja2 Instrumentation ==================================== .. automodule:: opentelemetry.instrumentation.jinja2 :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/kafka_python/000077500000000000000000000000001527070072300264165ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/kafka_python/kafka_python.rst000066400000000000000000000003571527070072300316330ustar00rootroot00000000000000.. include:: ../../../instrumentation/opentelemetry-instrumentation-kafka-python/README.rst :end-before: References API --- .. automodule:: opentelemetry.instrumentation.kafka :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/logging/000077500000000000000000000000001527070072300253665ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/logging/logging.rst000066400000000000000000000002741527070072300275510ustar00rootroot00000000000000OpenTelemetry Logging Instrumentation ===================================== .. automodule:: opentelemetry.instrumentation.logging :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/mysql/000077500000000000000000000000001527070072300251055ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/mysql/mysql.rst000066400000000000000000000002661527070072300270100ustar00rootroot00000000000000OpenTelemetry MySQL Instrumentation =================================== .. automodule:: opentelemetry.instrumentation.mysql :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/mysqlclient/000077500000000000000000000000001527070072300263045ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/mysqlclient/mysqlclient.rst000066400000000000000000000003101527070072300313740ustar00rootroot00000000000000OpenTelemetry mysqlclient Instrumentation ========================================= .. automodule:: opentelemetry.instrumentation.mysqlclient :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/pika/000077500000000000000000000000001527070072300246645ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/pika/pika.rst000066400000000000000000000002661527070072300263460ustar00rootroot00000000000000OpenTelemetry Pika Instrumentation ===================================== .. automodule:: opentelemetry.instrumentation.pika :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/psycopg/000077500000000000000000000000001527070072300254245ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/psycopg/psycopg.rst000066400000000000000000000002741527070072300276450ustar00rootroot00000000000000OpenTelemetry Psycopg Instrumentation ===================================== .. automodule:: opentelemetry.instrumentation.psycopg :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/psycopg2/000077500000000000000000000000001527070072300255065ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/psycopg2/psycopg2.rst000066400000000000000000000002771527070072300300140ustar00rootroot00000000000000OpenTelemetry Psycopg2 Instrumentation ====================================== .. automodule:: opentelemetry.instrumentation.psycopg2 :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/pymemcache/000077500000000000000000000000001527070072300260535ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/pymemcache/pymemcache.rst000066400000000000000000000003051527070072300307160ustar00rootroot00000000000000OpenTelemetry pymemcache Instrumentation ======================================== .. automodule:: opentelemetry.instrumentation.pymemcache :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/pymongo/000077500000000000000000000000001527070072300254305ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/pymongo/pymongo.rst000066400000000000000000000002741527070072300276550ustar00rootroot00000000000000OpenTelemetry pymongo Instrumentation ===================================== .. automodule:: opentelemetry.instrumentation.pymongo :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/pymssql/000077500000000000000000000000001527070072300254505ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/pymssql/pymssql.rst000066400000000000000000000002741527070072300277150ustar00rootroot00000000000000OpenTelemetry pymssql Instrumentation ===================================== .. automodule:: opentelemetry.instrumentation.pymssql :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/pymysql/000077500000000000000000000000001527070072300254565ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/pymysql/pymysql.rst000066400000000000000000000002741527070072300277310ustar00rootroot00000000000000OpenTelemetry PyMySQL Instrumentation ===================================== .. automodule:: opentelemetry.instrumentation.pymysql :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/pyramid/000077500000000000000000000000001527070072300254055ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/pyramid/pyramid.rst000066400000000000000000000002741527070072300276070ustar00rootroot00000000000000OpenTelemetry Pyramid Instrumentation ===================================== .. automodule:: opentelemetry.instrumentation.pyramid :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/redis/000077500000000000000000000000001527070072300250465ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/redis/redis.rst000066400000000000000000000003531527070072300267070ustar00rootroot00000000000000.. include:: ../../../instrumentation/opentelemetry-instrumentation-redis/README.rst :end-before: References Usage ----- .. automodule:: opentelemetry.instrumentation.redis :members: :undoc-members: :show-inheritance:opentelemetry-python-contrib-0.66b2/docs/instrumentation/remoulade/000077500000000000000000000000001527070072300257155ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/remoulade/remoulade.rst000066400000000000000000000003141527070072300304220ustar00rootroot00000000000000.. include:: ../../../instrumentation/opentelemetry-instrumentation-remoulade/README.rst .. automodule:: opentelemetry.instrumentation.remoulade :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/requests/000077500000000000000000000000001527070072300256135ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/requests/requests.rst000066400000000000000000000002771527070072300302260ustar00rootroot00000000000000OpenTelemetry requests Instrumentation ====================================== .. automodule:: opentelemetry.instrumentation.requests :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/sqlalchemy/000077500000000000000000000000001527070072300261025ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/sqlalchemy/sqlalchemy.rst000066400000000000000000000003051527070072300307740ustar00rootroot00000000000000OpenTelemetry SQLAlchemy Instrumentation ======================================== .. automodule:: opentelemetry.instrumentation.sqlalchemy :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/sqlite3/000077500000000000000000000000001527070072300253245ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/sqlite3/sqlite3.rst000066400000000000000000000002741527070072300274450ustar00rootroot00000000000000OpenTelemetry SQLite3 Instrumentation ===================================== .. automodule:: opentelemetry.instrumentation.sqlite3 :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/starlette/000077500000000000000000000000001527070072300257475ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/starlette/starlette.rst000066400000000000000000000003231527070072300305060ustar00rootroot00000000000000.. include:: ../../../instrumentation/opentelemetry-instrumentation-starlette/README.rst API --- .. automodule:: opentelemetry.instrumentation.starlette :members: :undoc-members: :show-inheritance:opentelemetry-python-contrib-0.66b2/docs/instrumentation/structlog/000077500000000000000000000000001527070072300257665ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/structlog/structlog.rst000066400000000000000000000003021527070072300305410ustar00rootroot00000000000000OpenTelemetry Structlog Instrumentation ======================================= .. automodule:: opentelemetry.instrumentation.structlog :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/system_metrics/000077500000000000000000000000001527070072300270125ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/system_metrics/system_metrics.rst000066400000000000000000000005361527070072300326220ustar00rootroot00000000000000OpenTelemetry system metrics Instrumentation ============================================ .. automodule:: opentelemetry.instrumentation.system_metrics :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.instrumentation.system_metrics.environment_variables :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/threading/000077500000000000000000000000001527070072300257055ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/threading/threading.rst000066400000000000000000000003021527070072300303770ustar00rootroot00000000000000OpenTelemetry Threading Instrumentation ======================================= .. automodule:: opentelemetry.instrumentation.threading :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/tornado/000077500000000000000000000000001527070072300254065ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/tornado/tornado.rst000066400000000000000000000002751527070072300276120ustar00rootroot00000000000000OpenTelemetry Tornado Instrumentation ====================================== .. automodule:: opentelemetry.instrumentation.tornado :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/tortoiseorm/000077500000000000000000000000001527070072300263265ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/tortoiseorm/tortoiseorm.rst000066400000000000000000000003171527070072300314470ustar00rootroot00000000000000.. include:: ../../../instrumentation/opentelemetry-instrumentation-tortoiseorm/README.rst .. automodule:: opentelemetry.instrumentation.tortoiseorm :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/urllib/000077500000000000000000000000001527070072300252315ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/urllib/urllib3.rst000066400000000000000000000003011527070072300273310ustar00rootroot00000000000000OpenTelemetry urllib Instrumentation ============================================ .. automodule:: opentelemetry.instrumentation.urllib :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/urllib3/000077500000000000000000000000001527070072300253145ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/urllib3/urllib3.rst000066400000000000000000000003031527070072300274160ustar00rootroot00000000000000OpenTelemetry urllib3 Instrumentation ============================================ .. automodule:: opentelemetry.instrumentation.urllib3 :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/instrumentation/wsgi/000077500000000000000000000000001527070072300247115ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/instrumentation/wsgi/wsgi.rst000066400000000000000000000002631527070072300264150ustar00rootroot00000000000000OpenTelemetry WSGI Instrumentation ================================== .. automodule:: opentelemetry.instrumentation.wsgi :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/make.bat000066400000000000000000000014231527070072300221020ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd opentelemetry-python-contrib-0.66b2/docs/nitpick-exceptions.ini000066400000000000000000000050101527070072300250120ustar00rootroot00000000000000[default] py-class= ; TODO: Understand why sphinx is not able to find this local class opentelemetry.propagators.textmap.CarrierT opentelemetry.propagators.textmap.Setter opentelemetry.propagators.textmap.Getter opentelemetry.propagators.textmap.TextMapPropagator ; - AwsXRayPropagator opentelemetry.propagators.textmap.DefaultGetter ; API opentelemetry.propagators.textmap.Getter ; - AWSXRayPropagator opentelemetry.sdk.trace.id_generator.IdGenerator opentelemetry.instrumentation.confluent_kafka.ProxiedProducer opentelemetry.instrumentation.confluent_kafka.ProxiedConsumer opentelemetry.instrumentation.instrumentor.BaseInstrumentor ; - AwsXRayIdGenerator TextMapPropagator CarrierT Setter Getter ; - AwsXRayPropagator.extract ; httpx changes __module__ causing Sphinx to error and no Sphinx site is available httpx.Client httpx.AsyncClient httpx.BaseTransport httpx2.BaseTransport openai.BaseTransport httpx.AsyncBaseTransport httpx2.AsyncBaseTransport httpx.SyncByteStream httpx.AsyncByteStream httpx.Response httpx.URL httpx.Headers aiohttp.web_request.Request celery.worker.request.Request yarl.URL cimpl.Producer cimpl.Consumer func Message TopicPartition callable Consumer confluent_kafka.Message psycopg.Connection psycopg.AsyncConnection ObjectProxy wrapt.proxies.ObjectProxy _wrappers.ObjectProxy fastapi.applications.FastAPI starlette.applications.Starlette _contextvars.Token opentelemetry.instrumentation.httpx._SyncOpenTelemetryTransportBase opentelemetry.instrumentation.httpx._AsyncOpenTelemetryTransportBase opentelemetry.instrumentation.httpx._BaseHTTPXClientInstrumentor any= ; API opentelemetry.propagators.textmap.TextMapPropagator.fields ; - AWSXRayPropagator TraceId ; - AwsXRayIdGenerator TraceIdRatioBased ; - AwsXRayIdGenerator ; SDK SpanProcessor TracerProvider ; - AwsXRayIdGenerator ; Instrumentation BaseInstrumentor ; - instrumentation.* Setter httpx instrument __iter__ list.__iter__ __getitem__ list.__getitem__ SQS.ReceiveMessage py-obj= opentelemetry.propagators.textmap.CarrierT opentelemetry.instrumentation.dbapi.ConnectionT opentelemetry.instrumentation.dbapi.CursorT py-data= typing.Union typing.Optional py-func= poll flush Message.error py-exc= KafkaException KafkaError opentelemetry-python-contrib-0.66b2/docs/opamp/000077500000000000000000000000001527070072300216115ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/opamp/client.rst000066400000000000000000000002471527070072300236240ustar00rootroot00000000000000OpenTelemetry Python - OpAMP Client =================================== .. automodule:: opentelemetry._opamp :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/performance/000077500000000000000000000000001527070072300227765ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/performance/benchmarks.rst000066400000000000000000000006571527070072300256550ustar00rootroot00000000000000Performance Tests - Benchmarks ============================== For instructions on running and writing benchmarks locally, see the `Benchmarks section of CONTRIBUTING.md `_. Live benchmark results are published at `opentelemetry.io/docs/languages/python/benchmarks `_. opentelemetry-python-contrib-0.66b2/docs/propagator/000077500000000000000000000000001527070072300226535ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/propagator/aws/000077500000000000000000000000001527070072300234455ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/propagator/aws/aws.rst000066400000000000000000000002771527070072300247770ustar00rootroot00000000000000OpenTelemetry Python - AWS X-Ray Propagator =========================================== .. automodule:: opentelemetry.propagators.aws :members: :undoc-members: :show-inheritance:opentelemetry-python-contrib-0.66b2/docs/resource/000077500000000000000000000000001527070072300223245ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/resource/container/000077500000000000000000000000001527070072300243065ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/resource/container/container.rst000066400000000000000000000003461527070072300270250ustar00rootroot00000000000000OpenTelemetry Python - Resource Detector for Containers ======================================================= .. automodule:: opentelemetry.resource.detector.containerid :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/docs/sdk-extension/000077500000000000000000000000001527070072300232705ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/sdk-extension/aws/000077500000000000000000000000001527070072300240625ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/docs/sdk-extension/aws/aws.rst000066400000000000000000000003301527070072300254020ustar00rootroot00000000000000OpenTelemetry Python - AWS SDK Extension ======================================== .. automodule:: opentelemetry.sdk.extension.aws.trace.aws_xray_id_generator :members: :undoc-members: :show-inheritance: opentelemetry-python-contrib-0.66b2/eachdist.ini000066400000000000000000000036411527070072300220360ustar00rootroot00000000000000# These will be sorted first in that order. # All packages that are depended upon by others should be listed here. [DEFAULT] ignore= _template sortfirst= opentelemetry-instrumentation util/opentelemetry-util-http instrumentation/opentelemetry-instrumentation-wsgi instrumentation/opentelemetry-instrumentation-dbapi instrumentation/opentelemetry-instrumentation-asgi instrumentation/opentelemetry-instrumentation-botocore instrumentation/* exporter/* ext/* [stable] version=1.45.2 packages= opentelemetry-sdk opentelemetry-proto opentelemetry-propagator-jaeger opentelemetry-propagator-b3 opentelemetry-exporter-zipkin-proto-http opentelemetry-exporter-zipkin-json opentelemetry-exporter-zipkin opentelemetry-exporter-otlp-proto-grpc opentelemetry-exporter-otlp opentelemetry-exporter-jaeger-thrift opentelemetry-exporter-jaeger-proto-grpc opentelemetry-exporter-jaeger opentelemetry-api opentelemetry-exporter-credential-provider-gcp [prerelease] version=0.66b2 packages= all opentelemetry-semantic-conventions opentelemetry-test-utils opentelemetry-instrumentation opentelemetry-contrib-instrumentations opentelemetry-distro opentelemetry-resource-detector-containerid [exclude_release] packages= opentelemetry-opamp-client opentelemetry-instrumentation-valkey-py opentelemetry-propagator-aws-xray opentelemetry-resource-detector-azure opentelemetry-resourcedetector-gcp opentelemetry-sdk-extension-aws opentelemetry-propagator-aws-xray opentelemetry-instrumentation-vertexai opentelemetry-instrumentation-openai-v2 opentelemetry-instrumentation-openai-agents-v2 opentelemetry-instrumentation-test opentelemetry-util-genai [lintroots] extraroots=examples/*,scripts/ subglob=*.py,tests/,test/,src/*,examples/* [testroots] extraroots=examples/*,tests/ subglob=tests/,test/ opentelemetry-python-contrib-0.66b2/exporter/000077500000000000000000000000001527070072300214155ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-credential-provider-gcp/000077500000000000000000000000001527070072300326665ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-credential-provider-gcp/LICENSE000066400000000000000000000261351527070072300337020ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000043541527070072300343040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-credential-provider-gcpOpenTelemetry GCP Credential Provider for OTLP Exporters ======================================================== .. image:: https://badge.fury.io/py/opentelemetry-exporter-credential-provider-gcp.svg :target: https://badge.fury.io/py/opentelemetry-exporter-credential-provider-gcp .. image:: https://readthedocs.org/projects/google-cloud-opentelemetry/badge/?version=latest :target: https://google-cloud-opentelemetry.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status This library provides support for supplying your machine's Application Default Credentials (https://cloud.google.com/docs/authentication/application-default-credentials) to the OTLP Exporters created automatically by OpenTelemetry Python's auto instrumentation. These credentials authorize OTLP traces to be sent to `telemetry.googleapis.com`. Currently `telemetry.googleapis.com` only supports OTLP traces, but eventually the endpoint will support OTLP logs and metrics too. To learn more about instrumentation and observability, including opinionated recommendations for Google Cloud Observability, visit `Instrumentation and observability `_. For how to export trace data when you use manual instrumentation visit `Migrate from the Trace exporter to the OTLP endpoint `_. Installation ------------ .. code:: bash pip install opentelemetry-exporter-credential-provider-gcp Usage ----- Your installed HTTP/GRPC OTLP Exporter must be at release `>=1.37` for this feature. Set the following environment variables: `export OTEL_RESOURCE_ATTRIBUTES="gcp.project_id="` `export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://telemetry.googleapis.com:443/v1/traces"` If you plan to have python auto instrumentation use the GRPC OTLP Exporter to send traces to Cloud Trace: `export OTEL_PYTHON_EXPORTER_OTLP_GRPC_TRACES_CREDENTIAL_PROVIDER=gcp_grpc_credentials` If you plan to have python auto instrumentation use the HTTP OTLP Exporter to send traces to Cloud Trace: `export OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER=gcp_http_credentials` References ---------- * `OpenTelemetry Project `_ pyproject.toml000066400000000000000000000031711527070072300355250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-credential-provider-gcp[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-exporter-credential-provider-gcp" dynamic = ["version"] description = "GCP OTLP Exporter Credential Provider for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "grpcio >= 1.67.0", "google-auth >= 2.25", "requests >= 2.7", ] [project.entry-points.opentelemetry_otlp_credential_provider] gcp_grpc_credentials = "opentelemetry.gcp_credential_provider:create_google_grpc_credentials" gcp_http_credentials = "opentelemetry.gcp_credential_provider:create_google_authorized_session" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/exporter/opentelemetry-exporter-credential-provider-gcp" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/gcp_credential_provider/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-credential-provider-gcp/src/000077500000000000000000000000001527070072300334555ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300362725ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-credential-provider-gcp/srcgcp_credential_provider/000077500000000000000000000000001527070072300431475ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-credential-provider-gcp/src/opentelemetry__init__.py000066400000000000000000000014441527070072300452630ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-credential-provider-gcp/src/opentelemetry/gcp_credential_provider# Copyright 2025 Google LLC # SPDX-License-Identifier: Apache-2.0 import google.auth import grpc import requests from google.auth.transport.grpc import AuthMetadataPlugin from google.auth.transport.requests import AuthorizedSession, Request # pyright: reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownVariableType=false def create_google_grpc_credentials() -> grpc.ChannelCredentials: credentials, _ = google.auth.default() return grpc.composite_channel_credentials( grpc.ssl_channel_credentials(), grpc.metadata_call_credentials(AuthMetadataPlugin(credentials=credentials, request=Request())), ) def create_google_authorized_session() -> requests.Session: credentials, _ = google.auth.default() return AuthorizedSession(credentials) version.py000066400000000000000000000001441527070072300452050ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-credential-provider-gcp/src/opentelemetry/gcp_credential_provider# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000002031527070072300370430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-credential-provider-gcppytest==7.4.4 opentelemetry-exporter-otlp-proto-http==1.37 opentelemetry-exporter-otlp-proto-common==1.37 opentelemetry-proto==1.37opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-credential-provider-gcp/tests/000077500000000000000000000000001527070072300340305ustar00rootroot00000000000000test_credential_provider.py000066400000000000000000000030601527070072300414050ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-credential-provider-gcp/tests# Copyright 2023 Google LLC # SPDX-License-Identifier: Apache-2.0 from os import environ from unittest import TestCase from unittest.mock import MagicMock, patch from google.auth.transport.requests import AuthorizedSession from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_GRPC_TRACES_CREDENTIAL_PROVIDER, _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER, ) # pyright: reportUnknownMemberType=false, reportUnknownVariableType=false, reportMissingImports=false, reportUntypedFunctionDecorator=false class TestOTLPTraceAutoInstrumentGcpCredential(TestCase): @patch("google.auth.default") @patch.dict( environ, { _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER: "gcp_http_credentials", _OTEL_PYTHON_EXPORTER_OTLP_GRPC_TRACES_CREDENTIAL_PROVIDER: "gcp_grpc_credentials", }, ) def test_loads_otlp_exporters_with_google_creds(self, mock_default: MagicMock): # pylint: disable=no-self-use """Test that OTel configuration internals can load the credentials from entrypoint by name""" mock_credentials = MagicMock() mock_credentials.project_id = "test-project" mock_default.return_value = (mock_credentials, "test-project") http_exporter = OTLPSpanExporter() assert isinstance(http_exporter._session, AuthorizedSession) # Assert that google.auth.default was called mock_default.assert_called_once() opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/000077500000000000000000000000001527070072300327715ustar00rootroot00000000000000README.rst000066400000000000000000000016751527070072300344120ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-writeOpenTelemetry Prometheus Remote Write Exporter ============================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-prometheus-remote-write.svg :target: https://pypi.org/project/opentelemetry-exporter-prometheus-remote-write/ This package contains an exporter to send metrics from the OpenTelemetry Python SDK directly to a Prometheus Remote Write integrated backend (such as Cortex or Thanos) without having to run an instance of the Prometheus server. Installation ------------ :: pip install opentelemetry-exporter-prometheus-remote-write .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ .. _Prometheus Remote Write integrated backend: https://prometheus.io/docs/operating/integrations/ References ---------- * `OpenTelemetry Project `_ * `Prometheus Remote Write Integration `_ opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/example/000077500000000000000000000000001527070072300344245ustar00rootroot00000000000000Dockerfile000066400000000000000000000003221527070072300363340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/exampleFROM python:3.10 RUN apt-get update -y && apt-get install libsnappy-dev -y WORKDIR /code COPY . . RUN pip install -e . RUN pip install -r ./example/requirements.txt CMD ["python", "./example/sampleapp.py"] README.md000066400000000000000000000037451527070072300356350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/example# Prometheus Remote Write Exporter Example This example uses [Docker Compose](https://docs.docker.com/compose/) to set up: 1. A Python program that creates 5 instruments with 5 unique aggregators and a randomized load generator 2. An instance of [Cortex](https://cortexmetrics.io/) to receive the metrics data 3. An instance of [Grafana](https://grafana.com/) to visualizse the exported data ## Requirements * Have Docker Compose [installed](https://docs.docker.com/compose/install/) *Users do not need to install Python as the app will be run in the Docker Container* ## Instructions 1. Run `docker-compose up -d` in the the `example/` directory The `-d` flag causes all services to run in detached mode and frees up your terminal session. This also causes no logs to show up. Users can attach themselves to the service's logs manually using `docker logs ${CONTAINER_ID} --follow` 2. Log into the Grafana instance at [http://localhost:3000](http://localhost:3000) * login credentials are `username: admin` and `password: admin` * There may be an additional screen on setting a new password. This can be skipped and is optional 3. Navigate to the `Data Sources` page * Look for a gear icon on the left sidebar and select `Data Sources` 4. Add a new Prometheus Data Source * Use `http://cortex:9009/api/prom` as the URL * (OPTIONAl) set the scrape interval to `2s` to make updates appear quickly * click `Save & Test` 5. Go to `Metrics Explore` to query metrics * Look for a compass icon on the left sidebar * click `Metrics` for a dropdown list of all the available metrics * (OPTIONAL) Adjust time range by clicking the `Last 6 hours` button on the upper right side of the graph * (OPTIONAL) Set up auto-refresh by selecting an option under the dropdown next to the refresh button on the upper right side of the graph * Click the refresh button and data should show up on the graph 6. Shutdown the services when finished * Run `docker-compose down` in the example directory cortex-config.yml000066400000000000000000000051611527070072300376420ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/example# This Cortex Config is copied from the Cortex Project documentation # Source: https://github.com/cortexproject/cortex/blob/master/docs/configuration/single-process-config.yaml # Configuration for running Cortex in single-process mode. # This configuration should not be used in production. # It is only for getting started and development. # Disable the requirement that every request to Cortex has a # X-Scope-OrgID header. `fake` will be substituted in instead. # pylint: skip-file auth_enabled: false server: http_listen_port: 9009 # Configure the server to allow messages up to 100MB. grpc_server_max_recv_msg_size: 104857600 grpc_server_max_send_msg_size: 104857600 grpc_server_max_concurrent_streams: 1000 distributor: shard_by_all_labels: true pool: health_check_ingesters: true ingester_client: grpc_client_config: # Configure the client to allow messages up to 100MB. max_recv_msg_size: 104857600 max_send_msg_size: 104857600 use_gzip_compression: true ingester: # We want our ingesters to flush chunks at the same time to optimise # deduplication opportunities. spread_flushes: true chunk_age_jitter: 0 walconfig: wal_enabled: true recover_from_wal: true wal_dir: /tmp/cortex/wal lifecycler: # The address to advertise for this ingester. Will be autodiscovered by # looking up address on eth0 or en0; can be specified if this fails. # address: 127.0.0.1 # We want to start immediately and flush on shutdown. join_after: 0 min_ready_duration: 0s final_sleep: 0s num_tokens: 512 tokens_file_path: /tmp/cortex/wal/tokens # Use an in memory ring store, so we don't need to launch a Consul. ring: kvstore: store: inmemory replication_factor: 1 # Use local storage - BoltDB for the index, and the filesystem # for the chunks. schema: configs: - from: 2019-07-29 store: boltdb object_store: filesystem schema: v10 index: prefix: index_ period: 1w storage: boltdb: directory: /tmp/cortex/index filesystem: directory: /tmp/cortex/chunks delete_store: store: boltdb purger: object_store_type: filesystem frontend_worker: # Configure the frontend worker in the querier to match worker count # to max_concurrent on the queriers. match_max_concurrent: true # Configure the ruler to scan the /tmp/cortex/rules directory for prometheus # rules: https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/#recording-rules ruler: enable_api: true enable_sharding: false storage: type: local local: directory: /tmp/cortex/rules docker-compose.yml000066400000000000000000000017311527070072300400040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/example# Copyright The OpenTelemetry Authors # # 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. version: "3.8" services: cortex: image: quay.io/cortexproject/cortex:v1.5.0 command: - -config.file=./config/cortex-config.yml volumes: - ./cortex-config.yml:/config/cortex-config.yml:ro ports: - 9009:9009 grafana: image: grafana/grafana:latest ports: - 3000:3000 sample_app: build: context: ../ dockerfile: ./example/Dockerfile requirements.txt000066400000000000000000000001661527070072300376340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/examplepsutil protobuf>=3.13.0 requests>=2.25.0 python-snappy>=0.5.4 opentelemetry-api opentelemetry-sdk opentelemetry-proto sampleapp.py000066400000000000000000000052231527070072300367030ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/example# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import random import sys import time from logging import INFO import psutil from opentelemetry import metrics from opentelemetry.exporter.prometheus_remote_write import ( PrometheusRemoteWriteMetricsExporter, ) from opentelemetry.metrics import Observation from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader logging.basicConfig(stream=sys.stdout, level=logging.INFO) logger = logging.getLogger(__name__) testing_labels = {"environment": "testing"} exporter = PrometheusRemoteWriteMetricsExporter( endpoint="http://cortex:9009/api/prom/push", headers={"X-Scope-Org-ID": "5"}, ) reader = PeriodicExportingMetricReader(exporter, 1000) provider = MeterProvider(metric_readers=[reader]) metrics.set_meter_provider(provider) meter = metrics.get_meter(__name__) # Callback to gather cpu usage def get_cpu_usage_callback(observer): for number, percent in enumerate(psutil.cpu_percent(percpu=True)): labels = {"cpu_number": str(number)} yield Observation(percent, labels) # Callback to gather RAM usage def get_ram_usage_callback(observer): ram_percent = psutil.virtual_memory().percent yield Observation(ram_percent, {}) requests_counter = meter.create_counter( name="requests", description="number of requests", unit="1", ) request_min_max = meter.create_counter( name="requests_min_max", description="min max sum count of requests", unit="1", ) request_last_value = meter.create_counter( name="requests_last_value", description="last value number of requests", unit="1", ) requests_active = meter.create_up_down_counter( name="requests_active", description="number of active requests", unit="1", ) meter.create_observable_counter( callbacks=[get_ram_usage_callback], name="ram_usage", description="ram usage", unit="1", ) meter.create_observable_up_down_counter( callbacks=[get_cpu_usage_callback], name="cpu_percent", description="per-cpu usage", unit="1", ) request_latency = meter.create_histogram("request_latency") # Load generator num = random.randint(0, 1000) while True: # counters requests_counter.add(num % 131 + 200, testing_labels) request_min_max.add(num % 181 + 200, testing_labels) request_last_value.add(num % 101 + 200, testing_labels) # updown counter requests_active.add(num % 7231 + 200, testing_labels) request_latency.record(num % 92, testing_labels) logger.log(level=INFO, msg="completed metrics collection cycle") time.sleep(1) num += 9791 opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/proto/000077500000000000000000000000001527070072300341345ustar00rootroot00000000000000.gitignore000066400000000000000000000000161527070072300360420ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/protoopentelemetry README.md000066400000000000000000000003421527070072300353330ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/proto## Instructions 1. Install protobuf tools. Can use your package manager or download from [GitHub](https://github.com/protocolbuffers/protobuf/releases/tag/v26.0) 2. Run `generate-proto-py.sh` from inside the `proto/` directorygenerate-proto-py.sh000077500000000000000000000041321527070072300377750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/proto#!/bin/bash set -e PROM_VERSION=v2.39.0 PROTO_VERSION=v1.3.2 # SRC_DIR is from protoc perspective. ie its the destination for our checkouts/clones SRC_DIR=opentelemetry/exporter/prometheus_remote_write/gen/ #TODO: # Check that black & protoc are installed properly echo "Creating our destination directory" mkdir -p ${SRC_DIR}/gogoproto # Clone prometheus echo "Grabbing Prometheus protobuf files" git clone --filter=blob:none --sparse https://github.com/prometheus/prometheus.git cd prometheus git checkout ${PROM_VERSION} git sparse-checkout set prompb cd .. # We also need gogo.proto which is in the protobuf Repo # Could also try to pull this locally from the install location of protobuf # but that will be harder in a platform agnostic way. echo "Grabbing gogo.proto" git clone --filter=blob:none --sparse https://github.com/gogo/protobuf.git cd protobuf git checkout ${PROTO_VERSION} git sparse-checkout set /gogoproto/gogo.proto cd .. # Move the proto files into our structure echo "Moving proto files to ${SRC_DIR}" cp prometheus/prompb/remote.proto prometheus/prompb/types.proto ${SRC_DIR} cp protobuf/gogoproto/gogo.proto ${SRC_DIR}/gogoproto/ # A bit of a hack, but we need to fix the imports to fit the python structure. # using sed to find the 3 files and point them at each other using OUR structure echo "Fixing imports" sed -i 's/import "types.proto";/import "opentelemetry\/exporter\/prometheus_remote_write\/gen\/types.proto";/' ${SRC_DIR}/remote.proto sed -i 's/import "gogoproto\/gogo.proto";/import "opentelemetry\/exporter\/prometheus_remote_write\/gen\/gogoproto\/gogo.proto";/' ${SRC_DIR}/remote.proto sed -i 's/import "gogoproto\/gogo.proto";/import "opentelemetry\/exporter\/prometheus_remote_write\/gen\/gogoproto\/gogo.proto";/' ${SRC_DIR}/types.proto # Cleanup the repos echo "Removing clones..." rm -rf protobuf prometheus # Used libprotoc 26 echo "Compiling proto files to Python" protoc -I . --python_out=../src ${SRC_DIR}/gogoproto/gogo.proto ${SRC_DIR}/remote.proto ${SRC_DIR}/types.proto echo "Running formatting on the generated files" ../../../scripts/eachdist.py format --path $PWD/.. pyproject.toml000066400000000000000000000027161527070072300356340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-exporter-prometheus-remote-write" dynamic = ["version"] description = "Prometheus Remote Write Metrics Exporter for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "protobuf >=5.26, <7.0", "requests ~= 2.28", "opentelemetry-api ~= 1.12", "opentelemetry-sdk ~= 1.12", "python-snappy ~= 0.6", ] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/exporter/opentelemetry-exporter-prometheus-remote-write" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/exporter/prometheus_remote_write/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/src/000077500000000000000000000000001527070072300335605ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300363755ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/srcexporter/000077500000000000000000000000001527070072300402455ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetryprometheus_remote_write/000077500000000000000000000000001527070072300452255ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter__init__.py000066400000000000000000000331241527070072300473410ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import re from collections import defaultdict from collections.abc import Mapping, Sequence from itertools import chain import requests import snappy from opentelemetry.exporter.prometheus_remote_write.gen.remote_pb2 import ( # pylint: disable=no-name-in-module WriteRequest, ) from opentelemetry.exporter.prometheus_remote_write.gen.types_pb2 import ( # pylint: disable=no-name-in-module Label, Sample, TimeSeries, ) from opentelemetry.sdk.metrics import ( Counter, ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter, ) from opentelemetry.sdk.metrics import Histogram as ClientHistogram from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Gauge, Histogram, Metric, MetricExporter, MetricExportResult, MetricsData, Sum, ) logger = logging.getLogger(__name__) PROMETHEUS_NAME_REGEX = re.compile(r"^\d|[^\w:]") PROMETHEUS_LABEL_REGEX = re.compile(r"^\d|[^\w]") UNDERSCORE_REGEX = re.compile(r"_+") class PrometheusRemoteWriteMetricsExporter(MetricExporter): """ Prometheus remote write metric exporter for OpenTelemetry. Args: endpoint: url where data will be sent (Required) basic_auth: username and password for authentication (Optional) headers: additional headers for remote write request (Optional) timeout: timeout for remote write requests in seconds, defaults to 30 (Optional) proxies: dict mapping request proxy protocols to proxy urls (Optional) tls_config: configuration for remote write TLS settings (Optional) """ def __init__( self, endpoint: str, basic_auth: dict = None, headers: dict = None, timeout: int = 30, tls_config: dict = None, proxies: dict = None, resources_as_labels: bool = True, preferred_temporality: dict[type, AggregationTemporality] = None, preferred_aggregation: dict = None, ): self.endpoint = endpoint self.basic_auth = basic_auth self.headers = headers self.timeout = timeout self.tls_config = tls_config self.proxies = proxies self.resources_as_labels = resources_as_labels if not preferred_temporality: preferred_temporality = { Counter: AggregationTemporality.CUMULATIVE, UpDownCounter: AggregationTemporality.CUMULATIVE, ClientHistogram: AggregationTemporality.CUMULATIVE, ObservableCounter: AggregationTemporality.CUMULATIVE, ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, } super().__init__(preferred_temporality, preferred_aggregation) @property def endpoint(self): return self._endpoint @endpoint.setter def endpoint(self, endpoint: str): if endpoint == "": raise ValueError("endpoint required") self._endpoint = endpoint @property def basic_auth(self): return self._basic_auth @basic_auth.setter def basic_auth(self, basic_auth: dict): if basic_auth: if "username" not in basic_auth: raise ValueError("username required in basic_auth") if "password_file" in basic_auth: if "password" in basic_auth: raise ValueError("basic_auth cannot contain password and password_file") with open( # pylint: disable=unspecified-encoding basic_auth["password_file"] ) as file: basic_auth["password"] = file.readline().strip() elif "password" not in basic_auth: raise ValueError("password required in basic_auth") self._basic_auth = basic_auth @property def timeout(self): return self._timeout @timeout.setter def timeout(self, timeout: int): if timeout <= 0: raise ValueError("timeout must be greater than 0") self._timeout = timeout @property def tls_config(self): return self._tls_config @tls_config.setter def tls_config(self, tls_config: dict): if tls_config: new_config = {} if "ca_file" in tls_config: new_config["ca_file"] = tls_config["ca_file"] if "cert_file" in tls_config and "key_file" in tls_config: new_config["cert_file"] = tls_config["cert_file"] new_config["key_file"] = tls_config["key_file"] elif "cert_file" in tls_config or "key_file" in tls_config: raise ValueError("tls_config requires both cert_file and key_file") if "insecure_skip_verify" in tls_config: new_config["insecure_skip_verify"] = tls_config["insecure_skip_verify"] self._tls_config = tls_config @property def proxies(self): return self._proxies @proxies.setter def proxies(self, proxies: dict): self._proxies = proxies @property def headers(self): return self._headers @headers.setter def headers(self, headers: dict): self._headers = headers def export( self, metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: if not metrics_data: return MetricExportResult.SUCCESS timeseries = self._translate_data(metrics_data) if not timeseries: logger.error("All records contain unsupported aggregators, export aborted") return MetricExportResult.FAILURE message = self._build_message(timeseries) headers = self._build_headers() return self._send_message(message, headers) def _translate_data(self, data: MetricsData) -> Sequence[TimeSeries]: rw_timeseries = [] for resource_metrics in data.resource_metrics: resource = resource_metrics.resource # OTLP Data model suggests combining some attrs into job/instance # Should we do that here? if self.resources_as_labels: resource_labels = [(n, str(v)) for n, v in resource.attributes.items()] else: resource_labels = [] # Scope name/version probably not too useful from a labeling perspective for scope_metrics in resource_metrics.scope_metrics: for metric in scope_metrics.metrics: rw_timeseries.extend(self._parse_metric(metric, resource_labels)) return rw_timeseries def _parse_metric(self, metric: Metric, resource_labels: Sequence) -> Sequence[TimeSeries]: """ Parses the Metric & lower objects, then converts the output into OM TimeSeries. Returns a List of TimeSeries objects based on one Metric """ # Create the metric name, will be a label later if metric.unit: # Prom. naming guidelines add unit to the name name = f"{metric.name}_{metric.unit}" else: name = metric.name # datapoints have attributes associated with them. these would be sent # to RW as different metrics: name & labels is a unique time series sample_sets = defaultdict(list) if isinstance(metric.data, (Gauge, Sum)): for dp in metric.data.data_points: attrs, sample = self._parse_data_point(dp, name) sample_sets[attrs].append(sample) elif isinstance(metric.data, Histogram): for dp in metric.data.data_points: dp_result = self._parse_histogram_data_point(dp, name) for attrs, sample in dp_result: sample_sets[attrs].append(sample) else: logger.warning("Unsupported Metric Type: %s", type(metric.data)) return [] return self._convert_to_timeseries(sample_sets, resource_labels) def _convert_to_timeseries( self, sample_sets: Mapping[tuple, Sequence], resource_labels: Sequence ) -> Sequence[TimeSeries]: timeseries = [] for labels, samples in sample_sets.items(): ts = TimeSeries() for label_name, label_value in sorted(chain(resource_labels, labels)): # Previous implementation did not str() the names... ts.labels.append(self._label(label_name, str(label_value))) for value, timestamp in samples: ts.samples.append(self._sample(value, timestamp)) timeseries.append(ts) return timeseries @staticmethod def _sample(value: int, timestamp: int) -> Sample: sample = Sample() sample.value = value sample.timestamp = timestamp return sample def _label(self, name: str, value: str) -> Label: label = Label() label.name = self._sanitize_string(name, "label") label.value = value return label @staticmethod def _sanitize_string(string: str, type_: str) -> str: # I Think Prometheus requires names to NOT start with a number this # would not catch that, but do cover the other cases. The naming rules # don't explicit say this, but the supplied regex implies it. # Got a little weird trying to do substitution with it, but can be # fixed if we allow numeric beginnings to metric names if type_ == "name": sanitized = PROMETHEUS_NAME_REGEX.sub("_", string) elif type_ == "label": sanitized = PROMETHEUS_LABEL_REGEX.sub("_", string) else: raise TypeError(f"Unsupported string type: {type_}") # Remove consecutive underscores # TODO: Unfortunately this clobbbers __name__ # sanitized = UNDERSCORE_REGEX.sub("_",sanitized) return sanitized def _parse_histogram_data_point(self, data_point, name): sample_attr_pairs = [] base_attrs = list(data_point.attributes.items()) timestamp = data_point.time_unix_nano // 1_000_000 def handle_bucket(value, bound=None, name_override=None): # Metric Level attributes + the bucket boundary attribute + name ts_attrs = base_attrs.copy() ts_attrs.append( ( "__name__", self._sanitize_string(name_override or name, "name"), ) ) if bound: ts_attrs.append(("le", str(bound))) # Value is count of values in each bucket ts_sample = (value, timestamp) return tuple(ts_attrs), ts_sample for bound_pos, bound in enumerate(data_point.explicit_bounds): sample_attr_pairs.append(handle_bucket(data_point.bucket_counts[bound_pos], bound)) # Add the last label for implicit +inf bucket sample_attr_pairs.append(handle_bucket(data_point.bucket_counts[-1], bound="+Inf")) # Lastly, add series for count & sum sample_attr_pairs.append(handle_bucket(data_point.sum, name_override=f"{name}_sum")) sample_attr_pairs.append(handle_bucket(data_point.count, name_override=f"{name}_count")) return sample_attr_pairs def _parse_data_point(self, data_point, name=None): attrs = tuple(data_point.attributes.items()) + (("__name__", self._sanitize_string(name, "name")),) sample = (data_point.value, (data_point.time_unix_nano // 1_000_000)) return attrs, sample @staticmethod def _build_message(timeseries: Sequence[TimeSeries]) -> bytes: write_request = WriteRequest() write_request.timeseries.extend(timeseries) serialized_message = write_request.SerializeToString() return snappy.compress(serialized_message) def _build_headers(self) -> dict: headers = { "Content-Encoding": "snappy", "Content-Type": "application/x-protobuf", "X-Prometheus-Remote-Write-Version": "0.1.0", } if self.headers: for header_name, header_value in self.headers.items(): headers[header_name] = header_value return headers def _send_message(self, message: bytes, headers: dict) -> MetricExportResult: auth = None if self.basic_auth: auth = (self.basic_auth["username"], self.basic_auth["password"]) cert = None verify = True if self.tls_config: if "ca_file" in self.tls_config: verify = self.tls_config["ca_file"] elif "insecure_skip_verify" in self.tls_config: verify = self.tls_config["insecure_skip_verify"] if "cert_file" in self.tls_config and "key_file" in self.tls_config: cert = ( self.tls_config["cert_file"], self.tls_config["key_file"], ) try: response = requests.post( self.endpoint, data=message, headers=headers, auth=auth, timeout=self.timeout, proxies=self.proxies, cert=cert, verify=verify, ) if not response.ok: response.raise_for_status() except requests.exceptions.RequestException as err: logger.error("Export POST request failed with reason: %s", err) return MetricExportResult.FAILURE return MetricExportResult.SUCCESS def force_flush(self, timeout_millis: float = 10_000) -> bool: return True def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass gen/000077500000000000000000000000001527070072300457765ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_writegogoproto/000077500000000000000000000000001527070072300500155ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gengogoproto/gogo_pb2.py000066400000000000000000000172521527070072300520740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/exporter/prometheus_remote_write/gen/gogoproto/gogo.proto # Protobuf Python Version: 5.26.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\nGopentelemetry/exporter/prometheus_remote_write/gen/gogoproto/gogo.proto\x12\tgogoproto\x1a google/protobuf/descriptor.proto:;\n\x13goproto_enum_prefix\x12\x1c.google.protobuf.EnumOptions\x18\xb1\xe4\x03 \x01(\x08:=\n\x15goproto_enum_stringer\x12\x1c.google.protobuf.EnumOptions\x18\xc5\xe4\x03 \x01(\x08:5\n\renum_stringer\x12\x1c.google.protobuf.EnumOptions\x18\xc6\xe4\x03 \x01(\x08:7\n\x0f\x65num_customname\x12\x1c.google.protobuf.EnumOptions\x18\xc7\xe4\x03 \x01(\t:0\n\x08\x65numdecl\x12\x1c.google.protobuf.EnumOptions\x18\xc8\xe4\x03 \x01(\x08:A\n\x14\x65numvalue_customname\x12!.google.protobuf.EnumValueOptions\x18\xd1\x83\x04 \x01(\t:;\n\x13goproto_getters_all\x12\x1c.google.protobuf.FileOptions\x18\x99\xec\x03 \x01(\x08:?\n\x17goproto_enum_prefix_all\x12\x1c.google.protobuf.FileOptions\x18\x9a\xec\x03 \x01(\x08:<\n\x14goproto_stringer_all\x12\x1c.google.protobuf.FileOptions\x18\x9b\xec\x03 \x01(\x08:9\n\x11verbose_equal_all\x12\x1c.google.protobuf.FileOptions\x18\x9c\xec\x03 \x01(\x08:0\n\x08\x66\x61\x63\x65_all\x12\x1c.google.protobuf.FileOptions\x18\x9d\xec\x03 \x01(\x08:4\n\x0cgostring_all\x12\x1c.google.protobuf.FileOptions\x18\x9e\xec\x03 \x01(\x08:4\n\x0cpopulate_all\x12\x1c.google.protobuf.FileOptions\x18\x9f\xec\x03 \x01(\x08:4\n\x0cstringer_all\x12\x1c.google.protobuf.FileOptions\x18\xa0\xec\x03 \x01(\x08:3\n\x0bonlyone_all\x12\x1c.google.protobuf.FileOptions\x18\xa1\xec\x03 \x01(\x08:1\n\tequal_all\x12\x1c.google.protobuf.FileOptions\x18\xa5\xec\x03 \x01(\x08:7\n\x0f\x64\x65scription_all\x12\x1c.google.protobuf.FileOptions\x18\xa6\xec\x03 \x01(\x08:3\n\x0btestgen_all\x12\x1c.google.protobuf.FileOptions\x18\xa7\xec\x03 \x01(\x08:4\n\x0c\x62\x65nchgen_all\x12\x1c.google.protobuf.FileOptions\x18\xa8\xec\x03 \x01(\x08:5\n\rmarshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xa9\xec\x03 \x01(\x08:7\n\x0funmarshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xaa\xec\x03 \x01(\x08:<\n\x14stable_marshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xab\xec\x03 \x01(\x08:1\n\tsizer_all\x12\x1c.google.protobuf.FileOptions\x18\xac\xec\x03 \x01(\x08:A\n\x19goproto_enum_stringer_all\x12\x1c.google.protobuf.FileOptions\x18\xad\xec\x03 \x01(\x08:9\n\x11\x65num_stringer_all\x12\x1c.google.protobuf.FileOptions\x18\xae\xec\x03 \x01(\x08:<\n\x14unsafe_marshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xaf\xec\x03 \x01(\x08:>\n\x16unsafe_unmarshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xb0\xec\x03 \x01(\x08:B\n\x1agoproto_extensions_map_all\x12\x1c.google.protobuf.FileOptions\x18\xb1\xec\x03 \x01(\x08:@\n\x18goproto_unrecognized_all\x12\x1c.google.protobuf.FileOptions\x18\xb2\xec\x03 \x01(\x08:8\n\x10gogoproto_import\x12\x1c.google.protobuf.FileOptions\x18\xb3\xec\x03 \x01(\x08:6\n\x0eprotosizer_all\x12\x1c.google.protobuf.FileOptions\x18\xb4\xec\x03 \x01(\x08:3\n\x0b\x63ompare_all\x12\x1c.google.protobuf.FileOptions\x18\xb5\xec\x03 \x01(\x08:4\n\x0ctypedecl_all\x12\x1c.google.protobuf.FileOptions\x18\xb6\xec\x03 \x01(\x08:4\n\x0c\x65numdecl_all\x12\x1c.google.protobuf.FileOptions\x18\xb7\xec\x03 \x01(\x08:<\n\x14goproto_registration\x12\x1c.google.protobuf.FileOptions\x18\xb8\xec\x03 \x01(\x08:7\n\x0fmessagename_all\x12\x1c.google.protobuf.FileOptions\x18\xb9\xec\x03 \x01(\x08:=\n\x15goproto_sizecache_all\x12\x1c.google.protobuf.FileOptions\x18\xba\xec\x03 \x01(\x08:;\n\x13goproto_unkeyed_all\x12\x1c.google.protobuf.FileOptions\x18\xbb\xec\x03 \x01(\x08::\n\x0fgoproto_getters\x12\x1f.google.protobuf.MessageOptions\x18\x81\xf4\x03 \x01(\x08:;\n\x10goproto_stringer\x12\x1f.google.protobuf.MessageOptions\x18\x83\xf4\x03 \x01(\x08:8\n\rverbose_equal\x12\x1f.google.protobuf.MessageOptions\x18\x84\xf4\x03 \x01(\x08:/\n\x04\x66\x61\x63\x65\x12\x1f.google.protobuf.MessageOptions\x18\x85\xf4\x03 \x01(\x08:3\n\x08gostring\x12\x1f.google.protobuf.MessageOptions\x18\x86\xf4\x03 \x01(\x08:3\n\x08populate\x12\x1f.google.protobuf.MessageOptions\x18\x87\xf4\x03 \x01(\x08:3\n\x08stringer\x12\x1f.google.protobuf.MessageOptions\x18\xc0\x8b\x04 \x01(\x08:2\n\x07onlyone\x12\x1f.google.protobuf.MessageOptions\x18\x89\xf4\x03 \x01(\x08:0\n\x05\x65qual\x12\x1f.google.protobuf.MessageOptions\x18\x8d\xf4\x03 \x01(\x08:6\n\x0b\x64\x65scription\x12\x1f.google.protobuf.MessageOptions\x18\x8e\xf4\x03 \x01(\x08:2\n\x07testgen\x12\x1f.google.protobuf.MessageOptions\x18\x8f\xf4\x03 \x01(\x08:3\n\x08\x62\x65nchgen\x12\x1f.google.protobuf.MessageOptions\x18\x90\xf4\x03 \x01(\x08:4\n\tmarshaler\x12\x1f.google.protobuf.MessageOptions\x18\x91\xf4\x03 \x01(\x08:6\n\x0bunmarshaler\x12\x1f.google.protobuf.MessageOptions\x18\x92\xf4\x03 \x01(\x08:;\n\x10stable_marshaler\x12\x1f.google.protobuf.MessageOptions\x18\x93\xf4\x03 \x01(\x08:0\n\x05sizer\x12\x1f.google.protobuf.MessageOptions\x18\x94\xf4\x03 \x01(\x08:;\n\x10unsafe_marshaler\x12\x1f.google.protobuf.MessageOptions\x18\x97\xf4\x03 \x01(\x08:=\n\x12unsafe_unmarshaler\x12\x1f.google.protobuf.MessageOptions\x18\x98\xf4\x03 \x01(\x08:A\n\x16goproto_extensions_map\x12\x1f.google.protobuf.MessageOptions\x18\x99\xf4\x03 \x01(\x08:?\n\x14goproto_unrecognized\x12\x1f.google.protobuf.MessageOptions\x18\x9a\xf4\x03 \x01(\x08:5\n\nprotosizer\x12\x1f.google.protobuf.MessageOptions\x18\x9c\xf4\x03 \x01(\x08:2\n\x07\x63ompare\x12\x1f.google.protobuf.MessageOptions\x18\x9d\xf4\x03 \x01(\x08:3\n\x08typedecl\x12\x1f.google.protobuf.MessageOptions\x18\x9e\xf4\x03 \x01(\x08:6\n\x0bmessagename\x12\x1f.google.protobuf.MessageOptions\x18\xa1\xf4\x03 \x01(\x08:<\n\x11goproto_sizecache\x12\x1f.google.protobuf.MessageOptions\x18\xa2\xf4\x03 \x01(\x08::\n\x0fgoproto_unkeyed\x12\x1f.google.protobuf.MessageOptions\x18\xa3\xf4\x03 \x01(\x08:1\n\x08nullable\x12\x1d.google.protobuf.FieldOptions\x18\xe9\xfb\x03 \x01(\x08:.\n\x05\x65mbed\x12\x1d.google.protobuf.FieldOptions\x18\xea\xfb\x03 \x01(\x08:3\n\ncustomtype\x12\x1d.google.protobuf.FieldOptions\x18\xeb\xfb\x03 \x01(\t:3\n\ncustomname\x12\x1d.google.protobuf.FieldOptions\x18\xec\xfb\x03 \x01(\t:0\n\x07jsontag\x12\x1d.google.protobuf.FieldOptions\x18\xed\xfb\x03 \x01(\t:1\n\x08moretags\x12\x1d.google.protobuf.FieldOptions\x18\xee\xfb\x03 \x01(\t:1\n\x08\x63\x61sttype\x12\x1d.google.protobuf.FieldOptions\x18\xef\xfb\x03 \x01(\t:0\n\x07\x63\x61stkey\x12\x1d.google.protobuf.FieldOptions\x18\xf0\xfb\x03 \x01(\t:2\n\tcastvalue\x12\x1d.google.protobuf.FieldOptions\x18\xf1\xfb\x03 \x01(\t:0\n\x07stdtime\x12\x1d.google.protobuf.FieldOptions\x18\xf2\xfb\x03 \x01(\x08:4\n\x0bstdduration\x12\x1d.google.protobuf.FieldOptions\x18\xf3\xfb\x03 \x01(\x08:3\n\nwktpointer\x12\x1d.google.protobuf.FieldOptions\x18\xf4\xfb\x03 \x01(\x08\x42\x45\n\x13\x63om.google.protobufB\nGoGoProtosZ\"github.com/gogo/protobuf/gogoproto') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.exporter.prometheus_remote_write.gen.gogoproto.gogo_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\023com.google.protobufB\nGoGoProtosZ\"github.com/gogo/protobuf/gogoproto' # @@protoc_insertion_point(module_scope) remote_pb2.py000066400000000000000000000072601527070072300504130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/exporter/prometheus_remote_write/gen/remote.proto # Protobuf Python Version: 5.26.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() from opentelemetry.exporter.prometheus_remote_write.gen import types_pb2 as opentelemetry_dot_exporter_dot_prometheus__remote__write_dot_gen_dot_types__pb2 from opentelemetry.exporter.prometheus_remote_write.gen.gogoproto import gogo_pb2 as opentelemetry_dot_exporter_dot_prometheus__remote__write_dot_gen_dot_gogoproto_dot_gogo__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n?opentelemetry/exporter/prometheus_remote_write/gen/remote.proto\x12\nprometheus\x1a>opentelemetry/exporter/prometheus_remote_write/gen/types.proto\x1aGopentelemetry/exporter/prometheus_remote_write/gen/gogoproto/gogo.proto\"z\n\x0cWriteRequest\x12\x30\n\ntimeseries\x18\x01 \x03(\x0b\x32\x16.prometheus.TimeSeriesB\x04\xc8\xde\x1f\x00\x12\x32\n\x08metadata\x18\x03 \x03(\x0b\x32\x1a.prometheus.MetricMetadataB\x04\xc8\xde\x1f\x00J\x04\x08\x02\x10\x03\"\xae\x01\n\x0bReadRequest\x12\"\n\x07queries\x18\x01 \x03(\x0b\x32\x11.prometheus.Query\x12\x45\n\x17\x61\x63\x63\x65pted_response_types\x18\x02 \x03(\x0e\x32$.prometheus.ReadRequest.ResponseType\"4\n\x0cResponseType\x12\x0b\n\x07SAMPLES\x10\x00\x12\x17\n\x13STREAMED_XOR_CHUNKS\x10\x01\"8\n\x0cReadResponse\x12(\n\x07results\x18\x01 \x03(\x0b\x32\x17.prometheus.QueryResult\"\x8f\x01\n\x05Query\x12\x1a\n\x12start_timestamp_ms\x18\x01 \x01(\x03\x12\x18\n\x10\x65nd_timestamp_ms\x18\x02 \x01(\x03\x12*\n\x08matchers\x18\x03 \x03(\x0b\x32\x18.prometheus.LabelMatcher\x12$\n\x05hints\x18\x04 \x01(\x0b\x32\x15.prometheus.ReadHints\"9\n\x0bQueryResult\x12*\n\ntimeseries\x18\x01 \x03(\x0b\x32\x16.prometheus.TimeSeries\"]\n\x13\x43hunkedReadResponse\x12\x31\n\x0e\x63hunked_series\x18\x01 \x03(\x0b\x32\x19.prometheus.ChunkedSeries\x12\x13\n\x0bquery_index\x18\x02 \x01(\x03\x42\x08Z\x06prompbb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.exporter.prometheus_remote_write.gen.remote_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'Z\006prompb' _globals['_WRITEREQUEST'].fields_by_name['timeseries']._loaded_options = None _globals['_WRITEREQUEST'].fields_by_name['timeseries']._serialized_options = b'\310\336\037\000' _globals['_WRITEREQUEST'].fields_by_name['metadata']._loaded_options = None _globals['_WRITEREQUEST'].fields_by_name['metadata']._serialized_options = b'\310\336\037\000' _globals['_WRITEREQUEST']._serialized_start=216 _globals['_WRITEREQUEST']._serialized_end=338 _globals['_READREQUEST']._serialized_start=341 _globals['_READREQUEST']._serialized_end=515 _globals['_READREQUEST_RESPONSETYPE']._serialized_start=463 _globals['_READREQUEST_RESPONSETYPE']._serialized_end=515 _globals['_READRESPONSE']._serialized_start=517 _globals['_READRESPONSE']._serialized_end=573 _globals['_QUERY']._serialized_start=576 _globals['_QUERY']._serialized_end=719 _globals['_QUERYRESULT']._serialized_start=721 _globals['_QUERYRESULT']._serialized_end=778 _globals['_CHUNKEDREADRESPONSE']._serialized_start=780 _globals['_CHUNKEDREADRESPONSE']._serialized_end=873 # @@protoc_insertion_point(module_scope) types_pb2.py000066400000000000000000000136341527070072300502660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/exporter/prometheus_remote_write/gen/types.proto # Protobuf Python Version: 5.26.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() from opentelemetry.exporter.prometheus_remote_write.gen.gogoproto import gogo_pb2 as opentelemetry_dot_exporter_dot_prometheus__remote__write_dot_gen_dot_gogoproto_dot_gogo__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n>opentelemetry/exporter/prometheus_remote_write/gen/types.proto\x12\nprometheus\x1aGopentelemetry/exporter/prometheus_remote_write/gen/gogoproto/gogo.proto\"\xf8\x01\n\x0eMetricMetadata\x12\x33\n\x04type\x18\x01 \x01(\x0e\x32%.prometheus.MetricMetadata.MetricType\x12\x1a\n\x12metric_family_name\x18\x02 \x01(\t\x12\x0c\n\x04help\x18\x04 \x01(\t\x12\x0c\n\x04unit\x18\x05 \x01(\t\"y\n\nMetricType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0b\n\x07\x43OUNTER\x10\x01\x12\t\n\x05GAUGE\x10\x02\x12\r\n\tHISTOGRAM\x10\x03\x12\x12\n\x0eGAUGEHISTOGRAM\x10\x04\x12\x0b\n\x07SUMMARY\x10\x05\x12\x08\n\x04INFO\x10\x06\x12\x0c\n\x08STATESET\x10\x07\"*\n\x06Sample\x12\r\n\x05value\x18\x01 \x01(\x01\x12\x11\n\ttimestamp\x18\x02 \x01(\x03\"U\n\x08\x45xemplar\x12\'\n\x06labels\x18\x01 \x03(\x0b\x32\x11.prometheus.LabelB\x04\xc8\xde\x1f\x00\x12\r\n\x05value\x18\x02 \x01(\x01\x12\x11\n\ttimestamp\x18\x03 \x01(\x03\"\x8f\x01\n\nTimeSeries\x12\'\n\x06labels\x18\x01 \x03(\x0b\x32\x11.prometheus.LabelB\x04\xc8\xde\x1f\x00\x12)\n\x07samples\x18\x02 \x03(\x0b\x32\x12.prometheus.SampleB\x04\xc8\xde\x1f\x00\x12-\n\texemplars\x18\x03 \x03(\x0b\x32\x14.prometheus.ExemplarB\x04\xc8\xde\x1f\x00\"$\n\x05Label\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"1\n\x06Labels\x12\'\n\x06labels\x18\x01 \x03(\x0b\x32\x11.prometheus.LabelB\x04\xc8\xde\x1f\x00\"\x82\x01\n\x0cLabelMatcher\x12+\n\x04type\x18\x01 \x01(\x0e\x32\x1d.prometheus.LabelMatcher.Type\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\t\"(\n\x04Type\x12\x06\n\x02\x45Q\x10\x00\x12\x07\n\x03NEQ\x10\x01\x12\x06\n\x02RE\x10\x02\x12\x07\n\x03NRE\x10\x03\"|\n\tReadHints\x12\x0f\n\x07step_ms\x18\x01 \x01(\x03\x12\x0c\n\x04\x66unc\x18\x02 \x01(\t\x12\x10\n\x08start_ms\x18\x03 \x01(\x03\x12\x0e\n\x06\x65nd_ms\x18\x04 \x01(\x03\x12\x10\n\x08grouping\x18\x05 \x03(\t\x12\n\n\x02\x62y\x18\x06 \x01(\x08\x12\x10\n\x08range_ms\x18\x07 \x01(\x03\"\x8b\x01\n\x05\x43hunk\x12\x13\n\x0bmin_time_ms\x18\x01 \x01(\x03\x12\x13\n\x0bmax_time_ms\x18\x02 \x01(\x03\x12(\n\x04type\x18\x03 \x01(\x0e\x32\x1a.prometheus.Chunk.Encoding\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\" \n\x08\x45ncoding\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x07\n\x03XOR\x10\x01\"a\n\rChunkedSeries\x12\'\n\x06labels\x18\x01 \x03(\x0b\x32\x11.prometheus.LabelB\x04\xc8\xde\x1f\x00\x12\'\n\x06\x63hunks\x18\x02 \x03(\x0b\x32\x11.prometheus.ChunkB\x04\xc8\xde\x1f\x00\x42\x08Z\x06prompbb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.exporter.prometheus_remote_write.gen.types_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'Z\006prompb' _globals['_EXEMPLAR'].fields_by_name['labels']._loaded_options = None _globals['_EXEMPLAR'].fields_by_name['labels']._serialized_options = b'\310\336\037\000' _globals['_TIMESERIES'].fields_by_name['labels']._loaded_options = None _globals['_TIMESERIES'].fields_by_name['labels']._serialized_options = b'\310\336\037\000' _globals['_TIMESERIES'].fields_by_name['samples']._loaded_options = None _globals['_TIMESERIES'].fields_by_name['samples']._serialized_options = b'\310\336\037\000' _globals['_TIMESERIES'].fields_by_name['exemplars']._loaded_options = None _globals['_TIMESERIES'].fields_by_name['exemplars']._serialized_options = b'\310\336\037\000' _globals['_LABELS'].fields_by_name['labels']._loaded_options = None _globals['_LABELS'].fields_by_name['labels']._serialized_options = b'\310\336\037\000' _globals['_CHUNKEDSERIES'].fields_by_name['labels']._loaded_options = None _globals['_CHUNKEDSERIES'].fields_by_name['labels']._serialized_options = b'\310\336\037\000' _globals['_CHUNKEDSERIES'].fields_by_name['chunks']._loaded_options = None _globals['_CHUNKEDSERIES'].fields_by_name['chunks']._serialized_options = b'\310\336\037\000' _globals['_METRICMETADATA']._serialized_start=152 _globals['_METRICMETADATA']._serialized_end=400 _globals['_METRICMETADATA_METRICTYPE']._serialized_start=279 _globals['_METRICMETADATA_METRICTYPE']._serialized_end=400 _globals['_SAMPLE']._serialized_start=402 _globals['_SAMPLE']._serialized_end=444 _globals['_EXEMPLAR']._serialized_start=446 _globals['_EXEMPLAR']._serialized_end=531 _globals['_TIMESERIES']._serialized_start=534 _globals['_TIMESERIES']._serialized_end=677 _globals['_LABEL']._serialized_start=679 _globals['_LABEL']._serialized_end=715 _globals['_LABELS']._serialized_start=717 _globals['_LABELS']._serialized_end=766 _globals['_LABELMATCHER']._serialized_start=769 _globals['_LABELMATCHER']._serialized_end=899 _globals['_LABELMATCHER_TYPE']._serialized_start=859 _globals['_LABELMATCHER_TYPE']._serialized_end=899 _globals['_READHINTS']._serialized_start=901 _globals['_READHINTS']._serialized_end=1025 _globals['_CHUNK']._serialized_start=1028 _globals['_CHUNK']._serialized_end=1167 _globals['_CHUNK_ENCODING']._serialized_start=1135 _globals['_CHUNK_ENCODING']._serialized_end=1167 _globals['_CHUNKEDSERIES']._serialized_start=1169 _globals['_CHUNKEDSERIES']._serialized_end=1266 # @@protoc_insertion_point(module_scope) version.py000066400000000000000000000001441527070072300472630ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000005721527070072300371570ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-writeasgiref==3.8.1 certifi==2024.7.4 charset-normalizer==3.3.2 cramjam==2.11.0 Deprecated==1.2.14 idna==3.7 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 protobuf==6.33.5 py-cpuinfo==9.0.0 pytest==7.4.4 python-snappy==0.7.1 requests==2.33.1 tomli==2.0.1 typing_extensions==4.12.2 urllib3==2.7.0 wrapt==1.16.0 zipp==3.19.2 -e exporter/opentelemetry-exporter-prometheus-remote-write opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/tests/000077500000000000000000000000001527070072300341335ustar00rootroot00000000000000__init__.py000066400000000000000000000001141527070072300361610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 conftest.py000066400000000000000000000030531527070072300362540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import random import pytest import opentelemetry.test.metrictestutil as metric_util # pylint: disable=no-name-in-module from opentelemetry.exporter.prometheus_remote_write import ( PrometheusRemoteWriteMetricsExporter, ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Histogram, HistogramDataPoint, Metric, ) @pytest.fixture def prom_rw(): return PrometheusRemoteWriteMetricsExporter("http://victoria:8428/api/v1/write") @pytest.fixture def metric(request): if hasattr(request, "param"): type_ = request.param else: type_ = random.choice(["gauge", "sum"]) if type_ == "gauge": return metric_util._generate_gauge("test.gauge", random.randint(0, 100)) if type_ == "sum": return metric_util._generate_sum("test.sum", random.randint(0, 9_999_999_999)) if type_ == "histogram": return _generate_histogram("test_histogram") raise ValueError(f"Unsupported metric type '{type_}'.") def _generate_histogram(name): dp = HistogramDataPoint( attributes={"foo": "bar", "baz": 42}, start_time_unix_nano=1641946016139533244, time_unix_nano=1641946016139533244, count=5, sum=420, bucket_counts=[1, 4], explicit_bounds=[10.0], min=8, max=80, ) data = Histogram( [dp], AggregationTemporality.CUMULATIVE, ) return Metric( name, "foo", "tu", data=data, ) test_prometheus_remote_write_exporter.py000066400000000000000000000256611527070072300444070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-prometheus-remote-write/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from unittest.mock import patch import pytest # pylint: disable=no-name-in-module from opentelemetry.exporter.prometheus_remote_write import ( PrometheusRemoteWriteMetricsExporter, ) from opentelemetry.exporter.prometheus_remote_write.gen.types_pb2 import ( # pylint: disable=E0611 Label, Sample, TimeSeries, ) from opentelemetry.sdk.metrics.export import ( Histogram, HistogramDataPoint, MetricExportResult, MetricsData, NumberDataPoint, ResourceMetrics, ScopeMetrics, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope @pytest.mark.parametrize( "name,result", [ ("abc.124", "abc_124"), (":abc", ":abc"), ("abc.name.hi", "abc_name_hi"), ("service.name...", "service_name___"), ("4hellowor:ld5∂©∑", "_hellowor:ld5___"), ], ) def test_regex(name, result, prom_rw): assert prom_rw._sanitize_string(name, "name") == result def test_regex_invalid(prom_rw): with pytest.raises(TypeError): prom_rw("foo_bar", "A random type") def test_parse_data_point(prom_rw): attrs = {"Foo": "Bar", "Baz": 42} timestamp = 1641946016139533244 value = 242.42 dp = NumberDataPoint(attrs, 0, timestamp, value) name = "abc.123_42" labels, sample = prom_rw._parse_data_point(dp, name) name = "abc_123_42" assert labels == (("Foo", "Bar"), ("Baz", 42), ("__name__", name)) assert sample == (value, timestamp // 1_000_000) def test_parse_histogram_dp(prom_rw): attrs = {"foo": "bar", "baz": 42} timestamp = 1641946016139533244 bounds = [10.0, 20.0] dp = HistogramDataPoint( attributes=attrs, start_time_unix_nano=1641946016139533244, time_unix_nano=timestamp, count=9, sum=180, bucket_counts=[1, 4, 4], explicit_bounds=bounds, min=8, max=80, ) name = "foo_histogram" label_sample_pairs = prom_rw._parse_histogram_data_point(dp, name) timestamp = timestamp // 1_000_000 bounds.append("+Inf") for pos, bound in enumerate(bounds): # We have to attributes, we kinda assume the bucket label is last... assert ("le", str(bound)) == label_sample_pairs[pos][0][-1] # Check and make sure we are putting the bucket counts in there assert (dp.bucket_counts[pos], timestamp) == label_sample_pairs[pos][1] # Last two are the sum & total count assert ("__name__", f"{name}_sum") in label_sample_pairs[-2][0] assert (dp.sum, timestamp) == label_sample_pairs[-2][1] assert ("__name__", f"{name}_count") in label_sample_pairs[-1][0] assert (dp.count, timestamp) == label_sample_pairs[-1][1] @pytest.mark.parametrize( "metric", [ "gauge", "sum", "histogram", ], indirect=["metric"], ) def test_parse_metric(metric, prom_rw): """ Ensures output from parse_metrics are TimeSeries with expected data/size """ attributes = { "service_name": "foo", "bool_value": True, } assert len(metric.data.data_points) == 1, "We can only support a single datapoint in tests" series = prom_rw._parse_metric(metric, tuple(attributes.items())) timestamp = metric.data.data_points[0].time_unix_nano // 1_000_000 for single_series in series: labels = str(single_series.labels) # Its a bit easier to validate these stringified where we dont have to # worry about ordering and protobuf TimeSeries object structure # This doesn't guarantee the labels aren't mixed up, but our other # test cases already do. assert "__name__" in labels assert prom_rw._sanitize_string(metric.name, "name") in labels combined_attrs = list(attributes.items()) + list(metric.data.data_points[0].attributes.items()) for name, value in combined_attrs: assert prom_rw._sanitize_string(name, "label") in labels assert str(value) in labels if isinstance(metric.data, Histogram): values = [ metric.data.data_points[0].count, metric.data.data_points[0].sum, metric.data.data_points[0].bucket_counts[0], metric.data.data_points[0].bucket_counts[1], ] else: values = [ metric.data.data_points[0].value, ] for sample in single_series.samples: assert sample.timestamp == timestamp assert sample.value in values def test_convert_to_timeseries(prom_rw): resource_labels = (("service_name", "foo"), ("bool_value", True)) sample_sets = { (("foo", "bar"), ("baz", 42), ("__name__", "test_histogram_tu")): [(1, 1641946016139)], (("baz", "42"), ("foo", "bar")): [(4, 1641946016139)], } timeseries = prom_rw._convert_to_timeseries(sample_sets, resource_labels) assert timeseries == [ TimeSeries( labels=[ Label(name="__name__", value="test_histogram_tu"), Label(name="baz", value="42"), Label(name="bool_value", value="True"), Label(name="foo", value="bar"), Label(name="service_name", value="foo"), ], samples=[Sample(value=1, timestamp=1641946016139)], ), TimeSeries( labels=[ Label(name="baz", value="42"), Label(name="bool_value", value="True"), Label(name="foo", value="bar"), Label(name="service_name", value="foo"), ], samples=[Sample(value=4, timestamp=1641946016139)], ), ] class TestValidation(unittest.TestCase): # Test cases to ensure exporter parameter validation works as intended def test_valid_standard_param(self): exporter = PrometheusRemoteWriteMetricsExporter( endpoint="/prom/test_endpoint", ) self.assertEqual(exporter.endpoint, "/prom/test_endpoint") def test_valid_basic_auth_param(self): exporter = PrometheusRemoteWriteMetricsExporter( endpoint="/prom/test_endpoint", basic_auth={ "username": "test_username", "password": "test_password", }, ) self.assertEqual(exporter.basic_auth["username"], "test_username") self.assertEqual(exporter.basic_auth["password"], "test_password") def test_invalid_no_endpoint_param(self): with self.assertRaises(ValueError): PrometheusRemoteWriteMetricsExporter("") def test_invalid_no_username_param(self): with self.assertRaises(ValueError): PrometheusRemoteWriteMetricsExporter( endpoint="/prom/test_endpoint", basic_auth={"password": "test_password"}, ) def test_invalid_no_password_param(self): with self.assertRaises(ValueError): PrometheusRemoteWriteMetricsExporter( endpoint="/prom/test_endpoint", basic_auth={"username": "test_username"}, ) def test_invalid_conflicting_passwords_param(self): with self.assertRaises(ValueError): PrometheusRemoteWriteMetricsExporter( endpoint="/prom/test_endpoint", basic_auth={ "username": "test_username", "password": "test_password", "password_file": "test_file", }, ) def test_invalid_timeout_param(self): with self.assertRaises(ValueError): PrometheusRemoteWriteMetricsExporter(endpoint="/prom/test_endpoint", timeout=0) def test_valid_tls_config_param(self): tls_config = { "ca_file": "test_ca_file", "cert_file": "test_cert_file", "key_file": "test_key_file", "insecure_skip_verify": True, } exporter = PrometheusRemoteWriteMetricsExporter(endpoint="/prom/test_endpoint", tls_config=tls_config) self.assertEqual(exporter.tls_config["ca_file"], tls_config["ca_file"]) self.assertEqual(exporter.tls_config["cert_file"], tls_config["cert_file"]) self.assertEqual(exporter.tls_config["key_file"], tls_config["key_file"]) self.assertEqual( exporter.tls_config["insecure_skip_verify"], tls_config["insecure_skip_verify"], ) # if cert_file is provided, then key_file must also be provided def test_invalid_tls_config_cert_only_param(self): tls_config = {"cert_file": "value"} with self.assertRaises(ValueError): PrometheusRemoteWriteMetricsExporter(endpoint="/prom/test_endpoint", tls_config=tls_config) # if cert_file is provided, then key_file must also be provided def test_invalid_tls_config_key_only_param(self): tls_config = {"cert_file": "value"} with self.assertRaises(ValueError): PrometheusRemoteWriteMetricsExporter(endpoint="/prom/test_endpoint", tls_config=tls_config) # Ensures export is successful with valid export_records and config @patch("requests.post") def test_valid_export(mock_post, prom_rw, metric): mock_post.return_value.configure_mock(status_code=200) # Assumed a "None" for Scope or Resource aren't valid, so build them here scope = ScopeMetrics(InstrumentationScope(name="prom-rw-test"), [metric], None) resource = ResourceMetrics(Resource({"service.name": "foo"}), [scope], None) record = MetricsData([resource]) result = prom_rw.export(record) assert result == MetricExportResult.SUCCESS assert mock_post.call_count == 1 result = prom_rw.export([]) assert result == MetricExportResult.SUCCESS def test_invalid_export(prom_rw): record = MetricsData([]) result = prom_rw.export(record) assert result == MetricExportResult.FAILURE @patch("requests.post") def test_valid_send_message(mock_post, prom_rw): mock_post.return_value.configure_mock(ok=True) result = prom_rw._send_message(b"", {}) assert mock_post.call_count == 1 assert result == MetricExportResult.SUCCESS def test_invalid_send_message(prom_rw): result = prom_rw._send_message(b"", {}) assert result == MetricExportResult.FAILURE # Verifies that build_message calls snappy.compress and returns SerializedString @patch("snappy.compress", return_value=b"") def test_build_message(mock_compress, prom_rw): message = prom_rw._build_message([TimeSeries()]) assert mock_compress.call_count == 1 assert isinstance(message, bytes) # Ensure correct headers are added when valid config is provided def test_build_headers(prom_rw): prom_rw.headers = {"Custom Header": "test_header"} headers = prom_rw._build_headers() assert headers["Content-Encoding"] == "snappy" assert headers["Content-Type"] == "application/x-protobuf" assert headers["X-Prometheus-Remote-Write-Version"] == "0.1.0" assert headers["Custom Header"] == "test_header" opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-richconsole/000077500000000000000000000000001527070072300304655ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-richconsole/README.rst000066400000000000000000000014231527070072300321540ustar00rootroot00000000000000OpenTelemetry Rich Console Exporter =================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-richconsole.svg :target: https://pypi.org/project/opentelemetry-exporter-richconsole/ This library is a console exporter using the Rich tree view. When used with a batch span processor, the rich console exporter will show the trace as a tree and all related spans as children within the tree, including properties. Installation ------------ :: pip install opentelemetry-exporter-richconsole .. _Rich: https://rich.readthedocs.io/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ References ---------- * `Rich `_ * `OpenTelemetry Project `_ opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-richconsole/pyproject.toml000066400000000000000000000026171527070072300334070ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-exporter-richconsole" dynamic = ["version"] description = "Rich Console Exporter for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-sdk ~= 1.12", "opentelemetry-semantic-conventions == 0.66b2", "rich>=10.0.0", ] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/exporter/opentelemetry-exporter-richconsole" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/exporter/richconsole/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-richconsole/src/000077500000000000000000000000001527070072300312545ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-richconsole/src/opentelemetry/000077500000000000000000000000001527070072300341505ustar00rootroot00000000000000exporter/000077500000000000000000000000001527070072300357415ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-richconsole/src/opentelemetryrichconsole/000077500000000000000000000000001527070072300402515ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-richconsole/src/opentelemetry/exporter__init__.py000066400000000000000000000141151527070072300423640ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-richconsole/src/opentelemetry/exporter/richconsole# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The **OpenTelemetry Rich Console Exporter** provides a span exporter from a batch span processor to print `OpenTelemetry`_ traces using `Rich`_. Installation ------------ :: pip install opentelemetry-exporter-richconsole Usage ----- The Rich Console Exporter is a console exporter that prints a tree view onto stdout of the traces with the related spans and properties as children of that tree. For the tree view, the Rich Console Exporter should be used with a BatchSpanProcessor. If used within a SimpleSpanProcessor, all spans will be printed in a list. .. code:: python from opentelemetry import trace from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.richconsole import RichConsoleSpanExporter from opentelemetry.sdk.trace import TracerProvider trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) tracer.add_span_processor(BatchSpanProcessor(RichConsoleSpanExporter())) API --- .. _Rich: https://rich.readthedocs.io/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ """ # pylint: disable=import-error import datetime import typing from rich.console import Console from rich.syntax import Syntax from rich.text import Text from rich.tree import Tree import opentelemetry.trace from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_STATEMENT, ) def _ns_to_time(nanoseconds): ts = datetime.datetime.fromtimestamp(nanoseconds / 1e9, datetime.timezone.utc) return ts.strftime("%H:%M:%S.%f") def _child_to_tree(child: Tree, span: ReadableSpan, *, suppress_resource: bool): child.add(Text.from_markup(f"[bold cyan]Kind :[/bold cyan] {span.kind.name}")) _add_status(child, span) _child_add_optional_attributes(child, span, suppress_resource=suppress_resource) def _add_status(child: Tree, span: ReadableSpan): if not span.status.is_unset: if not span.status.is_ok: child.add(Text.from_markup(f"[bold cyan]Status :[/bold cyan] [red]{span.status.status_code}[/red]")) else: child.add(Text.from_markup(f"[bold cyan]Status :[/bold cyan] {span.status.status_code}")) if span.status.description: child.add(Text.from_markup(f"[bold cyan]Description :[/bold cyan] {span.status.description}")) def _child_add_optional_attributes(child: Tree, span: ReadableSpan, *, suppress_resource: bool): if span.events: events = child.add(label=Text.from_markup("[bold cyan]Events :[/bold cyan] ")) for event in span.events: event_node = events.add(Text(event.name)) for key, val in event.attributes.items(): event_node.add(Text.from_markup(f"[bold cyan]{key} :[/bold cyan] {val}")) if span.attributes: attributes = child.add(label=Text.from_markup("[bold cyan]Attributes :[/bold cyan] ")) for attribute in span.attributes: if attribute == DB_STATEMENT: attributes.add(Text.from_markup(f"[bold cyan]{attribute} :[/bold cyan] ")) attributes.add(Syntax(span.attributes[attribute], "sql")) else: attributes.add(Text.from_markup(f"[bold cyan]{attribute} :[/bold cyan] {span.attributes[attribute]}")) if span.resource and not suppress_resource: resources = child.add(label=Text.from_markup("[bold cyan]Resources :[/bold cyan] ")) for resource in span.resource.attributes: resources.add(Text.from_markup(f"[bold cyan]{resource} :[/bold cyan] {span.resource.attributes[resource]}")) class RichConsoleSpanExporter(SpanExporter): """Implementation of :class:`SpanExporter` that prints spans to the console. Should be used within a BatchSpanProcessor """ def __init__( self, service_name: str | None = None, suppress_resource: bool = False, ): self.service_name = service_name self.suppress_resource = suppress_resource self.console = Console() def export(self, spans: typing.Sequence[ReadableSpan]) -> SpanExportResult: if not spans: return SpanExportResult.SUCCESS for tree in self.spans_to_tree(spans, suppress_resource=self.suppress_resource).values(): self.console.print(tree) return SpanExportResult.SUCCESS @staticmethod def spans_to_tree( spans: typing.Sequence[ReadableSpan], *, suppress_resource: bool = False, ) -> dict[str, Tree]: trees = {} parents = {} spans = list(spans) span_ids = {s.context.span_id for s in spans} while spans: for span in spans: if not span.parent or span.parent.span_id not in span_ids: trace_id = opentelemetry.trace.format_trace_id(span.context.trace_id) tree = trees.setdefault(trace_id, Tree(label=f"Trace {trace_id}")) child = tree.add( label=Text.from_markup( f"[blue][{_ns_to_time(span.start_time)}][/blue] [bold]{span.name}[/bold], span {opentelemetry.trace.format_span_id(span.context.span_id)}" ) ) parents[span.context.span_id] = child _child_to_tree(child, span, suppress_resource=suppress_resource) spans.remove(span) elif span.parent and span.parent.span_id in parents: child = parents[span.parent.span_id].add( label=Text.from_markup( f"[blue][{_ns_to_time(span.start_time)}][/blue] [bold]{span.name}[/bold], span {opentelemetry.trace.format_span_id(span.context.span_id)}" ) ) parents[span.context.span_id] = child _child_to_tree(child, span, suppress_resource=suppress_resource) spans.remove(span) return trees version.py000066400000000000000000000001441527070072300423070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-richconsole/src/opentelemetry/exporter/richconsole# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000005061527070072300346500ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-richconsoleasgiref==3.8.1 Deprecated==1.2.14 flaky==3.7.0 iniconfig==2.0.0 markdown-it-py==3.0.0 mdurl==0.1.2 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 Pygments==2.17.2 pytest==7.4.4 pytest-timeout==2.3.1 rich==13.7.1 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e exporter/opentelemetry-exporter-richconsole opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-richconsole/tests/000077500000000000000000000000001527070072300316275ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-richconsole/tests/__init__.py000066400000000000000000000000001527070072300337260ustar00rootroot00000000000000test_rich_exporter.py000066400000000000000000000072611527070072300360440ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/exporter/opentelemetry-exporter-richconsole/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import pytest from rich.text import Text from rich.tree import Tree import opentelemetry.trace from opentelemetry.exporter.richconsole import RichConsoleSpanExporter from opentelemetry.sdk import trace from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace.export import BatchSpanProcessor @pytest.fixture(name="span_processor") def fixture_span_processor(): exporter = RichConsoleSpanExporter() span_processor = BatchSpanProcessor(exporter) yield span_processor span_processor.shutdown() @pytest.fixture(name="tracer_provider") def fixture_tracer_provider(span_processor): tracer_provider = trace.TracerProvider() tracer_provider.add_span_processor(span_processor) yield tracer_provider def test_span_exporter(tracer_provider, span_processor, capsys): tracer = tracer_provider.get_tracer(__name__) span = tracer.start_span("test_span") span.set_attribute("key", "V4LuE") span.end() span_processor.force_flush() captured = capsys.readouterr() assert "V4LuE" in captured.out def walk_tree(root: Tree) -> int: # counts the amount of spans in a tree that contains a span return sum(walk_tree(child) for child in root.children) + int("span" in root.label) def test_multiple_traces(tracer_provider): exporter = RichConsoleSpanExporter() tracer = tracer_provider.get_tracer(__name__) with tracer.start_as_current_span("parent_1") as parent_1: with tracer.start_as_current_span("child_1") as child_1: pass with tracer.start_as_current_span("parent_2") as parent_2: pass trees = exporter.spans_to_tree((parent_2, parent_1, child_1)) # asserts that we have all traces assert len(trees) == 2 traceid_1 = opentelemetry.trace.format_trace_id(parent_1.context.trace_id) assert traceid_1 in trees assert opentelemetry.trace.format_trace_id(parent_2.context.trace_id) in trees # asserts that we have exactly the number of spans we exported assert sum(walk_tree(tree) for tree in trees.values()) == 3 # assert that the relationship is correct assert parent_1.name in trees[traceid_1].children[0].label assert any(child_1.name in child.label for child in trees[traceid_1].children[0].children) assert not any(parent_1.name in child.label for child in trees[traceid_1].children[0].children) assert not any(parent_2.name in child.label for child in trees[traceid_1].children[0].children) @pytest.mark.timeout(30) def test_no_deadlock(tracer_provider): # non-regression test for https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3254 tracer = tracer_provider.get_tracer(__name__) with tracer.start_as_current_span("parent"): with tracer.start_as_current_span("child") as child: pass RichConsoleSpanExporter.spans_to_tree((child,)) def test_suppress_resource(span_processor): attributes = {"resource.key": "resource.value"} resource = Resource(attributes) tracer_provider = trace.TracerProvider(resource=resource) tracer_provider.add_span_processor(span_processor) tracer = tracer_provider.get_tracer(__name__) with tracer.start_as_current_span("parent") as parent: with tracer.start_as_current_span("child") as child: pass trees = RichConsoleSpanExporter.spans_to_tree((parent, child), suppress_resource=True) assert len(trees) == 1 nodes = [next(t for t in trees.values())] for node in nodes: label = node.label if isinstance(label, Text): label = label.plain assert "resource" not in label.lower() nodes.extend(node.children) opentelemetry-python-contrib-0.66b2/gen-requirements.txt000066400000000000000000000001571527070072300236030ustar00rootroot00000000000000-c dev-requirements.txt astor==0.8.1 jinja2==3.1.6 markupsafe==2.0.1 ruff==0.16.1 requests tomli tomli_w hatch opentelemetry-python-contrib-0.66b2/instrumentation-genai/000077500000000000000000000000001527070072300240715ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/AGENTS.md000066400000000000000000000012601527070072300253730ustar00rootroot00000000000000# GenAI Instrumentation — Agent and Contributor Guidelines **GenAI instrumentations are no longer developed in this repository.** They live in [opentelemetry-python-genai](https://github.com/open-telemetry/opentelemetry-python-genai), which is where new instrumentations, features, and bug fixes go. The packages still present under this directory are deprecated, receive security patches only, and will be removed from this repository in the future. Do not add instrumentations here, and do not fix bugs or add features to the ones that remain. Send any such change to the `opentelemetry-python-genai` repo instead. Security patches are the only changes accepted in this directory. opentelemetry-python-contrib-0.66b2/instrumentation-genai/CLAUDE.md000077700000000000000000000000001527070072300266462AGENTS.mdustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/README.md000066400000000000000000000043271527070072300253560ustar00rootroot00000000000000 > [!WARNING] > GenAI instrumentations have been moved to the > [opentelemetry-python-genai](https://github.com/open-telemetry/opentelemetry-python-genai) > repository, which is now their home for new development and releases. The copies that > still live in this directory only receive security patches and will be removed from > here in the future. | Instrumentation | Supported Packages | Metrics support | Semconv status | | --------------- | ------------------ | --------------- | -------------- | | [opentelemetry-instrumentation-genai-anthropic](https://github.com/open-telemetry/opentelemetry-python-genai/tree/main/instrumentation/opentelemetry-instrumentation-genai-anthropic) | anthropic >= 0.16.0 | Yes | development | [opentelemetry-instrumentation-genai-claude-agent-sdk](https://github.com/open-telemetry/opentelemetry-python-genai/tree/main/instrumentation/opentelemetry-instrumentation-genai-claude-agent-sdk) | claude-agent-sdk >= 0.1.14 | No | development | [opentelemetry-instrumentation-google-genai](https://github.com/open-telemetry/opentelemetry-python-genai/tree/main/instrumentation/opentelemetry-instrumentation-google-genai) | google-genai >= 1.32.0 | No | development | [opentelemetry-instrumentation-genai-langchain](https://github.com/open-telemetry/opentelemetry-python-genai/tree/main/instrumentation/opentelemetry-instrumentation-genai-langchain) | langchain >= 0.3.21 | Yes | development | [opentelemetry-instrumentation-genai-openai-agents](https://github.com/open-telemetry/opentelemetry-python-genai/tree/main/instrumentation/opentelemetry-instrumentation-genai-openai-agents) | openai-agents >= 0.3.3 | No | development | [opentelemetry-instrumentation-genai-openai](https://github.com/open-telemetry/opentelemetry-python-genai/tree/main/instrumentation/opentelemetry-instrumentation-genai-openai) | openai >= 1.26.0 | Yes | development | [opentelemetry-instrumentation-vertexai](./opentelemetry-instrumentation-vertexai) | google-cloud-aiplatform >= 1.64 | No | development | [opentelemetry-instrumentation-genai-weaviate-client](https://github.com/open-telemetry/opentelemetry-python-genai/tree/main/instrumentation/opentelemetry-instrumentation-genai-weaviate-client) | weaviate-client >= 3.0.0,<5.0.0 | No | developmentopentelemetry-instrumentation-openai-agents-v2/000077500000000000000000000000001527070072300352245ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai.gitignore000066400000000000000000000001161527070072300372120ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2examples/.env examples/openai_agents_multi_agent_travel/.env examples/**/.env CHANGELOG.md000066400000000000000000000030411527070072300370330ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased - Deprecate `opentelemetry-instrumentation-openai-agents-v2`. Use the `opentelemetry-instrumentation-genai-openai-agents` package instead. This package now only receives security patches. - Align AgentSpanData test stubs and span processor with real OpenAI Agents SDK; remove non-existent `operation`, `description`, `agent_id`, and `model` fields. ([#4229](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4229)) - Document official package metadata and README for the OpenAI Agents instrumentation. ([#3859](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3859)) - Populate instructions and tool definitions from Response obj. ([#4196](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4196)) ## Version 0.1.0 (2025-10-15) - Initial barebones package skeleton: minimal instrumentor stub, version module, and packaging metadata/entry point. ([#3805](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3805)) - Implement OpenAI Agents span processing aligned with GenAI semantic conventions. ([#3817](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3817)) - Input and output according to GenAI spec. ([#3824](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3824)) LICENSE000066400000000000000000000261551527070072300362420ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (which shall not include communication that is clearly marked or otherwise designated in writing by the copyright owner as "Not a Work"). "Derivative Works" shall mean any work, whether in Source or Object form, that is based upon (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and derivative works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is clearly marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to use, reproduce, modify, distribute, and prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, trademark, patent, attribution and other notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright notice to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Support. When redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or support. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000116161527070072300367200ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2OpenTelemetry OpenAI Agents Instrumentation =========================================== .. warning:: **This package is deprecated.** Use the `opentelemetry-instrumentation-genai-openai-agents `_ package instead. ``opentelemetry-instrumentation-openai-agents-v2`` now only receives security patches. Please migrate by replacing it with ``opentelemetry-instrumentation-genai-openai-agents``: .. code-block:: console pip uninstall opentelemetry-instrumentation-openai-agents-v2 pip install opentelemetry-instrumentation-genai-openai-agents The replacement package contains **breaking changes** relative to this one. Review the `CHANGELOG `_ before upgrading. |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-openai-agents-v2.svg :target: https://pypi.org/project/opentelemetry-instrumentation-openai-agents-v2/ This library provides the official OpenTelemetry instrumentation for the `openai-agents SDK `_. It converts the rich trace data emitted by the Agents runtime into the GenAI semantic conventions, enriches spans with request/response payload metadata, and records duration/token usage metrics. Features -------- * Generates spans for agents, tools, generations, guardrails, and handoffs using the OpenTelemetry GenAI semantic conventions. * Captures prompts, responses, tool arguments, and system instructions when content capture is enabled. * Publishes duration and token metrics for every operation. * Supports environment overrides so you can configure agent metadata or disable telemetry without code changes. Installation ------------ If your application is already configured with OpenTelemetry, install the package and its optional instruments: .. code-block:: console pip install opentelemetry-instrumentation-openai-agents-v2 pip install openai-agents Usage ----- Instrumentation automatically wires the Agents tracing processor into the SDK. Configure OpenTelemetry as usual, then call :class:`OpenAIAgentsInstrumentor`. .. code-block:: python from agents import Agent, Runner, function_tool from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.instrumentation.openai_agents import OpenAIAgentsInstrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor def configure_otel() -> None: provider = TracerProvider() provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter())) trace.set_tracer_provider(provider) OpenAIAgentsInstrumentor().instrument(tracer_provider=provider) @function_tool def get_weather(city: str) -> str: return f"The forecast for {city} is sunny with pleasant temperatures." assistant = Agent( name="Travel Concierge", instructions="You are a concise travel concierge.", tools=[get_weather], ) result = Runner.run_sync(assistant, "I'm visiting Barcelona this weekend. How should I pack?") print(result.final_output) Configuration ------------- The instrumentor exposes runtime toggles through keyword arguments and environment variables: * ``OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`` or ``OTEL_INSTRUMENTATION_OPENAI_AGENTS_CAPTURE_CONTENT`` – controls how much message content is captured. Valid values map to :class:`opentelemetry.instrumentation.openai_agents.ContentCaptureMode` (``span_only``, ``event_only``, ``span_and_event``, ``no_content``). * ``OTEL_INSTRUMENTATION_OPENAI_AGENTS_CAPTURE_METRICS`` – set to ``false`` to disable duration/token metrics. * ``OTEL_INSTRUMENTATION_OPENAI_AGENTS_SYSTEM`` – overrides the ``gen_ai.system`` attribute when your deployment is not the default OpenAI platform. You can also override agent metadata directly when calling ``OpenAIAgentsInstrumentor().instrument(...)`` using ``agent_name``, ``agent_id``, ``agent_description``, ``base_url``, ``server_address``, and ``server_port``. Examples -------- The :mod:`examples` directory contains runnable scenarios, including: * ``examples/manual`` – manual OpenTelemetry configuration for a single agent run. * ``examples/content-capture`` – demonstrates span and event content capture. * ``examples/zero-code`` – end-to-end setup using environment configuration only. References ---------- * `OpenTelemetry Python Contrib `_ * `OpenTelemetry GenAI semantic conventions `_ * `OpenAI Agents SDK `_ examples/000077500000000000000000000000001527070072300370425ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2content-capture/000077500000000000000000000000001527070072300421555ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/examples.env.example000066400000000000000000000005221527070072300443770ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/examples/content-capture# Copy to .env and add values before running the sample. # Required for OpenAI client (only used if you swap in a real OpenAI call) OPENAI_API_KEY= # Optional overrides for span attributes / exporters OTEL_SERVICE_NAME=openai-agents-content-capture-demo OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 OTEL_EXPORTER_OTLP_PROTOCOL=grpc README.md000066400000000000000000000026361527070072300434430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/examples/content-capture# OpenAI Agents Content Capture Demo This example exercises the `OpenAIAgentsInstrumentor` with message content capture enabled, illustrating how prompts, responses, and tool payloads are recorded on spans and span events. > The demo uses the local tracing utilities from the `openai-agents` > package—no outbound API calls are made. ## Prerequisites 1. Activate the repository virtual environment: ```bash source ../../.venv/bin/activate ``` 2. Copy `.env.example` to `.env` and provide any overrides you need (for example, setting `OTEL_EXPORTER_OTLP_ENDPOINT`). 3. Ensure `openai-agents` is installed in the environment (it is included in the shared development venv for this repository). ## Run the demo ```bash python main.py ``` The script will: - Configure the OpenTelemetry SDK with an OTLP exporter so spans reach your collector. - Instrument the OpenAI Agents tracing hooks with content capture enabled. - Simulate an agent invocation that performs a generation and a tool call. - Print the resulting spans, attributes, and events (including JSON-encoded prompts and responses) to stdout. ## Customisation tips - Set `OTEL_SERVICE_NAME` before running to override the default service name. - Adjust the OTLP exporter configuration (endpoint, protocol) through `.env`. - Modify the prompts, tool payloads, or add additional spans in `run_workflow` to explore different content capture scenarios. main.py000066400000000000000000000071051527070072300434560ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/examples/content-capture# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Content capture demo for the OpenAI Agents instrumentation. This script spins up the instrumentation with message capture enabled and simulates an agent invocation plus a tool call using the tracing helpers from the ``openai-agents`` package. Spans are exported to the console so you can inspect captured prompts, responses, and tool payloads without making any OpenAI API calls. """ from __future__ import annotations import json import os from typing import Any from agents.tracing import agent_span, function_span, generation_span, trace from dotenv import load_dotenv from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.instrumentation.openai_agents import ( OpenAIAgentsInstrumentor, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor load_dotenv() # take environment variables from .env. def configure_tracing() -> None: """Configure a tracer provider that exports spans via OTLP.""" resource = Resource.create( {"service.name": os.environ.get("OTEL_SERVICE_NAME", "openai-agents-content-capture-demo")} ) provider = TracerProvider(resource=resource) provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter())) # Instrument with explicit content capture mode to ensure prompts/responses are recorded. OpenAIAgentsInstrumentor().instrument( tracer_provider=provider, capture_message_content="span_and_event", system="openai", agent_name="Travel Concierge", base_url="https://api.openai.com/v1", ) def dump(title: str, payload: Any) -> None: """Pretty-print helper used to show intermediate context.""" print(f"\n=== {title} ===") print(json.dumps(payload, indent=2)) def run_workflow() -> None: """Simulate an agent workflow with a generation and a tool invocation.""" itinerary_prompt = [ {"role": "system", "content": "Plan high level travel itineraries."}, { "role": "user", "content": "I'm visiting Paris for 3 days in November.", }, ] tool_args = {"city": "Paris", "date": "2025-11-12"} tool_result = { "forecast": "Mostly sunny, highs 15°C", "packing_tips": ["light jacket", "comfortable shoes"], } with trace("travel-booking-workflow"): with agent_span(name="travel_planner") as agent: dump( "Agent span started", {"span_id": agent.span_id, "trace_id": agent.trace_id}, ) with generation_span( input=itinerary_prompt, output=[ { "role": "assistant", "content": ("Day 1 visit the Louvre, Day 2 tour Versailles, Day 3 explore Montmartre."), } ], model="gpt-4o-mini", usage={ "input_tokens": 128, "output_tokens": 96, "total_tokens": 224, }, ): pass with function_span( name="fetch_weather", input=json.dumps(tool_args), output=tool_result, ): pass print("\nWorkflow complete – spans exported to the configured OTLP endpoint.") def main() -> None: configure_tracing() run_workflow() if __name__ == "__main__": main() manual/000077500000000000000000000000001527070072300403175ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/examples.env.example000066400000000000000000000003111527070072300425350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/examples/manual# Copy to .env and add real values before running main.py OPENAI_API_KEY= OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 OTEL_EXPORTER_OTLP_PROTOCOL=grpc OTEL_SERVICE_NAME=openai-agents-manual-demo README.rst000066400000000000000000000027001527070072300420050ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/examples/manualOpenTelemetry OpenAI Agents Instrumentation Example =================================================== This example demonstrates how to manually configure the OpenTelemetry SDK alongside the OpenAI Agents instrumentation. Running `main.py `_ produces spans for the end-to-end agent run, including tool invocations and model generations. Spans are exported through OTLP/gRPC to the endpoint configured in the environment. Setup ----- 1. Copy `.env.example <.env.example>`_ to `.env` and update it with your real ``OPENAI_API_KEY``. If your OTLP collector is not reachable via ``http://localhost:4317``, adjust the endpoint variables as needed. 2. Create a virtual environment and install the dependencies: :: python3 -m venv .venv source .venv/bin/activate pip install "python-dotenv[cli]" uv pip install -r requirements.txt --prerelease=allow Run --- Execute the sample with ``dotenv`` so the environment variables from ``.env`` are applied: :: dotenv run -- python main.py Ensure ``OPENAI_API_KEY`` is present in your environment (or ``.env`` file); the OpenAI client raises ``OpenAIError`` if the key is missing. The script automatically loads environment variables from ``.env`` so running ``python main.py`` directly also works if the shell already has the required values exported. You should see the agent response printed to the console while spans export to your configured observability backend. main.py000066400000000000000000000032771527070072300416260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/examples/manual# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: skip-file """Manual OpenAI Agents instrumentation example.""" from __future__ import annotations from agents import Agent, Runner, function_tool from dotenv import load_dotenv from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.instrumentation.openai_agents import ( OpenAIAgentsInstrumentor, ) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor def configure_otel() -> None: """Configure the OpenTelemetry SDK for exporting spans.""" provider = TracerProvider() provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter())) trace.set_tracer_provider(provider) OpenAIAgentsInstrumentor().instrument(tracer_provider=provider) @function_tool def get_weather(city: str) -> str: """Return a canned weather response for the requested city.""" return f"The forecast for {city} is sunny with pleasant temperatures." def run_agent() -> None: """Create a simple agent and execute a single run.""" assistant = Agent( name="Travel Concierge", instructions=( "You are a concise travel concierge. Use the weather tool when the traveler asks about local conditions." ), tools=[get_weather], ) result = Runner.run_sync( assistant, "I'm visiting Barcelona this weekend. How should I pack?", ) print("Agent response:") print(result.final_output) def main() -> None: load_dotenv() configure_otel() run_agent() if __name__ == "__main__": main() requirements.txt000066400000000000000000000002541527070072300436040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/examples/manualopenai-agents~=0.3.3 python-dotenv~=1.0 opentelemetry-sdk~=1.36.0 opentelemetry-exporter-otlp-proto-grpc~=1.36.0 opentelemetry-instrumentation-openai-agents-v2~=0.1.0.dev zero-code/000077500000000000000000000000001527070072300407315ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/examples.env.example000066400000000000000000000006141527070072300431550ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/examples/zero-code# Update this with your real OpenAI API key OPENAI_API_KEY=sk-YOUR_API_KEY # Uncomment and adjust if you use a non-default OTLP collector endpoint # OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 # OTEL_EXPORTER_OTLP_PROTOCOL=grpc OTEL_SERVICE_NAME=opentelemetry-python-openai-agents-zero-code # Optionally override the agent name reported on spans # OTEL_GENAI_AGENT_NAME=Travel Concierge README.rst000066400000000000000000000027641527070072300424310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/examples/zero-codeOpenTelemetry OpenAI Agents Zero-Code Instrumentation Example ============================================================= This example shows how to capture telemetry from OpenAI Agents without changing your application code by using ``opentelemetry-instrument``. When `main.py `_ is executed, spans describing the agent workflow are exported to the configured OTLP endpoint. The spans include details such as the operation name, tool usage, and token consumption (when available). Setup ----- 1. Copy `.env.example <.env.example>`_ to `.env` and update it with your real ``OPENAI_API_KEY``. Adjust the OTLP endpoint settings if your collector is not reachable via ``http://localhost:4317``. 2. Create a virtual environment and install the dependencies: :: python3 -m venv .venv source .venv/bin/activate pip install "python-dotenv[cli]" uv pip install -r requirements.txt --prerelease=allow Run --- Execute the sample via ``opentelemetry-instrument`` so the OpenAI Agents instrumentation is activated automatically: :: dotenv run -- opentelemetry-instrument python main.py Ensure ``OPENAI_API_KEY`` is set in your shell or `.env`; the OpenAI client raises ``OpenAIError`` if the key is missing. Because ``main.py`` invokes ``load_dotenv``, running ``python main.py`` directly also works when the required environment variables are already exported. You should see the agent response printed to the console while spans export to your observability backend. main.py000066400000000000000000000034261527070072300422340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/examples/zero-code# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Zero-code OpenAI Agents example.""" from __future__ import annotations from agents import Agent, Runner, function_tool from dotenv import load_dotenv from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.instrumentation.openai_agents import ( OpenAIAgentsInstrumentor, ) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor def configure_tracing() -> None: """Ensure tracing exports spans even without auto-instrumentation.""" current_provider = trace.get_tracer_provider() if isinstance(current_provider, TracerProvider): provider = current_provider else: provider = TracerProvider() provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter())) trace.set_tracer_provider(provider) OpenAIAgentsInstrumentor().instrument(tracer_provider=provider) @function_tool def get_weather(city: str) -> str: """Return a canned weather response for the requested city.""" return f"The forecast for {city} is sunny with pleasant temperatures." def run_agent() -> None: assistant = Agent( name="Travel Concierge", instructions=( "You are a concise travel concierge. Use the weather tool when the traveler asks about local conditions." ), tools=[get_weather], ) result = Runner.run_sync( assistant, "I'm visiting Barcelona this weekend. How should I pack?", ) print("Agent response:") print(result.final_output) def main() -> None: load_dotenv() configure_tracing() run_agent() if __name__ == "__main__": main() requirements.txt000066400000000000000000000003111527070072300442100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/examples/zero-codeopenai-agents~=0.3.3 python-dotenv~=1.0 opentelemetry-sdk~=1.36.0 opentelemetry-exporter-otlp-proto-grpc~=1.36.0 opentelemetry-distro~=0.57b0 opentelemetry-instrumentation-openai-agents-v2~=0.1.0.dev pyproject.toml000066400000000000000000000033431527070072300401430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-openai-agents-v2" dynamic = ["version"] description = "OpenTelemetry Official OpenAI Agents instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api >= 1.40", "opentelemetry-instrumentation >= 0.61b0", "opentelemetry-semantic-conventions >= 0.61b0", "opentelemetry-util-genai >= 0.4b0, < 0.6b0", "typing-extensions >= 4.5.0", ] [project.optional-dependencies] instruments = [ "openai-agents >= 0.3.3", ] [project.entry-points.opentelemetry_instrumentor] openai_agents = "opentelemetry.instrumentation.openai_agents:OpenAIAgentsInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/openai_agents/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] src/000077500000000000000000000000001527070072300360135ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2opentelemetry/000077500000000000000000000000001527070072300407075ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/srcinstrumentation/000077500000000000000000000000001527070072300441525ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetryopenai_agents/000077500000000000000000000000001527070072300467665ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation__init__.py000066400000000000000000000161751527070072300511110ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """OpenAI Agents instrumentation for OpenTelemetry.""" # pylint: disable=too-many-locals from __future__ import annotations import importlib import logging import os from collections.abc import Collection from typing import Any from typing_extensions import deprecated from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAI, ) from opentelemetry.semconv.schemas import Schemas from opentelemetry.trace import get_tracer from .package import _instruments from .span_processor import ( ContentCaptureMode, GenAIEvaluationAttributes, GenAIOperationName, GenAIOutputType, GenAIProvider, GenAISemanticProcessor, GenAIToolType, ) __all__ = [ "GenAIEvaluationAttributes", "GenAIOperationName", "GenAIOutputType", "GenAIProvider", "GenAIToolType", "OpenAIAgentsInstrumentor", ] logger = logging.getLogger(__name__) _CONTENT_CAPTURE_ENV = "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" _SYSTEM_OVERRIDE_ENV = "OTEL_INSTRUMENTATION_OPENAI_AGENTS_SYSTEM" _CAPTURE_CONTENT_ENV = "OTEL_INSTRUMENTATION_OPENAI_AGENTS_CAPTURE_CONTENT" _CAPTURE_METRICS_ENV = "OTEL_INSTRUMENTATION_OPENAI_AGENTS_CAPTURE_METRICS" def _load_tracing_module(): # pragma: no cover - exercised via tests return importlib.import_module("agents.tracing") def _get_registered_processors(provider) -> list: multi = getattr(provider, "_multi_processor", None) processors = getattr(multi, "_processors", ()) return list(processors) def _resolve_system(value: str | None) -> str: if not value: return GenAI.GenAiSystemValues.OPENAI.value normalized = value.strip().lower() for member in GenAI.GenAiSystemValues: if normalized == member.value: return member.value if normalized == member.name.lower(): return member.value return value def _resolve_content_mode(value: Any) -> ContentCaptureMode: if isinstance(value, ContentCaptureMode): return value if isinstance(value, bool): return ContentCaptureMode.SPAN_AND_EVENT if value else ContentCaptureMode.NO_CONTENT if value is None: return ContentCaptureMode.SPAN_AND_EVENT text = str(value).strip().lower() if not text: return ContentCaptureMode.SPAN_AND_EVENT mapping = { "span_only": ContentCaptureMode.SPAN_ONLY, "span-only": ContentCaptureMode.SPAN_ONLY, "span": ContentCaptureMode.SPAN_ONLY, "event_only": ContentCaptureMode.EVENT_ONLY, "event-only": ContentCaptureMode.EVENT_ONLY, "event": ContentCaptureMode.EVENT_ONLY, "span_and_event": ContentCaptureMode.SPAN_AND_EVENT, "span-and-event": ContentCaptureMode.SPAN_AND_EVENT, "span_and_events": ContentCaptureMode.SPAN_AND_EVENT, "all": ContentCaptureMode.SPAN_AND_EVENT, "true": ContentCaptureMode.SPAN_AND_EVENT, "1": ContentCaptureMode.SPAN_AND_EVENT, "yes": ContentCaptureMode.SPAN_AND_EVENT, "no_content": ContentCaptureMode.NO_CONTENT, "false": ContentCaptureMode.NO_CONTENT, "0": ContentCaptureMode.NO_CONTENT, "no": ContentCaptureMode.NO_CONTENT, "none": ContentCaptureMode.NO_CONTENT, } return mapping.get(text, ContentCaptureMode.SPAN_AND_EVENT) def _resolve_bool(value: Any, default: bool) -> bool: if value is None: return default if isinstance(value, bool): return value text = str(value).strip().lower() if text in {"true", "1", "yes", "on"}: return True if text in {"false", "0", "no", "off"}: return False return default @deprecated( "opentelemetry-instrumentation-openai-agents-v2 is deprecated. Use the " "opentelemetry-instrumentation-genai-openai-agents package instead. This " "package only receives security patches." ) class OpenAIAgentsInstrumentor(BaseInstrumentor): """Instrumentation that bridges OpenAI Agents tracing to OpenTelemetry. .. deprecated:: 0.2.0 Use the ``opentelemetry-instrumentation-genai-openai-agents`` package instead. This package only receives security patches. """ def __init__(self) -> None: super().__init__() self._processor: GenAISemanticProcessor | None = None def _instrument(self, **kwargs) -> None: if self._processor is not None: return tracer_provider = kwargs.get("tracer_provider") tracer = get_tracer( __name__, "", tracer_provider, schema_url=Schemas.V1_28_0.value, ) system_override = kwargs.get("system") or os.getenv(_SYSTEM_OVERRIDE_ENV) system = _resolve_system(system_override) content_override = kwargs.get("capture_message_content") if content_override is None: content_override = os.getenv(_CONTENT_CAPTURE_ENV) or os.getenv(_CAPTURE_CONTENT_ENV) content_mode = _resolve_content_mode(content_override) metrics_override = kwargs.get("capture_metrics") if metrics_override is None: metrics_override = os.getenv(_CAPTURE_METRICS_ENV) metrics_enabled = _resolve_bool(metrics_override, default=True) agent_name = kwargs.get("agent_name") agent_id = kwargs.get("agent_id") agent_description = kwargs.get("agent_description") base_url = kwargs.get("base_url") server_address = kwargs.get("server_address") server_port = kwargs.get("server_port") processor = GenAISemanticProcessor( tracer=tracer, system_name=system, include_sensitive_data=content_mode != ContentCaptureMode.NO_CONTENT, content_mode=content_mode, metrics_enabled=metrics_enabled, agent_name=agent_name, agent_id=agent_id, agent_description=agent_description, base_url=base_url, server_address=server_address, server_port=server_port, agent_name_default="OpenAI Agent", agent_id_default="agent", agent_description_default="OpenAI Agents instrumentation", base_url_default="https://api.openai.com", server_address_default="api.openai.com", server_port_default=443, ) tracing = _load_tracing_module() provider = tracing.get_trace_provider() existing = _get_registered_processors(provider) provider.set_processors([*existing, processor]) self._processor = processor def _uninstrument(self, **kwargs) -> None: if self._processor is None: return tracing = _load_tracing_module() provider = tracing.get_trace_provider() current = _get_registered_processors(provider) filtered = [proc for proc in current if proc is not self._processor] provider.set_processors(filtered) try: self._processor.shutdown() finally: self._processor = None def instrumentation_dependencies(self) -> Collection[str]: return _instruments package.py000066400000000000000000000002221527070072300507270ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("openai-agents >= 0.3.3",) _supports_metrics = False span_processor.py000066400000000000000000002343421527070072300524100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ GenAI Semantic Convention Trace Processor This module implements a custom trace processor that enriches spans with OpenTelemetry GenAI semantic conventions attributes following the OpenInference processor pattern. It adds standardized attributes for generative AI operations using iterator-based attribute extraction. References: - OpenTelemetry GenAI Semantic Conventions: https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/ - OpenInference Pattern: https://github.com/Arize-ai/openinference """ # pylint: disable=too-many-lines,invalid-name,too-many-locals,too-many-branches,too-many-statements,too-many-return-statements,too-many-nested-blocks,too-many-arguments,too-many-instance-attributes,broad-exception-caught,no-self-use,consider-iterating-dictionary,unused-variable,unnecessary-pass from __future__ import annotations import importlib import logging from collections.abc import Iterator, Sequence from dataclasses import dataclass from datetime import datetime, timezone from enum import Enum from typing import Any from urllib.parse import urlparse from opentelemetry.util.genai.utils import gen_ai_json_dumps try: from agents.tracing import Span, Trace, TracingProcessor from agents.tracing.span_data import ( AgentSpanData, FunctionSpanData, GenerationSpanData, GuardrailSpanData, HandoffSpanData, ResponseSpanData, SpeechSpanData, TranscriptionSpanData, ) except ModuleNotFoundError: # pragma: no cover - test stubs tracing_module = importlib.import_module("agents.tracing") Span = tracing_module.Span Trace = tracing_module.Trace TracingProcessor = tracing_module.TracingProcessor AgentSpanData = getattr(tracing_module, "AgentSpanData", Any) # type: ignore[assignment] FunctionSpanData = getattr(tracing_module, "FunctionSpanData", Any) # type: ignore[assignment] GenerationSpanData = getattr(tracing_module, "GenerationSpanData", Any) # type: ignore[assignment] GuardrailSpanData = getattr(tracing_module, "GuardrailSpanData", Any) # type: ignore[assignment] HandoffSpanData = getattr(tracing_module, "HandoffSpanData", Any) # type: ignore[assignment] ResponseSpanData = getattr(tracing_module, "ResponseSpanData", Any) # type: ignore[assignment] SpeechSpanData = getattr(tracing_module, "SpeechSpanData", Any) # type: ignore[assignment] TranscriptionSpanData = getattr(tracing_module, "TranscriptionSpanData", Any) # type: ignore[assignment] from opentelemetry.context import attach, detach from opentelemetry.metrics import Histogram, get_meter from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, ) from opentelemetry.semconv._incubating.attributes import ( server_attributes as ServerAttributes, ) from opentelemetry.trace import Span as OtelSpan from opentelemetry.trace import ( SpanKind, Status, StatusCode, Tracer, set_span_in_context, ) from opentelemetry.util.types import AttributeValue # Import all semantic convention constants # ---- GenAI semantic convention helpers (embedded from constants.py) ---- def _enum_values(enum_cls) -> dict[str, str]: """Return mapping of enum member name to value.""" return {member.name: member.value for member in enum_cls} _PROVIDER_VALUES = _enum_values(GenAIAttributes.GenAiProviderNameValues) class GenAIProvider: OPENAI = _PROVIDER_VALUES["OPENAI"] GCP_GEN_AI = _PROVIDER_VALUES["GCP_GEN_AI"] GCP_VERTEX_AI = _PROVIDER_VALUES["GCP_VERTEX_AI"] GCP_GEMINI = _PROVIDER_VALUES["GCP_GEMINI"] ANTHROPIC = _PROVIDER_VALUES["ANTHROPIC"] COHERE = _PROVIDER_VALUES["COHERE"] AZURE_AI_INFERENCE = _PROVIDER_VALUES["AZURE_AI_INFERENCE"] AZURE_AI_OPENAI = _PROVIDER_VALUES["AZURE_AI_OPENAI"] IBM_WATSONX_AI = _PROVIDER_VALUES["IBM_WATSONX_AI"] AWS_BEDROCK = _PROVIDER_VALUES["AWS_BEDROCK"] PERPLEXITY = _PROVIDER_VALUES["PERPLEXITY"] X_AI = _PROVIDER_VALUES["X_AI"] DEEPSEEK = _PROVIDER_VALUES["DEEPSEEK"] GROQ = _PROVIDER_VALUES["GROQ"] MISTRAL_AI = _PROVIDER_VALUES["MISTRAL_AI"] ALL = set(_PROVIDER_VALUES.values()) _OPERATION_VALUES = _enum_values(GenAIAttributes.GenAiOperationNameValues) class GenAIOperationName: CHAT = _OPERATION_VALUES["CHAT"] GENERATE_CONTENT = _OPERATION_VALUES["GENERATE_CONTENT"] TEXT_COMPLETION = _OPERATION_VALUES["TEXT_COMPLETION"] EMBEDDINGS = _OPERATION_VALUES["EMBEDDINGS"] CREATE_AGENT = _OPERATION_VALUES["CREATE_AGENT"] INVOKE_AGENT = _OPERATION_VALUES["INVOKE_AGENT"] EXECUTE_TOOL = _OPERATION_VALUES["EXECUTE_TOOL"] # Operations below are not yet covered by the spec but remain for backwards compatibility TRANSCRIPTION = "transcription" SPEECH = "speech_generation" GUARDRAIL = "guardrail_check" HANDOFF = "agent_handoff" RESPONSE = "response" # internal aggregator in current processor CLASS_FALLBACK = { "generationspan": CHAT, "responsespan": RESPONSE, "functionspan": EXECUTE_TOOL, "agentspan": INVOKE_AGENT, } _OUTPUT_VALUES = _enum_values(GenAIAttributes.GenAiOutputTypeValues) class GenAIOutputType: TEXT = _OUTPUT_VALUES["TEXT"] JSON = _OUTPUT_VALUES["JSON"] IMAGE = _OUTPUT_VALUES["IMAGE"] SPEECH = _OUTPUT_VALUES["SPEECH"] class GenAIToolType: FUNCTION = "function" EXTENSION = "extension" DATASTORE = "datastore" ALL = {FUNCTION, EXTENSION, DATASTORE} class GenAIEvaluationAttributes: NAME = "gen_ai.evaluation.name" SCORE_VALUE = "gen_ai.evaluation.score.value" SCORE_LABEL = "gen_ai.evaluation.score.label" EXPLANATION = "gen_ai.evaluation.explanation" def _attr(name: str, fallback: str) -> str: return getattr(GenAIAttributes, name, fallback) GEN_AI_PROVIDER_NAME = _attr("GEN_AI_PROVIDER_NAME", "gen_ai.provider.name") GEN_AI_OPERATION_NAME = _attr("GEN_AI_OPERATION_NAME", "gen_ai.operation.name") GEN_AI_REQUEST_MODEL = _attr("GEN_AI_REQUEST_MODEL", "gen_ai.request.model") GEN_AI_REQUEST_MAX_TOKENS = _attr("GEN_AI_REQUEST_MAX_TOKENS", "gen_ai.request.max_tokens") GEN_AI_REQUEST_TEMPERATURE = _attr("GEN_AI_REQUEST_TEMPERATURE", "gen_ai.request.temperature") GEN_AI_REQUEST_TOP_P = _attr("GEN_AI_REQUEST_TOP_P", "gen_ai.request.top_p") GEN_AI_REQUEST_TOP_K = _attr("GEN_AI_REQUEST_TOP_K", "gen_ai.request.top_k") GEN_AI_REQUEST_FREQUENCY_PENALTY = _attr("GEN_AI_REQUEST_FREQUENCY_PENALTY", "gen_ai.request.frequency_penalty") GEN_AI_REQUEST_PRESENCE_PENALTY = _attr("GEN_AI_REQUEST_PRESENCE_PENALTY", "gen_ai.request.presence_penalty") GEN_AI_REQUEST_CHOICE_COUNT = _attr("GEN_AI_REQUEST_CHOICE_COUNT", "gen_ai.request.choice.count") GEN_AI_REQUEST_STOP_SEQUENCES = _attr("GEN_AI_REQUEST_STOP_SEQUENCES", "gen_ai.request.stop_sequences") GEN_AI_REQUEST_ENCODING_FORMATS = _attr("GEN_AI_REQUEST_ENCODING_FORMATS", "gen_ai.request.encoding_formats") GEN_AI_REQUEST_SEED = _attr("GEN_AI_REQUEST_SEED", "gen_ai.request.seed") GEN_AI_RESPONSE_ID = _attr("GEN_AI_RESPONSE_ID", "gen_ai.response.id") GEN_AI_RESPONSE_MODEL = _attr("GEN_AI_RESPONSE_MODEL", "gen_ai.response.model") GEN_AI_RESPONSE_FINISH_REASONS = _attr("GEN_AI_RESPONSE_FINISH_REASONS", "gen_ai.response.finish_reasons") GEN_AI_USAGE_INPUT_TOKENS = _attr("GEN_AI_USAGE_INPUT_TOKENS", "gen_ai.usage.input_tokens") GEN_AI_USAGE_OUTPUT_TOKENS = _attr("GEN_AI_USAGE_OUTPUT_TOKENS", "gen_ai.usage.output_tokens") GEN_AI_CONVERSATION_ID = _attr("GEN_AI_CONVERSATION_ID", "gen_ai.conversation.id") GEN_AI_AGENT_ID = _attr("GEN_AI_AGENT_ID", "gen_ai.agent.id") GEN_AI_AGENT_NAME = _attr("GEN_AI_AGENT_NAME", "gen_ai.agent.name") GEN_AI_AGENT_DESCRIPTION = _attr("GEN_AI_AGENT_DESCRIPTION", "gen_ai.agent.description") GEN_AI_TOOL_NAME = _attr("GEN_AI_TOOL_NAME", "gen_ai.tool.name") GEN_AI_TOOL_TYPE = _attr("GEN_AI_TOOL_TYPE", "gen_ai.tool.type") GEN_AI_TOOL_CALL_ID = _attr("GEN_AI_TOOL_CALL_ID", "gen_ai.tool.call.id") GEN_AI_TOOL_DESCRIPTION = _attr("GEN_AI_TOOL_DESCRIPTION", "gen_ai.tool.description") GEN_AI_OUTPUT_TYPE = _attr("GEN_AI_OUTPUT_TYPE", "gen_ai.output.type") GEN_AI_SYSTEM_INSTRUCTIONS = _attr("GEN_AI_SYSTEM_INSTRUCTIONS", "gen_ai.system_instructions") GEN_AI_INPUT_MESSAGES = _attr("GEN_AI_INPUT_MESSAGES", "gen_ai.input.messages") GEN_AI_OUTPUT_MESSAGES = _attr("GEN_AI_OUTPUT_MESSAGES", "gen_ai.output.messages") GEN_AI_DATA_SOURCE_ID = _attr("GEN_AI_DATA_SOURCE_ID", "gen_ai.data_source.id") # The semantic conventions currently expose multiple usage token attributes; we retain the # completion/prompt aliases for backwards compatibility where used. GEN_AI_USAGE_PROMPT_TOKENS = _attr("GEN_AI_USAGE_PROMPT_TOKENS", "gen_ai.usage.prompt_tokens") GEN_AI_USAGE_COMPLETION_TOKENS = _attr("GEN_AI_USAGE_COMPLETION_TOKENS", "gen_ai.usage.completion_tokens") # Attributes not (yet) defined in the spec retain their literal values. GEN_AI_TOOL_CALL_ARGUMENTS = "gen_ai.tool.call.arguments" GEN_AI_TOOL_CALL_RESULT = "gen_ai.tool.call.result" GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions" GEN_AI_ORCHESTRATOR_AGENT_DEFINITIONS = "gen_ai.orchestrator.agent.definitions" GEN_AI_GUARDRAIL_NAME = "gen_ai.guardrail.name" GEN_AI_GUARDRAIL_TRIGGERED = "gen_ai.guardrail.triggered" GEN_AI_HANDOFF_FROM_AGENT = "gen_ai.handoff.from_agent" GEN_AI_HANDOFF_TO_AGENT = "gen_ai.handoff.to_agent" GEN_AI_EMBEDDINGS_DIMENSION_COUNT = "gen_ai.embeddings.dimension.count" GEN_AI_TOKEN_TYPE = _attr("GEN_AI_TOKEN_TYPE", "gen_ai.token.type") # ---- Normalization utilities (embedded from utils.py) ---- def normalize_provider(provider: str | None) -> str | None: """Normalize provider name to spec-compliant value.""" if not provider: return None normalized = provider.strip().lower() if normalized in GenAIProvider.ALL: return normalized return provider # passthrough if unknown (forward compat) def validate_tool_type(tool_type: str | None) -> str: """Validate and normalize tool type.""" if not tool_type: return GenAIToolType.FUNCTION # default normalized = tool_type.strip().lower() return normalized if normalized in GenAIToolType.ALL else GenAIToolType.FUNCTION def normalize_output_type(output_type: str | None) -> str: """Normalize output type to spec-compliant value.""" if not output_type: return GenAIOutputType.TEXT # default normalized = output_type.strip().lower() base_map = { "json_object": GenAIOutputType.JSON, "jsonschema": GenAIOutputType.JSON, "speech_audio": GenAIOutputType.SPEECH, "audio_speech": GenAIOutputType.SPEECH, "image_png": GenAIOutputType.IMAGE, "function_arguments_json": GenAIOutputType.JSON, "tool_call": GenAIOutputType.JSON, "transcription_json": GenAIOutputType.JSON, } if normalized in base_map: return base_map[normalized] if normalized in { GenAIOutputType.TEXT, GenAIOutputType.JSON, GenAIOutputType.IMAGE, GenAIOutputType.SPEECH, }: return normalized return GenAIOutputType.TEXT # default for unknown # Legacy attributes removed logger = logging.getLogger(__name__) GEN_AI_SYSTEM_KEY = getattr(GenAIAttributes, "GEN_AI_SYSTEM", "gen_ai.system") class ContentCaptureMode(Enum): """Controls whether sensitive content is recorded on spans, events, or both.""" NO_CONTENT = "no_content" SPAN_ONLY = "span_only" EVENT_ONLY = "event_only" SPAN_AND_EVENT = "span_and_event" @property def capture_in_span(self) -> bool: return self in ( ContentCaptureMode.SPAN_ONLY, ContentCaptureMode.SPAN_AND_EVENT, ) @property def capture_in_event(self) -> bool: return self in ( ContentCaptureMode.EVENT_ONLY, ContentCaptureMode.SPAN_AND_EVENT, ) @dataclass class ContentPayload: """Container for normalized content associated with a span.""" input_messages: list[dict[str, Any]] | None = None output_messages: list[dict[str, Any]] | None = None system_instructions: list[dict[str, str]] | None = None tool_arguments: Any = None tool_result: Any = None def _is_instance_of(value: Any, classes: Any) -> bool: """Safe isinstance that tolerates typing.Any placeholders.""" if not isinstance(classes, tuple): classes = (classes,) for cls in classes: try: if isinstance(value, cls): return True except TypeError: continue return False def _infer_server_attributes(base_url: str | None) -> dict[str, Any]: """Return server.address / server.port attributes if base_url provided.""" out: dict[str, Any] = {} if not base_url: return out try: parsed = urlparse(base_url) if parsed.hostname: out[ServerAttributes.SERVER_ADDRESS] = parsed.hostname if parsed.port: out[ServerAttributes.SERVER_PORT] = parsed.port except Exception: return out return out def safe_json_dumps(obj: Any) -> str: """Safely convert object to JSON string (fallback to str).""" try: return gen_ai_json_dumps(obj) except (TypeError, ValueError): return str(obj) def _as_utc_nano(dt: datetime) -> int: """Convert datetime to UTC nanoseconds timestamp.""" return int(dt.astimezone(timezone.utc).timestamp() * 1_000_000_000) def _get_span_status(span: Span[Any]) -> Status: """Get OpenTelemetry span status from agent span.""" if error := getattr(span, "error", None): return Status( status_code=StatusCode.ERROR, description=f"{error.get('message', '')}: {error.get('data', '')}", ) return Status(StatusCode.OK) def get_span_name( operation_name: str, model: str | None = None, agent_name: str | None = None, tool_name: str | None = None, ) -> str: """Generate spec-compliant span name based on operation type.""" base_name = operation_name if operation_name in { GenAIOperationName.CHAT, GenAIOperationName.TEXT_COMPLETION, GenAIOperationName.EMBEDDINGS, GenAIOperationName.TRANSCRIPTION, GenAIOperationName.SPEECH, }: return f"{base_name} {model}" if model else base_name if operation_name == GenAIOperationName.CREATE_AGENT: return f"{base_name} {agent_name}" if agent_name else base_name if operation_name == GenAIOperationName.INVOKE_AGENT: return f"{base_name} {agent_name}" if agent_name else base_name if operation_name == GenAIOperationName.EXECUTE_TOOL: return f"{base_name} {tool_name}" if tool_name else base_name if operation_name == GenAIOperationName.HANDOFF: return f"{base_name} {agent_name}" if agent_name else base_name return base_name class GenAISemanticProcessor(TracingProcessor): """Trace processor adding GenAI semantic convention attributes with metrics.""" # pylint: disable=too-many-positional-arguments def __init__( self, tracer: Tracer | None = None, system_name: str = "openai", include_sensitive_data: bool = True, content_mode: ContentCaptureMode = ContentCaptureMode.SPAN_AND_EVENT, base_url: str | None = None, agent_name: str | None = None, agent_id: str | None = None, agent_description: str | None = None, server_address: str | None = None, server_port: int | None = None, metrics_enabled: bool = True, agent_name_default: str | None = None, agent_id_default: str | None = None, agent_description_default: str | None = None, base_url_default: str | None = None, server_address_default: str | None = None, server_port_default: int | None = None, ): """Initialize processor with metrics support. Args: tracer: Optional OpenTelemetry tracer system_name: Provider name (openai/azure.ai.inference/etc.) include_sensitive_data: Include model/tool IO when True base_url: API endpoint for server.address/port agent_name: Name of the agent (can be overridden by env var) agent_id: ID of the agent (can be overridden by env var) agent_description: Description of the agent (can be overridden by env var) server_address: Server address (can be overridden by env var or base_url) server_port: Server port (can be overridden by env var or base_url) """ self._tracer = tracer self.system_name = normalize_provider(system_name) or system_name self._content_mode = content_mode self.include_sensitive_data = include_sensitive_data and ( content_mode.capture_in_span or content_mode.capture_in_event ) effective_base_url = base_url or base_url_default self.base_url = effective_base_url # Agent information - prefer explicit overrides; otherwise defer to span data self.agent_name = agent_name self.agent_id = agent_id self.agent_description = agent_description self._agent_name_default = agent_name_default self._agent_id_default = agent_id_default self._agent_description_default = agent_description_default # Server information - use init parameters, then base_url inference self.server_address = server_address or server_address_default resolved_port = server_port if server_port is not None else server_port_default self.server_port = resolved_port # If server info not provided, try to extract from base_url if (not self.server_address or not self.server_port) and effective_base_url: server_attrs = _infer_server_attributes(effective_base_url) if not self.server_address: self.server_address = server_attrs.get(ServerAttributes.SERVER_ADDRESS) if not self.server_port: self.server_port = server_attrs.get(ServerAttributes.SERVER_PORT) # Content capture configuration self._capture_messages = content_mode.capture_in_span or content_mode.capture_in_event self._capture_system_instructions = True self._capture_tool_definitions = True # Span tracking self._root_spans: dict[str, OtelSpan] = {} self._otel_spans: dict[str, OtelSpan] = {} self._tokens: dict[str, object] = {} self._span_parents: dict[str, str | None] = {} self._agent_content: dict[str, dict[str, list[Any]]] = {} # Metrics configuration self._metrics_enabled = metrics_enabled self._meter = None self._duration_histogram: Histogram | None = None self._token_usage_histogram: Histogram | None = None if self._metrics_enabled: self._init_metrics() def _get_server_attributes(self) -> dict[str, Any]: """Get server attributes from configured values.""" attrs = {} if self.server_address: attrs[ServerAttributes.SERVER_ADDRESS] = self.server_address if self.server_port: attrs[ServerAttributes.SERVER_PORT] = self.server_port return attrs def _init_metrics(self): """Initialize metric instruments.""" self._meter = get_meter("opentelemetry.instrumentation.openai_agents", "0.1.0") # Operation duration histogram self._duration_histogram = self._meter.create_histogram( name="gen_ai.client.operation.duration", description="GenAI operation duration", unit="s", ) # Token usage histogram self._token_usage_histogram = self._meter.create_histogram( name="gen_ai.client.token.usage", description="Number of input and output tokens used", unit="{token}", ) def _record_metrics(self, span: Span[Any], attributes: dict[str, AttributeValue]) -> None: """Record metrics for the span.""" if not self._metrics_enabled or (self._duration_histogram is None and self._token_usage_histogram is None): return try: # Calculate duration duration = None if hasattr(span, "started_at") and hasattr(span, "ended_at"): try: start = datetime.fromisoformat(span.started_at) end = datetime.fromisoformat(span.ended_at) duration = (end - start).total_seconds() except Exception: pass # Build metric attributes metric_attrs = { GEN_AI_PROVIDER_NAME: attributes.get(GEN_AI_PROVIDER_NAME), GEN_AI_OPERATION_NAME: attributes.get(GEN_AI_OPERATION_NAME), GEN_AI_REQUEST_MODEL: (attributes.get(GEN_AI_REQUEST_MODEL) or attributes.get(GEN_AI_RESPONSE_MODEL)), ServerAttributes.SERVER_ADDRESS: attributes.get(ServerAttributes.SERVER_ADDRESS), ServerAttributes.SERVER_PORT: attributes.get(ServerAttributes.SERVER_PORT), } # Add error type if present if error := getattr(span, "error", None): error_type = error.get("type") or error.get("name") if error_type: metric_attrs["error.type"] = error_type # Remove None values metric_attrs = {k: v for k, v in metric_attrs.items() if v is not None} # Record duration if duration is not None and self._duration_histogram is not None: self._duration_histogram.record(duration, metric_attrs) # Record token usage if self._token_usage_histogram: input_tokens = attributes.get(GEN_AI_USAGE_INPUT_TOKENS) if isinstance(input_tokens, (int, float)): token_attrs = dict(metric_attrs) token_attrs[GEN_AI_TOKEN_TYPE] = "input" self._token_usage_histogram.record(input_tokens, token_attrs) output_tokens = attributes.get(GEN_AI_USAGE_OUTPUT_TOKENS) if isinstance(output_tokens, (int, float)): token_attrs = dict(metric_attrs) token_attrs[GEN_AI_TOKEN_TYPE] = "output" self._token_usage_histogram.record(output_tokens, token_attrs) except Exception as e: logger.debug("Failed to record metrics: %s", e) def _emit_content_events( self, span: Span[Any], otel_span: OtelSpan, payload: ContentPayload, agent_content: dict[str, list[Any]] | None = None, ) -> None: """Intentionally skip emitting gen_ai.* events to avoid payload duplication.""" if not self.include_sensitive_data or not self._content_mode.capture_in_event or not otel_span.is_recording(): return logger.debug( "Event capture requested for span %s but is currently disabled", getattr(span, "span_id", ""), ) return def _collect_system_instructions(self, messages: Sequence[Any] | None) -> list[dict[str, str]]: """Return system/ai role instructions as typed text objects. Enforces format: [{"type": "text", "content": "..."}]. Handles message content that may be a string, list of parts, or a dict with text/content fields. """ if not messages: return [] out: list[dict[str, str]] = [] for m in messages: if not isinstance(m, dict): continue role = m.get("role") if role in {"system", "ai"}: content = m.get("content") out.extend(self._normalize_to_text_parts(content)) return out def _normalize_to_text_parts(self, content: Any) -> list[dict[str, str]]: """Normalize arbitrary content into typed text parts. - String -> [{type: text, content: }] - List/Tuple -> map each item to a text part (string/dict supported) - Dict -> use 'text' or 'content' field when available; else str(dict) - Other -> str(value) """ parts: list[dict[str, str]] = [] if content is None: return parts if isinstance(content, str): parts.append({"type": "text", "content": content}) return parts if isinstance(content, (list, tuple)): for item in content: if isinstance(item, str): parts.append({"type": "text", "content": item}) elif isinstance(item, dict): txt = item.get("text") or item.get("content") if isinstance(txt, str) and txt: parts.append({"type": "text", "content": txt}) else: parts.append({"type": "text", "content": str(item)}) else: parts.append({"type": "text", "content": str(item)}) return parts if isinstance(content, dict): txt = content.get("text") or content.get("content") if isinstance(txt, str) and txt: parts.append({"type": "text", "content": txt}) else: parts.append({"type": "text", "content": str(content)}) return parts # Fallback for other types parts.append({"type": "text", "content": str(content)}) return parts def _redacted_text_parts(self) -> list[dict[str, str]]: """Return a single redacted text part for system instructions.""" return [{"type": "text", "content": "readacted"}] def _normalize_messages_to_role_parts(self, messages: Sequence[Any] | None) -> list[dict[str, Any]]: """Normalize input messages to enforced role+parts schema. Each message becomes: {"role": , "parts": [ {"type": ..., ...} ]} Redaction: when include_sensitive_data is False, replace text content, tool_call arguments, and tool_call_response result with "readacted". """ if not messages: return [] normalized: list[dict[str, Any]] = [] for m in messages: if not isinstance(m, dict): # Fallback: treat as user text normalized.append( { "role": "user", "parts": [ { "type": "text", "content": "readacted" if not self.include_sensitive_data else str(m), } ], } ) continue role = m.get("role") or "user" parts: list[dict[str, Any]] = [] # Existing parts array if isinstance(m.get("parts"), (list, tuple)): for p in m["parts"]: if isinstance(p, dict): ptype = p.get("type") or "text" newp: dict[str, Any] = {"type": ptype} if ptype == "text": txt = p.get("content") or p.get("text") newp["content"] = ( "readacted" if not self.include_sensitive_data else (txt if isinstance(txt, str) else str(p)) ) elif ptype == "tool_call": newp["id"] = p.get("id") newp["name"] = p.get("name") args = p.get("arguments") newp["arguments"] = "readacted" if not self.include_sensitive_data else args elif ptype == "tool_call_response": newp["id"] = p.get("id") or m.get("tool_call_id") result = p.get("result") or p.get("content") newp["result"] = "readacted" if not self.include_sensitive_data else result else: newp["content"] = "readacted" if not self.include_sensitive_data else str(p) parts.append(newp) else: parts.append( { "type": "text", "content": "readacted" if not self.include_sensitive_data else str(p), } ) # OpenAI content content = m.get("content") if isinstance(content, str): parts.append( { "type": "text", "content": "readacted" if not self.include_sensitive_data else content, } ) elif isinstance(content, (list, tuple)): for item in content: if isinstance(item, dict): itype = item.get("type") or "text" if itype == "text": txt = item.get("text") or item.get("content") parts.append( { "type": "text", "content": "readacted" if not self.include_sensitive_data else (txt if isinstance(txt, str) else str(item)), } ) else: # Fallback for other part types parts.append( { "type": "text", "content": "readacted" if not self.include_sensitive_data else str(item), } ) else: parts.append( { "type": "text", "content": "readacted" if not self.include_sensitive_data else str(item), } ) # Assistant tool_calls if role == "assistant" and isinstance(m.get("tool_calls"), (list, tuple)): for tc in m["tool_calls"]: if not isinstance(tc, dict): continue p = {"type": "tool_call"} p["id"] = tc.get("id") fn = tc.get("function") or {} if isinstance(fn, dict): p["name"] = fn.get("name") args = fn.get("arguments") p["arguments"] = "readacted" if not self.include_sensitive_data else args parts.append(p) # Tool call response if role in {"tool", "function"}: p = {"type": "tool_call_response"} p["id"] = m.get("tool_call_id") or m.get("id") result = m.get("result") or m.get("content") p["result"] = "readacted" if not self.include_sensitive_data else result parts.append(p) if parts: normalized.append({"role": role, "parts": parts}) elif not self.include_sensitive_data: normalized.append({"role": role, "parts": self._redacted_text_parts()}) return normalized def _normalize_output_messages_to_role_parts(self, span_data: Any) -> list[dict[str, Any]]: """Normalize output messages to enforced role+parts schema. Produces: [{"role": "assistant", "parts": [{"type": "text", "content": "..."}], optional "finish_reason": "..." }] """ messages: list[dict[str, Any]] = [] parts: list[dict[str, Any]] = [] finish_reason: str | None = None # Response span: prefer consolidated output_text response = getattr(span_data, "response", None) if response is not None: # Collect text content output_text = getattr(response, "output_text", None) if isinstance(output_text, str) and output_text: parts.append( { "type": "text", "content": ("readacted" if not self.include_sensitive_data else output_text), } ) else: output = getattr(response, "output", None) if isinstance(output, Sequence): for item in output: # ResponseOutputMessage may have a string representation txt = getattr(item, "content", None) if isinstance(txt, str) and txt: parts.append( { "type": "text", "content": ("readacted" if not self.include_sensitive_data else txt), } ) else: # Fallback: stringified parts.append( { "type": "text", "content": ("readacted" if not self.include_sensitive_data else str(item)), } ) # Capture finish_reason from parts when present fr = getattr(item, "finish_reason", None) if isinstance(fr, str) and not finish_reason: finish_reason = fr # Generation span: use span_data.output if not parts: output = getattr(span_data, "output", None) if isinstance(output, Sequence): for item in output: if isinstance(item, dict): if item.get("type") == "text": txt = item.get("content") or item.get("text") if isinstance(txt, str) and txt: parts.append( { "type": "text", "content": ("readacted" if not self.include_sensitive_data else txt), } ) elif "content" in item and isinstance(item["content"], str): parts.append( { "type": "text", "content": ("readacted" if not self.include_sensitive_data else item["content"]), } ) else: parts.append( { "type": "text", "content": ("readacted" if not self.include_sensitive_data else str(item)), } ) if not finish_reason and isinstance(item.get("finish_reason"), str): finish_reason = item.get("finish_reason") elif isinstance(item, str): parts.append( { "type": "text", "content": ("readacted" if not self.include_sensitive_data else item), } ) else: parts.append( { "type": "text", "content": ("readacted" if not self.include_sensitive_data else str(item)), } ) # Build assistant message msg: dict[str, Any] = {"role": "assistant", "parts": parts} if finish_reason: msg["finish_reason"] = finish_reason # Only include if there is content if parts: messages.append(msg) return messages def _build_content_payload(self, span: Span[Any]) -> ContentPayload: """Normalize content from span data for attribute/event capture.""" payload = ContentPayload() span_data = getattr(span, "span_data", None) if span_data is None or not self.include_sensitive_data: return payload capture_messages = self._capture_messages and ( self._content_mode.capture_in_span or self._content_mode.capture_in_event ) capture_system = self._capture_system_instructions and ( self._content_mode.capture_in_span or self._content_mode.capture_in_event ) capture_tools = self._content_mode.capture_in_span or ( self._content_mode.capture_in_event and _is_instance_of(span_data, FunctionSpanData) ) if _is_instance_of(span_data, GenerationSpanData): span_input = getattr(span_data, "input", None) if capture_messages and span_input: payload.input_messages = self._normalize_messages_to_role_parts(span_input) if capture_system and span_input: sys_instr = self._collect_system_instructions(span_input) if sys_instr: payload.system_instructions = sys_instr if capture_messages and (getattr(span_data, "output", None) or getattr(span_data, "response", None)): normalized_out = self._normalize_output_messages_to_role_parts(span_data) if normalized_out: payload.output_messages = normalized_out elif _is_instance_of(span_data, ResponseSpanData): span_input = getattr(span_data, "input", None) response_obj = getattr(span_data, "response", None) if capture_messages and span_input: payload.input_messages = self._normalize_messages_to_role_parts(span_input) if capture_system and response_obj and hasattr(response_obj, "instructions"): payload.system_instructions = self._normalize_to_text_parts(response_obj.instructions) if capture_system and span_input: sys_instr = self._collect_system_instructions(span_input) if sys_instr: payload.system_instructions = sys_instr if capture_messages: normalized_out = self._normalize_output_messages_to_role_parts(span_data) if normalized_out: payload.output_messages = normalized_out elif _is_instance_of(span_data, FunctionSpanData) and capture_tools: def _serialize_tool_value(value: Any) -> str | None: if value is None: return None if isinstance(value, (dict, list)): return safe_json_dumps(value) return str(value) payload.tool_arguments = _serialize_tool_value(getattr(span_data, "input", None)) payload.tool_result = _serialize_tool_value(getattr(span_data, "output", None)) return payload def _find_agent_parent_span_id(self, span_id: str | None) -> str | None: """Return nearest ancestor span id that represents an agent.""" current = span_id visited: set[str] = set() while current: if current in visited: break visited.add(current) if current in self._agent_content: return current current = self._span_parents.get(current) return None def _update_agent_aggregate(self, span: Span[Any], payload: ContentPayload) -> None: """Accumulate child span content for parent agent span.""" agent_id = self._find_agent_parent_span_id(span.parent_id) if not agent_id: return entry = self._agent_content.setdefault( agent_id, { "input_messages": [], "output_messages": [], "system_instructions": [], "request_model": None, }, ) if payload.input_messages: entry["input_messages"] = self._merge_content_sequence(entry["input_messages"], payload.input_messages) if payload.output_messages: entry["output_messages"] = self._merge_content_sequence(entry["output_messages"], payload.output_messages) if payload.system_instructions: entry["system_instructions"] = self._merge_content_sequence( entry["system_instructions"], payload.system_instructions ) if not entry.get("request_model"): model = getattr(span.span_data, "model", None) if not model: response_obj = getattr(span.span_data, "response", None) model = getattr(response_obj, "model", None) if model: entry["request_model"] = model def _infer_output_type(self, span_data: Any) -> str: """Infer gen_ai.output.type for multiple span kinds.""" if _is_instance_of(span_data, FunctionSpanData): # Tool results are typically JSON return GenAIOutputType.JSON if _is_instance_of(span_data, TranscriptionSpanData): return GenAIOutputType.TEXT if _is_instance_of(span_data, SpeechSpanData): return GenAIOutputType.SPEECH if _is_instance_of(span_data, GuardrailSpanData): return GenAIOutputType.TEXT if _is_instance_of(span_data, HandoffSpanData): return GenAIOutputType.TEXT # Check for embeddings operation if _is_instance_of(span_data, GenerationSpanData): if hasattr(span_data, "embedding_dimension"): return GenAIOutputType.TEXT # Embeddings are numeric but represented as text # Generation/Response - check output structure output = getattr(span_data, "output", None) or getattr(getattr(span_data, "response", None), "output", None) if isinstance(output, Sequence) and output: first = output[0] if isinstance(first, dict): item_type = first.get("type") if isinstance(item_type, str): normalized = item_type.strip().lower() if normalized in {"image", "image_url"}: return GenAIOutputType.IMAGE if normalized in {"audio", "speech", "audio_url"}: return GenAIOutputType.SPEECH if normalized in { "json", "json_object", "jsonschema", "function_call", "tool_call", "tool_result", }: return GenAIOutputType.JSON if normalized in { "text", "output_text", "message", "assistant", }: return GenAIOutputType.TEXT # Conversation style payloads if "role" in first: parts = first.get("parts") if isinstance(parts, Sequence) and parts: # If all parts are textual (or missing explicit type), treat as text textual = True for part in parts: if isinstance(part, dict): part_type = str(part.get("type", "")).lower() if part_type in {"image", "image_url"}: return GenAIOutputType.IMAGE if part_type in { "audio", "speech", "audio_url", }: return GenAIOutputType.SPEECH if part_type and part_type not in { "text", "output_text", "assistant", }: textual = False elif not isinstance(part, str): textual = False if textual: return GenAIOutputType.TEXT content_value = first.get("content") if isinstance(content_value, str): return GenAIOutputType.TEXT # Detect structured data without explicit type json_like_keys = { "schema", "properties", "arguments", "result", "data", "json", "output_json", } if json_like_keys.intersection(first.keys()): return GenAIOutputType.JSON return GenAIOutputType.TEXT @staticmethod def _sanitize_usage_payload(usage: Any) -> None: """Remove non-spec usage fields (e.g., total tokens) in-place.""" if not usage: return if isinstance(usage, dict): usage.pop("total_tokens", None) return if hasattr(usage, "total_tokens"): try: usage.total_tokens = None except Exception: # pragma: no cover - defensive try: delattr(usage, "total_tokens") except Exception: # pragma: no cover - defensive pass def _get_span_kind(self, span_data: Any) -> SpanKind: """Determine appropriate span kind based on span data type.""" if _is_instance_of(span_data, FunctionSpanData): return SpanKind.INTERNAL # Tool execution is internal if _is_instance_of( span_data, ( GenerationSpanData, ResponseSpanData, TranscriptionSpanData, SpeechSpanData, ), ): return SpanKind.CLIENT # API calls to model providers if _is_instance_of(span_data, AgentSpanData): return SpanKind.CLIENT if _is_instance_of(span_data, (GuardrailSpanData, HandoffSpanData)): return SpanKind.INTERNAL # Agent operations are internal return SpanKind.INTERNAL def on_trace_start(self, trace: Trace) -> None: """Create root span when trace starts.""" if self._tracer: attributes = { GEN_AI_PROVIDER_NAME: self.system_name, GEN_AI_SYSTEM_KEY: self.system_name, GEN_AI_OPERATION_NAME: GenAIOperationName.INVOKE_AGENT, } # Legacy emission removed # Add configured agent and server attributes if self.agent_name: attributes[GEN_AI_AGENT_NAME] = self.agent_name if self.agent_id: attributes[GEN_AI_AGENT_ID] = self.agent_id if self.agent_description: attributes[GEN_AI_AGENT_DESCRIPTION] = self.agent_description attributes.update(self._get_server_attributes()) otel_span = self._tracer.start_span( name=trace.name, attributes=attributes, kind=SpanKind.SERVER, # Root span is typically server ) self._root_spans[trace.trace_id] = otel_span def on_trace_end(self, trace: Trace) -> None: """End root span when trace ends.""" if root_span := self._root_spans.pop(trace.trace_id, None): if root_span.is_recording(): root_span.set_status(Status(StatusCode.OK)) root_span.end() self._cleanup_spans_for_trace(trace.trace_id) def on_span_start(self, span: Span[Any]) -> None: """Start child span for agent span.""" if not self._tracer or not span.started_at: return self._span_parents[span.span_id] = span.parent_id if _is_instance_of(span.span_data, AgentSpanData) and span.span_id not in self._agent_content: self._agent_content[span.span_id] = { "input_messages": [], "output_messages": [], "system_instructions": [], "request_model": None, } parent_span = self._otel_spans.get(span.parent_id) if span.parent_id else self._root_spans.get(span.trace_id) context = set_span_in_context(parent_span) if parent_span else None # Get operation details for span naming operation_name = self._get_operation_name(span.span_data) model = getattr(span.span_data, "model", None) if model is None: response_obj = getattr(span.span_data, "response", None) model = getattr(response_obj, "model", None) # Use configured agent name or get from span data agent_name = self.agent_name if not agent_name and _is_instance_of(span.span_data, AgentSpanData): agent_name = getattr(span.span_data, "name", None) if not agent_name: agent_name = self._agent_name_default tool_name = getattr(span.span_data, "name", None) if _is_instance_of(span.span_data, FunctionSpanData) else None # Generate spec-compliant span name span_name = get_span_name(operation_name, model, agent_name, tool_name) attributes = { GEN_AI_PROVIDER_NAME: self.system_name, GEN_AI_SYSTEM_KEY: self.system_name, GEN_AI_OPERATION_NAME: operation_name, } # Legacy emission removed # Add configured agent and server attributes agent_name_override = self.agent_name or self._agent_name_default agent_id_override = self.agent_id or self._agent_id_default agent_desc_override = self.agent_description or self._agent_description_default if agent_name_override: attributes[GEN_AI_AGENT_NAME] = agent_name_override if agent_id_override: attributes[GEN_AI_AGENT_ID] = agent_id_override if agent_desc_override: attributes[GEN_AI_AGENT_DESCRIPTION] = agent_desc_override attributes.update(self._get_server_attributes()) otel_span = self._tracer.start_span( name=span_name, context=context, attributes=attributes, kind=self._get_span_kind(span.span_data), ) self._otel_spans[span.span_id] = otel_span self._tokens[span.span_id] = attach(set_span_in_context(otel_span)) def on_span_end(self, span: Span[Any]) -> None: """Finalize span with attributes, events, and metrics.""" if token := self._tokens.pop(span.span_id, None): detach(token) payload = self._build_content_payload(span) self._update_agent_aggregate(span, payload) agent_content = ( self._agent_content.get(span.span_id) if _is_instance_of(span.span_data, AgentSpanData) else None ) if not (otel_span := self._otel_spans.pop(span.span_id, None)): # Log attributes even without OTel span try: attributes = dict(self._extract_genai_attributes(span, payload, agent_content)) for key, value in attributes.items(): logger.debug("GenAI attr span %s: %s=%s", span.span_id, key, value) except Exception as e: logger.warning( "Failed to extract attributes for span %s: %s", span.span_id, e, ) if _is_instance_of(span.span_data, AgentSpanData): self._agent_content.pop(span.span_id, None) self._span_parents.pop(span.span_id, None) return try: # Extract and set attributes attributes: dict[str, AttributeValue] = {} # Optimize for non-sampled spans to avoid heavy work if not otel_span.is_recording(): otel_span.end() return for key, value in self._extract_genai_attributes(span, payload, agent_content): otel_span.set_attribute(key, value) attributes[key] = value if _is_instance_of(span.span_data, (GenerationSpanData, ResponseSpanData)): operation_name = attributes.get(GEN_AI_OPERATION_NAME) model_for_name = attributes.get(GEN_AI_REQUEST_MODEL) or (attributes.get(GEN_AI_RESPONSE_MODEL)) if operation_name and model_for_name: agent_name_for_name = attributes.get(GEN_AI_AGENT_NAME) tool_name_for_name = attributes.get(GEN_AI_TOOL_NAME) new_name = get_span_name( operation_name, model_for_name, agent_name_for_name, tool_name_for_name, ) if new_name != otel_span.name: otel_span.update_name(new_name) # Emit span events for captured content when configured self._emit_content_events(span, otel_span, payload, agent_content) # Emit operation details event if configured # Set error status if applicable otel_span.set_status(status=_get_span_status(span)) if getattr(span, "error", None): err_obj = span.error err_type = err_obj.get("type") or err_obj.get("name") if err_type: otel_span.set_attribute("error.type", err_type) # Record metrics before ending span self._record_metrics(span, attributes) # End the span otel_span.end() except Exception as e: logger.warning("Failed to enrich span %s: %s", span.span_id, e) otel_span.set_status(Status(StatusCode.ERROR, str(e))) otel_span.end() finally: if _is_instance_of(span.span_data, AgentSpanData): self._agent_content.pop(span.span_id, None) self._span_parents.pop(span.span_id, None) def shutdown(self) -> None: """Clean up resources on shutdown.""" for span_id, otel_span in list(self._otel_spans.items()): otel_span.set_status(Status(StatusCode.ERROR, "Application shutdown")) otel_span.end() for trace_id, root_span in list(self._root_spans.items()): root_span.set_status(Status(StatusCode.ERROR, "Application shutdown")) root_span.end() self._otel_spans.clear() self._root_spans.clear() self._tokens.clear() self._span_parents.clear() self._agent_content.clear() def force_flush(self) -> None: """Force flush (no-op for this processor).""" def _get_operation_name(self, span_data: Any) -> str: """Determine operation name from span data type.""" if _is_instance_of(span_data, GenerationSpanData): # Check if it's embeddings if hasattr(span_data, "embedding_dimension"): return GenAIOperationName.EMBEDDINGS # Check if it's chat or completion if span_data.input: first_input = span_data.input[0] if span_data.input else None if isinstance(first_input, dict) and "role" in first_input: return GenAIOperationName.CHAT return GenAIOperationName.TEXT_COMPLETION if _is_instance_of(span_data, AgentSpanData): # The OpenAI Agents SDK AgentSpanData has no "operation" field; # agent spans always represent invoke_agent. return GenAIOperationName.INVOKE_AGENT if _is_instance_of(span_data, FunctionSpanData): return GenAIOperationName.EXECUTE_TOOL if _is_instance_of(span_data, ResponseSpanData): return GenAIOperationName.CHAT # Response typically from chat if _is_instance_of(span_data, TranscriptionSpanData): return GenAIOperationName.TRANSCRIPTION if _is_instance_of(span_data, SpeechSpanData): return GenAIOperationName.SPEECH if _is_instance_of(span_data, GuardrailSpanData): return GenAIOperationName.GUARDRAIL if _is_instance_of(span_data, HandoffSpanData): return GenAIOperationName.HANDOFF return "unknown" def _extract_genai_attributes( self, span: Span[Any], payload: ContentPayload, agent_content: dict[str, list[Any]] | None = None, ) -> Iterator[tuple[str, AttributeValue]]: """Yield (attr, value) pairs for GenAI semantic conventions.""" span_data = span.span_data # Base attributes yield GEN_AI_PROVIDER_NAME, self.system_name yield GEN_AI_SYSTEM_KEY, self.system_name # Legacy emission removed # Add configured agent attributes (always include when set) agent_name_override = self.agent_name or self._agent_name_default agent_id_override = self.agent_id or self._agent_id_default agent_desc_override = self.agent_description or self._agent_description_default if agent_name_override: yield GEN_AI_AGENT_NAME, agent_name_override if agent_id_override: yield GEN_AI_AGENT_ID, agent_id_override if agent_desc_override: yield GEN_AI_AGENT_DESCRIPTION, agent_desc_override # Server attributes for key, value in self._get_server_attributes().items(): yield key, value # Process different span types if _is_instance_of(span_data, GenerationSpanData): yield from self._get_attributes_from_generation_span_data(span_data, payload) elif _is_instance_of(span_data, AgentSpanData): yield from self._get_attributes_from_agent_span_data(span_data, agent_content) elif _is_instance_of(span_data, FunctionSpanData): yield from self._get_attributes_from_function_span_data(span_data, payload) elif _is_instance_of(span_data, ResponseSpanData): yield from self._get_attributes_from_response_span_data(span_data, payload) elif _is_instance_of(span_data, TranscriptionSpanData): yield from self._get_attributes_from_transcription_span_data(span_data) elif _is_instance_of(span_data, SpeechSpanData): yield from self._get_attributes_from_speech_span_data(span_data) elif _is_instance_of(span_data, GuardrailSpanData): yield from self._get_attributes_from_guardrail_span_data(span_data) elif _is_instance_of(span_data, HandoffSpanData): yield from self._get_attributes_from_handoff_span_data(span_data) def _get_attributes_from_generation_span_data( self, span_data: GenerationSpanData, payload: ContentPayload ) -> Iterator[tuple[str, AttributeValue]]: """Extract attributes from generation span.""" # Operation name operation_name = self._get_operation_name(span_data) yield GEN_AI_OPERATION_NAME, operation_name # Model information if span_data.model: yield GEN_AI_REQUEST_MODEL, span_data.model # Check for embeddings-specific attributes if hasattr(span_data, "embedding_dimension"): yield ( GEN_AI_EMBEDDINGS_DIMENSION_COUNT, span_data.embedding_dimension, ) # Check for data source if hasattr(span_data, "data_source_id"): yield GEN_AI_DATA_SOURCE_ID, span_data.data_source_id finish_reasons: list[Any] = [] if span_data.output: for part in span_data.output: if isinstance(part, dict): fr = part.get("finish_reason") or part.get("stop_reason") else: fr = getattr(part, "finish_reason", None) if fr: finish_reasons.append(fr if isinstance(fr, str) else str(fr)) if finish_reasons: yield GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons # Usage information if span_data.usage: usage = span_data.usage self._sanitize_usage_payload(usage) if "prompt_tokens" in usage or "input_tokens" in usage: tokens = usage.get("prompt_tokens") or usage.get("input_tokens") if tokens is not None: yield GEN_AI_USAGE_INPUT_TOKENS, tokens if "completion_tokens" in usage or "output_tokens" in usage: tokens = usage.get("completion_tokens") or usage.get("output_tokens") if tokens is not None: yield GEN_AI_USAGE_OUTPUT_TOKENS, tokens # Model configuration if span_data.model_config: mc = span_data.model_config param_map = { "temperature": GEN_AI_REQUEST_TEMPERATURE, "top_p": GEN_AI_REQUEST_TOP_P, "top_k": GEN_AI_REQUEST_TOP_K, "max_tokens": GEN_AI_REQUEST_MAX_TOKENS, "presence_penalty": GEN_AI_REQUEST_PRESENCE_PENALTY, "frequency_penalty": GEN_AI_REQUEST_FREQUENCY_PENALTY, "seed": GEN_AI_REQUEST_SEED, "n": GEN_AI_REQUEST_CHOICE_COUNT, "stop": GEN_AI_REQUEST_STOP_SEQUENCES, "encoding_formats": GEN_AI_REQUEST_ENCODING_FORMATS, } for k, attr in param_map.items(): if hasattr(mc, "__contains__") and k in mc: value = mc[k] else: value = getattr(mc, k, None) if value is not None: yield attr, value if hasattr(mc, "get"): base_url = mc.get("base_url") or mc.get("baseUrl") or mc.get("endpoint") else: base_url = ( getattr(mc, "base_url", None) or getattr(mc, "baseUrl", None) or getattr(mc, "endpoint", None) ) for key, value in _infer_server_attributes(base_url).items(): yield key, value # Sensitive data capture if ( self.include_sensitive_data and self._content_mode.capture_in_span and self._capture_messages and payload.input_messages ): yield ( GEN_AI_INPUT_MESSAGES, safe_json_dumps(payload.input_messages), ) if ( self.include_sensitive_data and self._content_mode.capture_in_span and self._capture_system_instructions and payload.system_instructions ): yield ( GEN_AI_SYSTEM_INSTRUCTIONS, safe_json_dumps(payload.system_instructions), ) if ( self.include_sensitive_data and self._content_mode.capture_in_span and self._capture_messages and payload.output_messages ): yield ( GEN_AI_OUTPUT_MESSAGES, safe_json_dumps(payload.output_messages), ) # Output type yield ( GEN_AI_OUTPUT_TYPE, normalize_output_type(self._infer_output_type(span_data)), ) def _merge_content_sequence( self, existing: list[Any], incoming: Sequence[Any], ) -> list[Any]: """Merge normalized message/content lists without duplicating snapshots.""" if not incoming: return existing incoming_list = [self._clone_message(item) for item in incoming] if self.include_sensitive_data: filtered = [msg for msg in incoming_list if not self._is_placeholder_message(msg)] if filtered: incoming_list = filtered if not existing: return incoming_list result = [self._clone_message(item) for item in existing] for idx, new_msg in enumerate(incoming_list): if idx < len(result): if ( self.include_sensitive_data and self._is_placeholder_message(new_msg) and not self._is_placeholder_message(result[idx]) ): continue if result[idx] != new_msg: result[idx] = self._clone_message(new_msg) else: if self.include_sensitive_data and self._is_placeholder_message(new_msg): if ( any(not self._is_placeholder_message(existing_msg) for existing_msg in result) or new_msg in result ): continue result.append(self._clone_message(new_msg)) return result def _clone_message(self, message: Any) -> Any: if isinstance(message, dict): return { key: self._clone_message(value) if isinstance(value, (dict, list)) else value for key, value in message.items() } if isinstance(message, list): return [self._clone_message(item) for item in message] return message def _is_placeholder_message(self, message: Any) -> bool: if not isinstance(message, dict): return False parts = message.get("parts") if not isinstance(parts, list) or not parts: return False for part in parts: if not isinstance(part, dict) or part.get("type") != "text" or part.get("content") != "readacted": return False return True def _get_attributes_from_agent_span_data( self, span_data: AgentSpanData, agent_content: dict[str, list[Any]] | None = None, ) -> Iterator[tuple[str, AttributeValue]]: """Extract attributes from agent span.""" yield GEN_AI_OPERATION_NAME, self._get_operation_name(span_data) name = self.agent_name or getattr(span_data, "name", None) or self._agent_name_default if name: yield GEN_AI_AGENT_NAME, name # agent_id and description are not available on the OpenAI Agents SDK # AgentSpanData; only use user-configured overrides. agent_id = self.agent_id or self._agent_id_default if agent_id: yield GEN_AI_AGENT_ID, agent_id description = self.agent_description or self._agent_description_default if description: yield GEN_AI_AGENT_DESCRIPTION, description # The OpenAI Agents SDK AgentSpanData has no "model" field; fall back to # the model aggregated from child generation/response spans. model = None if agent_content: model = agent_content.get("request_model") if model: yield GEN_AI_REQUEST_MODEL, model if hasattr(span_data, "conversation_id") and span_data.conversation_id: yield GEN_AI_CONVERSATION_ID, span_data.conversation_id # Agent definitions if self._capture_tool_definitions and hasattr(span_data, "agent_definitions"): yield ( GEN_AI_ORCHESTRATOR_AGENT_DEFINITIONS, safe_json_dumps(span_data.agent_definitions), ) # System instructions from agent definitions if self._capture_system_instructions and hasattr(span_data, "agent_definitions"): try: defs = span_data.agent_definitions if isinstance(defs, (list, tuple)): collected: list[dict[str, str]] = [] for d in defs: if isinstance(d, dict): msgs = d.get("messages") or d.get("system_messages") if isinstance(msgs, (list, tuple)): collected.extend(self._collect_system_instructions(msgs)) if collected: yield ( GEN_AI_SYSTEM_INSTRUCTIONS, safe_json_dumps(collected), ) except Exception: pass if ( self.include_sensitive_data and self._content_mode.capture_in_span and self._capture_messages and agent_content ): if agent_content.get("input_messages"): yield ( GEN_AI_INPUT_MESSAGES, safe_json_dumps(agent_content["input_messages"]), ) if agent_content.get("output_messages"): yield ( GEN_AI_OUTPUT_MESSAGES, safe_json_dumps(agent_content["output_messages"]), ) if ( self.include_sensitive_data and self._content_mode.capture_in_span and self._capture_system_instructions and agent_content and agent_content.get("system_instructions") ): yield ( GEN_AI_SYSTEM_INSTRUCTIONS, safe_json_dumps(agent_content["system_instructions"]), ) yield ( GEN_AI_OUTPUT_TYPE, normalize_output_type(self._infer_output_type(span_data)), ) def _get_attributes_from_function_span_data( self, span_data: FunctionSpanData, payload: ContentPayload ) -> Iterator[tuple[str, AttributeValue]]: """Extract attributes from function/tool span.""" yield GEN_AI_OPERATION_NAME, GenAIOperationName.EXECUTE_TOOL if span_data.name: yield GEN_AI_TOOL_NAME, span_data.name # Tool type - validate and normalize tool_type = "function" # Default for function spans if hasattr(span_data, "tool_type"): tool_type = span_data.tool_type yield GEN_AI_TOOL_TYPE, validate_tool_type(tool_type) if hasattr(span_data, "call_id") and span_data.call_id: yield GEN_AI_TOOL_CALL_ID, span_data.call_id if hasattr(span_data, "description") and span_data.description: yield GEN_AI_TOOL_DESCRIPTION, span_data.description # Tool definitions if self._capture_tool_definitions and hasattr(span_data, "tool_definitions"): yield ( GEN_AI_TOOL_DEFINITIONS, safe_json_dumps(span_data.tool_definitions), ) # Tool input/output (sensitive) if self.include_sensitive_data and self._content_mode.capture_in_span and payload.tool_arguments is not None: yield GEN_AI_TOOL_CALL_ARGUMENTS, payload.tool_arguments if self.include_sensitive_data and self._content_mode.capture_in_span and payload.tool_result is not None: yield GEN_AI_TOOL_CALL_RESULT, payload.tool_result yield ( GEN_AI_OUTPUT_TYPE, normalize_output_type(self._infer_output_type(span_data)), ) def _get_attributes_from_response_span_data( self, span_data: ResponseSpanData, payload: ContentPayload ) -> Iterator[tuple[str, AttributeValue]]: """Extract attributes from response span.""" yield GEN_AI_OPERATION_NAME, GenAIOperationName.CHAT # Response information if span_data.response: if hasattr(span_data.response, "id") and span_data.response.id: yield GEN_AI_RESPONSE_ID, span_data.response.id # Model from response if hasattr(span_data.response, "model") and span_data.response.model: yield GEN_AI_RESPONSE_MODEL, span_data.response.model if not getattr(span_data, "model", None): yield GEN_AI_REQUEST_MODEL, span_data.response.model # Finish reasons finish_reasons = [] if hasattr(span_data.response, "output") and span_data.response.output: for part in span_data.response.output: if isinstance(part, dict): fr = part.get("finish_reason") or part.get("stop_reason") else: fr = getattr(part, "finish_reason", None) if fr: finish_reasons.append(fr) if finish_reasons: yield GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons # Usage from response if hasattr(span_data.response, "usage") and span_data.response.usage: usage = span_data.response.usage self._sanitize_usage_payload(usage) input_tokens = getattr(usage, "input_tokens", None) if input_tokens is None: input_tokens = getattr(usage, "prompt_tokens", None) if input_tokens is not None: yield GEN_AI_USAGE_INPUT_TOKENS, input_tokens output_tokens = getattr(usage, "output_tokens", None) if output_tokens is None: output_tokens = getattr(usage, "completion_tokens", None) if output_tokens is not None: yield GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens # Tool definitions from response if self._capture_tool_definitions and hasattr(span_data.response, "tools"): yield ( GEN_AI_TOOL_DEFINITIONS, safe_json_dumps( list( map( lambda tool: tool.to_dict(), span_data.response.tools, ) ) ), ) # Input/output messages if ( self.include_sensitive_data and self._content_mode.capture_in_span and self._capture_messages and payload.input_messages ): yield ( GEN_AI_INPUT_MESSAGES, safe_json_dumps(payload.input_messages), ) if ( self.include_sensitive_data and self._content_mode.capture_in_span and self._capture_system_instructions and payload.system_instructions ): yield ( GEN_AI_SYSTEM_INSTRUCTIONS, safe_json_dumps(payload.system_instructions), ) if ( self.include_sensitive_data and self._content_mode.capture_in_span and self._capture_messages and payload.output_messages ): yield ( GEN_AI_OUTPUT_MESSAGES, safe_json_dumps(payload.output_messages), ) yield ( GEN_AI_OUTPUT_TYPE, normalize_output_type(self._infer_output_type(span_data)), ) def _get_attributes_from_transcription_span_data( self, span_data: TranscriptionSpanData ) -> Iterator[tuple[str, AttributeValue]]: """Extract attributes from transcription span.""" yield GEN_AI_OPERATION_NAME, GenAIOperationName.TRANSCRIPTION if hasattr(span_data, "model") and span_data.model: yield GEN_AI_REQUEST_MODEL, span_data.model # Audio format if hasattr(span_data, "format") and span_data.format: yield "gen_ai.audio.input.format", span_data.format # Transcript (sensitive) if ( self.include_sensitive_data and self._content_mode.capture_in_span and self._capture_messages and hasattr(span_data, "transcript") ): yield "gen_ai.transcription.text", span_data.transcript yield ( GEN_AI_OUTPUT_TYPE, normalize_output_type(self._infer_output_type(span_data)), ) def _get_attributes_from_speech_span_data(self, span_data: SpeechSpanData) -> Iterator[tuple[str, AttributeValue]]: """Extract attributes from speech span.""" yield GEN_AI_OPERATION_NAME, GenAIOperationName.SPEECH if hasattr(span_data, "model") and span_data.model: yield GEN_AI_REQUEST_MODEL, span_data.model if hasattr(span_data, "voice") and span_data.voice: yield "gen_ai.speech.voice", span_data.voice if hasattr(span_data, "format") and span_data.format: yield "gen_ai.audio.output.format", span_data.format # Input text (sensitive) if ( self.include_sensitive_data and self._content_mode.capture_in_span and self._capture_messages and hasattr(span_data, "input_text") ): yield "gen_ai.speech.input_text", span_data.input_text yield ( GEN_AI_OUTPUT_TYPE, normalize_output_type(self._infer_output_type(span_data)), ) def _get_attributes_from_guardrail_span_data( self, span_data: GuardrailSpanData ) -> Iterator[tuple[str, AttributeValue]]: """Extract attributes from guardrail span.""" yield GEN_AI_OPERATION_NAME, GenAIOperationName.GUARDRAIL if span_data.name: yield GEN_AI_GUARDRAIL_NAME, span_data.name yield GEN_AI_GUARDRAIL_TRIGGERED, span_data.triggered yield ( GEN_AI_OUTPUT_TYPE, normalize_output_type(self._infer_output_type(span_data)), ) def _get_attributes_from_handoff_span_data( self, span_data: HandoffSpanData ) -> Iterator[tuple[str, AttributeValue]]: """Extract attributes from handoff span.""" yield GEN_AI_OPERATION_NAME, GenAIOperationName.HANDOFF if span_data.from_agent: yield GEN_AI_HANDOFF_FROM_AGENT, span_data.from_agent if span_data.to_agent: yield GEN_AI_HANDOFF_TO_AGENT, span_data.to_agent yield ( GEN_AI_OUTPUT_TYPE, normalize_output_type(self._infer_output_type(span_data)), ) def _cleanup_spans_for_trace(self, trace_id: str) -> None: """Clean up spans for a trace to prevent memory leaks.""" spans_to_remove = [span_id for span_id in self._otel_spans.keys() if span_id.startswith(trace_id)] for span_id in spans_to_remove: if otel_span := self._otel_spans.pop(span_id, None): otel_span.set_status(Status(StatusCode.ERROR, "Trace ended before span completion")) otel_span.end() self._tokens.pop(span_id, None) __all__ = [ "ContentCaptureMode", "ContentPayload", "GenAIEvaluationAttributes", "GenAIOperationName", "GenAIOutputType", "GenAIProvider", "GenAISemanticProcessor", "GenAIToolType", "normalize_output_type", "normalize_provider", "validate_tool_type", ] version.py000066400000000000000000000001471527070072300510270ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.2.0.dev" tests/000077500000000000000000000000001527070072300363665ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2conftest.py000066400000000000000000000012021527070072300405600ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import sys from pathlib import Path TESTS_ROOT = Path(__file__).resolve().parent GENAI_ROOT = TESTS_ROOT.parent REPO_ROOT = GENAI_ROOT.parent PROJECT_ROOT = REPO_ROOT.parent for path in ( PROJECT_ROOT / "opentelemetry-instrumentation" / "src", GENAI_ROOT / "src", PROJECT_ROOT / "util" / "opentelemetry-util-genai" / "src", REPO_ROOT / "openai_agents_lib", REPO_ROOT / "openai_lib", TESTS_ROOT / "stubs", ): path_str = str(path) if path_str not in sys.path: sys.path.insert(0, path_str) requirements.latest.txt000066400000000000000000000034541527070072300431530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests# Copyright The OpenTelemetry Authors # # 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. # ******************************** # WARNING: NOT HERMETIC !!!!!!!!!! # ******************************** # # This "requirements.txt" is installed in conjunction # with multiple other dependencies in the top-level "tox.ini" # file. In particular, please see: # # openai_agents-latest: {[testenv]test_deps} # openai_agents-latest: -r {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/requirements.latest.txt # # This provides additional dependencies, namely: # # opentelemetry-api # opentelemetry-sdk # opentelemetry-semantic-conventions # # ... with a "dev" version based on the latest distribution. # This variant of the requirements aims to test the system using # the newest supported version of external dependencies. openai-agents==0.3.3 pydantic>=2.10,<3 httpx==0.27.2 Deprecated==1.2.14 importlib-metadata==6.11.0 packaging==24.0 pytest==7.4.4 pytest-asyncio==0.21.0 wrapt==1.16.0 # test with the latest version of opentelemetry-api, sdk, and semantic conventions grpcio>=1.60.0; implementation_name != "pypy" grpcio<1.60.0; implementation_name == "pypy" -e opentelemetry-instrumentation -e instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2 -e util/opentelemetry-util-genai requirements.oldest.txt000066400000000000000000000026441527070072300431510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests# Copyright The OpenTelemetry Authors # # 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. # This variant of the requirements aims to test the system using # the oldest supported version of external dependencies. openai-agents==0.3.3 pydantic>=2.10,<3 httpx==0.27.2 Deprecated==1.2.14 importlib-metadata==6.11.0 packaging==24.0 pytest==7.4.4 pytest-asyncio==0.21.0 wrapt==1.16.0 opentelemetry-exporter-otlp-proto-grpc~=1.30 opentelemetry-exporter-otlp-proto-http~=1.30 opentelemetry-api==1.40 # when updating, also update in pyproject.toml opentelemetry-sdk==1.40 # when updating, also update in pyproject.toml opentelemetry-semantic-conventions==0.61b0 # when updating, also update in pyproject.toml opentelemetry-util-genai==0.4b.0 # when updating, also update in pyproject.toml grpcio>=1.60.0; implementation_name != "pypy" grpcio<1.60.0; implementation_name == "pypy" -e instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2 stubs/000077500000000000000000000000001527070072300375265ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/testsagents/000077500000000000000000000000001527070072300410075ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/stubs__init__.py000066400000000000000000000001461527070072300431210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/stubs/agents# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Stub package for tests tracing/000077500000000000000000000000001527070072300424365ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/stubs/agents__init__.py000066400000000000000000000136371527070072300445610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/stubs/agents/tracing# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: skip-file from __future__ import annotations from collections.abc import Mapping, Sequence from contextlib import contextmanager from dataclasses import dataclass from itertools import count from typing import Any from .processor_interface import TracingProcessor from .spans import Span from .traces import Trace SPAN_TYPE_AGENT = "agent" SPAN_TYPE_FUNCTION = "function" SPAN_TYPE_GENERATION = "generation" SPAN_TYPE_RESPONSE = "response" __all__ = [ "AgentSpanData", "FunctionSpanData", "GenerationSpanData", "ResponseSpanData", "TraceProvider", "agent_span", "function_span", "generation_span", "get_trace_provider", "response_span", "set_trace_processors", "trace", ] @dataclass class AgentSpanData: name: str | None = None handoffs: list[str] | None = None tools: list[str] | None = None output_type: str | None = None @property def type(self) -> str: return SPAN_TYPE_AGENT @dataclass class FunctionSpanData: name: str | None = None input: Any = None output: Any = None @property def type(self) -> str: return SPAN_TYPE_FUNCTION @dataclass class GenerationSpanData: input: Sequence[Mapping[str, Any]] | None = None output: Sequence[Mapping[str, Any]] | None = None model: str | None = None model_config: Mapping[str, Any] | None = None usage: Mapping[str, Any] | None = None @property def type(self) -> str: return SPAN_TYPE_GENERATION @dataclass class ResponseSpanData: response: Any = None @property def type(self) -> str: return SPAN_TYPE_RESPONSE class _ProcessorFanout(TracingProcessor): def __init__(self) -> None: self._processors: list[TracingProcessor] = [] def add_tracing_processor(self, processor: TracingProcessor) -> None: self._processors.append(processor) def set_processors(self, processors: list[TracingProcessor]) -> None: self._processors = list(processors) def on_trace_start(self, trace: Trace) -> None: for processor in list(self._processors): processor.on_trace_start(trace) def on_trace_end(self, trace: Trace) -> None: for processor in list(self._processors): processor.on_trace_end(trace) def on_span_start(self, span: Span) -> None: for processor in list(self._processors): processor.on_span_start(span) def on_span_end(self, span: Span) -> None: for processor in list(self._processors): processor.on_span_end(span) def shutdown(self) -> None: for processor in list(self._processors): processor.shutdown() def force_flush(self) -> None: for processor in list(self._processors): processor.force_flush() class TraceProvider: def __init__(self) -> None: self._multi_processor = _ProcessorFanout() self._ids = count(1) def register_processor(self, processor: TracingProcessor) -> None: self._multi_processor.add_tracing_processor(processor) def set_processors(self, processors: list[TracingProcessor]) -> None: self._multi_processor.set_processors(processors) def create_trace( self, name: str, trace_id: str | None = None, group_id: str | None = None, metadata: Mapping[str, Any] | None = None, disabled: bool = False, ) -> Trace: trace_id = trace_id or f"trace_{next(self._ids)}" return Trace(name, trace_id, self._multi_processor) def create_span( self, span_data: Any, span_id: str | None = None, parent: Trace | Span | None = None, disabled: bool = False, ) -> Span: span_id = span_id or f"span_{next(self._ids)}" if isinstance(parent, Span): trace_id = parent.trace_id parent_id = parent.span_id elif isinstance(parent, Trace): trace_id = parent.trace_id parent_id = None else: trace_id = f"trace_{next(self._ids)}" parent_id = None return Span(trace_id, span_id, span_data, parent_id, self._multi_processor) def shutdown(self) -> None: self._multi_processor.shutdown() _PROVIDER = TraceProvider() _CURRENT_TRACE: Trace | None = None def get_trace_provider() -> TraceProvider: return _PROVIDER def set_trace_processors(processors: list[TracingProcessor]) -> None: _PROVIDER.set_processors(processors) @contextmanager def trace(name: str, **kwargs: Any): global _CURRENT_TRACE trace_obj = _PROVIDER.create_trace(name, **kwargs) previous = _CURRENT_TRACE _CURRENT_TRACE = trace_obj trace_obj.start() try: yield trace_obj finally: trace_obj.finish() _CURRENT_TRACE = previous @contextmanager def generation_span(**kwargs: Any): data = GenerationSpanData(**kwargs) span = _PROVIDER.create_span(data, parent=_CURRENT_TRACE) span.start() try: yield span finally: span.finish() @contextmanager def agent_span( name: str, handoffs: list[str] | None = None, tools: list[str] | None = None, output_type: str | None = None, **kwargs: Any, ): data = AgentSpanData(name=name, handoffs=handoffs, tools=tools, output_type=output_type) span = _PROVIDER.create_span(data, parent=_CURRENT_TRACE) span.start() try: yield span finally: span.finish() @contextmanager def function_span(**kwargs: Any): data = FunctionSpanData(**kwargs) span = _PROVIDER.create_span(data, parent=_CURRENT_TRACE) span.start() try: yield span finally: span.finish() @contextmanager def response_span(**kwargs: Any): data = ResponseSpanData(**kwargs) span = _PROVIDER.create_span(data, parent=_CURRENT_TRACE) span.start() try: yield span finally: span.finish() processor_interface.py000066400000000000000000000011161527070072300470460ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/stubs/agents/tracing# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations class TracingProcessor: def on_trace_start(self, trace): # pragma: no cover - stub pass def on_trace_end(self, trace): # pragma: no cover - stub pass def on_span_start(self, span): # pragma: no cover - stub pass def on_span_end(self, span): # pragma: no cover - stub pass def shutdown(self) -> None: # pragma: no cover - stub pass def force_flush(self) -> None: # pragma: no cover - stub pass spans.py000066400000000000000000000022101527070072300441270ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/stubs/agents/tracing# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from datetime import datetime from typing import Any class Span: def __init__( self, trace_id: str, span_id: str, span_data: Any, parent_id: str | None, processor, ) -> None: self.trace_id = trace_id self.span_id = span_id self.span_data = span_data self.parent_id = parent_id self.started_at: str | None = None self.ended_at: str | None = None self.error = None self._processor = processor def start(self) -> None: if self.started_at is not None: return self.started_at = datetime.utcnow().isoformat() + "Z" self._processor.on_span_start(self) def finish(self) -> None: if self.ended_at is not None: return self.ended_at = datetime.utcnow().isoformat() + "Z" self._processor.on_span_end(self) def __enter__(self) -> Span: self.start() return self def __exit__(self, exc_type, exc, tb) -> None: self.finish() traces.py000066400000000000000000000016561527070072300443010ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/stubs/agents/tracing# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from datetime import datetime class Trace: def __init__(self, name: str, trace_id: str, processor) -> None: self.name = name self.trace_id = trace_id self._processor = processor self.started_at: str | None = None self.ended_at: str | None = None def start(self) -> None: if self.started_at is not None: return self.started_at = datetime.utcnow().isoformat() + "Z" self._processor.on_trace_start(self) def finish(self) -> None: if self.ended_at is not None: return self.ended_at = datetime.utcnow().isoformat() + "Z" self._processor.on_trace_end(self) def __enter__(self) -> Trace: self.start() return self def __exit__(self, exc_type, exc, tb) -> None: self.finish() opentelemetry/000077500000000000000000000000001527070072300424225ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/stubsinstrumentation/000077500000000000000000000000001527070072300456655ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/stubs/opentelemetryinstrumentor.py000066400000000000000000000013771527070072300510200ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/stubs/opentelemetry/instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: skip-file from __future__ import annotations from collections.abc import Collection class BaseInstrumentor: def __init__(self) -> None: pass def instrument(self, **kwargs) -> None: self._instrument(**kwargs) def uninstrument(self, **kwargs) -> None: self._uninstrument(**kwargs) # Subclasses override def _instrument(self, **kwargs) -> None: # pragma: no cover - stub raise NotImplementedError def _uninstrument(self, **kwargs) -> None: # pragma: no cover - stub raise NotImplementedError def instrumentation_dependencies( self, ) -> Collection[str]: # pragma: no cover return [] wrapt.py000066400000000000000000000011071527070072300412340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/stubs# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 class ObjectProxy: """Minimal stand-in for wrapt.ObjectProxy used in tests.""" def __init__(self, wrapped): self.__wrapped__ = wrapped def __getattr__(self, item): return getattr(self.__wrapped__, item) def __setattr__(self, key, value): if key == "__wrapped__": super().__setattr__(key, value) else: setattr(self.__wrapped__, key, value) def __call__(self, *args, **kwargs): return self.__wrapped__(*args, **kwargs) test_tracer.py000066400000000000000000000423471527070072300412710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=wrong-import-position,wrong-import-order,import-error,no-name-in-module,unexpected-keyword-arg,no-value-for-parameter,redefined-outer-name from __future__ import annotations import json import sys from pathlib import Path from types import SimpleNamespace from typing import Any TESTS_ROOT = Path(__file__).resolve().parent stub_path = TESTS_ROOT / "stubs" if str(stub_path) not in sys.path: sys.path.insert(0, str(stub_path)) sys.modules.pop("agents", None) sys.modules.pop("agents.tracing", None) import agents.tracing as agents_tracing # noqa: E402 from agents.tracing import ( # noqa: E402 agent_span, function_span, generation_span, response_span, set_trace_processors, trace, ) from openai.types.responses import FunctionTool # noqa: E402 from opentelemetry.instrumentation.openai_agents import ( # noqa: E402 OpenAIAgentsInstrumentor, ) from opentelemetry.instrumentation.openai_agents.span_processor import ( # noqa: E402 ContentPayload, GenAISemanticProcessor, ) from opentelemetry.sdk.trace import TracerProvider # noqa: E402 try: from opentelemetry.sdk.trace.export import ( # type: ignore[attr-defined] InMemorySpanExporter, SimpleSpanProcessor, ) except ImportError: # pragma: no cover - support older/newer SDK layouts from opentelemetry.sdk.trace.export import ( SimpleSpanProcessor, ) from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) from opentelemetry.semconv._incubating.attributes import ( # noqa: E402 gen_ai_attributes as GenAI, ) from opentelemetry.semconv._incubating.attributes import ( # noqa: E402 server_attributes as ServerAttributes, ) from opentelemetry.trace import SpanKind # noqa: E402 GEN_AI_PROVIDER_NAME = GenAI.GEN_AI_PROVIDER_NAME GEN_AI_INPUT_MESSAGES = getattr(GenAI, "GEN_AI_INPUT_MESSAGES", "gen_ai.input.messages") GEN_AI_OUTPUT_MESSAGES = getattr(GenAI, "GEN_AI_OUTPUT_MESSAGES", "gen_ai.output.messages") GEN_AI_TOOL_DEFINITIONS = getattr(GenAI, "GEN_AI_TOOL_DEFINITIONS", "gen_ai.tool.definitions") def _instrument_with_provider(**instrument_kwargs): set_trace_processors([]) provider = TracerProvider() exporter = InMemorySpanExporter() provider.add_span_processor(SimpleSpanProcessor(exporter)) instrumentor = OpenAIAgentsInstrumentor() instrumentor.instrument(tracer_provider=provider, **instrument_kwargs) return instrumentor, exporter def test_generation_span_creates_client_span(): instrumentor, exporter = _instrument_with_provider() try: with trace("workflow"): with generation_span( input=[{"role": "user", "content": "hi"}], model="gpt-4o-mini", model_config={ "temperature": 0.2, "base_url": "https://api.openai.com", }, usage={"input_tokens": 12, "output_tokens": 3}, ): pass spans = exporter.get_finished_spans() client_span = next(span for span in spans if span.kind is SpanKind.CLIENT) assert client_span.attributes[GEN_AI_PROVIDER_NAME] == "openai" assert client_span.attributes[GenAI.GEN_AI_OPERATION_NAME] == "chat" assert client_span.attributes[GenAI.GEN_AI_REQUEST_MODEL] == "gpt-4o-mini" assert client_span.name == "chat gpt-4o-mini" assert client_span.attributes[GenAI.GEN_AI_USAGE_INPUT_TOKENS] == 12 assert client_span.attributes[GenAI.GEN_AI_USAGE_OUTPUT_TOKENS] == 3 assert client_span.attributes[ServerAttributes.SERVER_ADDRESS] == "api.openai.com" finally: instrumentor.uninstrument() exporter.clear() def test_generation_span_without_roles_uses_text_completion(): instrumentor, exporter = _instrument_with_provider() try: with trace("workflow"): with generation_span( input=[{"content": "tell me a joke"}], model="gpt-4o-mini", model_config={"temperature": 0.7}, ): pass spans = exporter.get_finished_spans() completion_span = next( span for span in spans if span.attributes[GenAI.GEN_AI_OPERATION_NAME] == GenAI.GenAiOperationNameValues.TEXT_COMPLETION.value ) assert completion_span.kind is SpanKind.CLIENT assert completion_span.name == "text_completion gpt-4o-mini" assert completion_span.attributes[GenAI.GEN_AI_REQUEST_MODEL] == "gpt-4o-mini" finally: instrumentor.uninstrument() exporter.clear() def test_function_span_records_tool_attributes(): instrumentor, exporter = _instrument_with_provider() try: with trace("workflow"): with function_span(name="fetch_weather", input='{"city": "Paris"}'): pass spans = exporter.get_finished_spans() tool_span = next(span for span in spans if span.kind is SpanKind.INTERNAL) assert tool_span.attributes[GenAI.GEN_AI_OPERATION_NAME] == "execute_tool" assert tool_span.attributes[GenAI.GEN_AI_TOOL_NAME] == "fetch_weather" assert tool_span.attributes[GenAI.GEN_AI_TOOL_TYPE] == "function" assert tool_span.attributes[GEN_AI_PROVIDER_NAME] == "openai" finally: instrumentor.uninstrument() exporter.clear() def test_agent_invoke_span_records_attributes(): instrumentor, exporter = _instrument_with_provider() try: with trace("workflow"): with agent_span( name="support_bot", handoffs=["escalation_bot"], tools=["search"], output_type="str", ): pass spans = exporter.get_finished_spans() invoke_span = next( span for span in spans if span.attributes[GenAI.GEN_AI_OPERATION_NAME] == GenAI.GenAiOperationNameValues.INVOKE_AGENT.value ) assert invoke_span.kind is SpanKind.CLIENT assert invoke_span.name == "invoke_agent support_bot" assert invoke_span.attributes[GEN_AI_PROVIDER_NAME] == "openai" assert invoke_span.attributes[GenAI.GEN_AI_AGENT_NAME] == "support_bot" finally: instrumentor.uninstrument() exporter.clear() def _placeholder_message() -> dict[str, Any]: return { "role": "user", "parts": [{"type": "text", "content": "readacted"}], } def test_normalize_messages_skips_empty_when_sensitive_enabled(): processor = GenAISemanticProcessor(metrics_enabled=False) normalized = processor._normalize_messages_to_role_parts([{"role": "user", "content": None}]) assert not normalized def test_normalize_messages_emits_placeholder_when_sensitive_disabled(): processor = GenAISemanticProcessor(include_sensitive_data=False, metrics_enabled=False) normalized = processor._normalize_messages_to_role_parts([{"role": "user", "content": None}]) assert normalized == [_placeholder_message()] def test_agent_content_aggregation_skips_duplicate_snapshots(): processor = GenAISemanticProcessor(metrics_enabled=False) agent_id = "agent-span" processor._agent_content[agent_id] = { "input_messages": [], "output_messages": [], "system_instructions": [], } payload = ContentPayload( input_messages=[ {"role": "user", "parts": [{"type": "text", "content": "hello"}]}, { "role": "user", "parts": [{"type": "text", "content": "readacted"}], }, ] ) processor._update_agent_aggregate( SimpleNamespace(span_id="child-1", parent_id=agent_id, span_data=None), payload, ) processor._update_agent_aggregate( SimpleNamespace(span_id="child-2", parent_id=agent_id, span_data=None), payload, ) aggregated = processor._agent_content[agent_id]["input_messages"] assert aggregated == [{"role": "user", "parts": [{"type": "text", "content": "hello"}]}] # ensure data copied rather than reused to prevent accidental mutation assert aggregated is not payload.input_messages def test_agent_content_aggregation_filters_placeholder_append_when_sensitive(): processor = GenAISemanticProcessor(metrics_enabled=False) agent_id = "agent-span" processor._agent_content[agent_id] = { "input_messages": [], "output_messages": [], "system_instructions": [], } initial_payload = ContentPayload(input_messages=[{"role": "user", "parts": [{"type": "text", "content": "hello"}]}]) processor._update_agent_aggregate( SimpleNamespace(span_id="child-1", parent_id=agent_id, span_data=None), initial_payload, ) placeholder_payload = ContentPayload(input_messages=[_placeholder_message()]) processor._update_agent_aggregate( SimpleNamespace(span_id="child-2", parent_id=agent_id, span_data=None), placeholder_payload, ) aggregated = processor._agent_content[agent_id]["input_messages"] assert aggregated == [{"role": "user", "parts": [{"type": "text", "content": "hello"}]}] def test_agent_content_aggregation_retains_placeholder_when_sensitive_disabled(): processor = GenAISemanticProcessor(include_sensitive_data=False, metrics_enabled=False) agent_id = "agent-span" processor._agent_content[agent_id] = { "input_messages": [], "output_messages": [], "system_instructions": [], } placeholder_payload = ContentPayload(input_messages=[_placeholder_message()]) processor._update_agent_aggregate( SimpleNamespace(span_id="child-1", parent_id=agent_id, span_data=None), placeholder_payload, ) aggregated = processor._agent_content[agent_id]["input_messages"] assert aggregated == [_placeholder_message()] def test_agent_content_aggregation_appends_new_messages_once(): processor = GenAISemanticProcessor(metrics_enabled=False) agent_id = "agent-span" processor._agent_content[agent_id] = { "input_messages": [], "output_messages": [], "system_instructions": [], } initial_payload = ContentPayload(input_messages=[{"role": "user", "parts": [{"type": "text", "content": "hello"}]}]) processor._update_agent_aggregate( SimpleNamespace(span_id="child-1", parent_id=agent_id, span_data=None), initial_payload, ) extended_messages = [ {"role": "user", "parts": [{"type": "text", "content": "hello"}]}, { "role": "assistant", "parts": [{"type": "text", "content": "hi there"}], }, ] extended_payload = ContentPayload(input_messages=extended_messages) processor._update_agent_aggregate( SimpleNamespace(span_id="child-2", parent_id=agent_id, span_data=None), extended_payload, ) aggregated = processor._agent_content[agent_id]["input_messages"] assert aggregated == extended_messages assert extended_payload.input_messages == extended_messages def test_agent_span_collects_child_messages(): instrumentor, exporter = _instrument_with_provider() try: provider = agents_tracing.get_trace_provider() with trace("workflow") as workflow: agent_span_obj = provider.create_span( agents_tracing.AgentSpanData(name="helper"), parent=workflow, ) agent_span_obj.start() generation = agents_tracing.GenerationSpanData( input=[{"role": "user", "content": "hi"}], output=[{"type": "text", "content": "hello"}], model="gpt-4o-mini", ) gen_span = provider.create_span(generation, parent=agent_span_obj) gen_span.start() gen_span.finish() agent_span_obj.finish() spans = exporter.get_finished_spans() agent_span = next( span for span in spans if span.attributes.get(GenAI.GEN_AI_OPERATION_NAME) == GenAI.GenAiOperationNameValues.INVOKE_AGENT.value ) prompt = json.loads(agent_span.attributes[GEN_AI_INPUT_MESSAGES]) completion = json.loads(agent_span.attributes[GEN_AI_OUTPUT_MESSAGES]) assert prompt == [ { "role": "user", "parts": [{"type": "text", "content": "hi"}], } ] assert completion == [ { "role": "assistant", "parts": [{"type": "text", "content": "hello"}], } ] assert not agent_span.events finally: instrumentor.uninstrument() exporter.clear() def test_agent_name_override_applied_to_agent_spans(): instrumentor, exporter = _instrument_with_provider(agent_name="Travel Concierge") try: with trace("workflow"): with agent_span(name="support_bot"): pass spans = exporter.get_finished_spans() agent_span_record = next( span for span in spans if span.attributes[GenAI.GEN_AI_OPERATION_NAME] == GenAI.GenAiOperationNameValues.INVOKE_AGENT.value ) assert agent_span_record.kind is SpanKind.CLIENT assert agent_span_record.name == "invoke_agent Travel Concierge" assert agent_span_record.attributes[GenAI.GEN_AI_AGENT_NAME] == "Travel Concierge" finally: instrumentor.uninstrument() exporter.clear() def test_capture_mode_can_be_disabled(): instrumentor, exporter = _instrument_with_provider(capture_message_content="no_content") try: with trace("workflow"): with generation_span( input=[{"role": "user", "content": "hi"}], output=[{"role": "assistant", "content": "hello"}], model="gpt-4o-mini", ): pass spans = exporter.get_finished_spans() client_span = next(span for span in spans if span.kind is SpanKind.CLIENT) assert GEN_AI_INPUT_MESSAGES not in client_span.attributes assert GEN_AI_OUTPUT_MESSAGES not in client_span.attributes for event in client_span.events: assert GEN_AI_INPUT_MESSAGES not in event.attributes assert GEN_AI_OUTPUT_MESSAGES not in event.attributes finally: instrumentor.uninstrument() exporter.clear() def test_response_span_records_response_attributes(): instrumentor, exporter = _instrument_with_provider() class _Usage: def __init__(self, input_tokens: int, output_tokens: int) -> None: self.input_tokens = input_tokens self.output_tokens = output_tokens class _Response: def __init__(self) -> None: self.id = "resp-123" self.instructions = "You are a helpful assistant." self.model = "gpt-4o-mini" self.usage = _Usage(42, 9) self.tools = [ FunctionTool( name="get_current_weather", type="function", description="Get the current weather in a given location", parameters={ "type": "object", "properties": { "location": { "title": "Location", "type": "string", }, }, "required": ["location"], }, ) ] self.output = [{"finish_reason": "stop"}] try: with trace("workflow"): with response_span(response=_Response()): pass spans = exporter.get_finished_spans() response = next( span for span in spans if span.attributes[GenAI.GEN_AI_OPERATION_NAME] == GenAI.GenAiOperationNameValues.CHAT.value ) assert response.kind is SpanKind.CLIENT assert response.name == "chat gpt-4o-mini" assert response.attributes[GEN_AI_PROVIDER_NAME] == "openai" assert response.attributes[GenAI.GEN_AI_RESPONSE_ID] == "resp-123" assert response.attributes[GenAI.GEN_AI_RESPONSE_MODEL] == "gpt-4o-mini" assert response.attributes[GenAI.GEN_AI_USAGE_INPUT_TOKENS] == 42 assert response.attributes[GenAI.GEN_AI_USAGE_OUTPUT_TOKENS] == 9 assert response.attributes[GenAI.GEN_AI_RESPONSE_FINISH_REASONS] == ("stop",) system_instructions = json.loads(response.attributes[GenAI.GEN_AI_SYSTEM_INSTRUCTIONS]) assert system_instructions == [{"type": "text", "content": "You are a helpful assistant."}] tool_definitions = json.loads(response.attributes[GEN_AI_TOOL_DEFINITIONS]) assert tool_definitions == [ { "type": "function", "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": {"title": "Location", "type": "string"}, }, "required": ["location"], }, } ] finally: instrumentor.uninstrument() exporter.clear() test_z_instrumentor_behaviors.py000066400000000000000000000042061527070072300451450ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=wrong-import-position,wrong-import-order,import-error from __future__ import annotations import sys from pathlib import Path TESTS_ROOT = Path(__file__).resolve().parent stub_path = TESTS_ROOT / "stubs" if str(stub_path) not in sys.path: sys.path.insert(0, str(stub_path)) from agents.tracing import ( # noqa: E402 get_trace_provider, set_trace_processors, ) from opentelemetry.instrumentation.openai_agents import ( # noqa: E402 OpenAIAgentsInstrumentor, ) from opentelemetry.instrumentation.openai_agents.package import ( # noqa: E402 _instruments, ) from opentelemetry.sdk.trace import TracerProvider # noqa: E402 def test_double_instrument_is_noop(): set_trace_processors([]) provider = TracerProvider() instrumentor = OpenAIAgentsInstrumentor() instrumentor.instrument(tracer_provider=provider) trace_provider = get_trace_provider() assert len(trace_provider._multi_processor._processors) == 1 instrumentor.instrument(tracer_provider=provider) assert len(trace_provider._multi_processor._processors) == 1 instrumentor.uninstrument() instrumentor.uninstrument() set_trace_processors([]) def test_instrumentation_dependencies_exposed(): instrumentor = OpenAIAgentsInstrumentor() assert instrumentor.instrumentation_dependencies() == _instruments def test_default_agent_configuration(): set_trace_processors([]) provider = TracerProvider() instrumentor = OpenAIAgentsInstrumentor() try: instrumentor.instrument(tracer_provider=provider) processor = instrumentor._processor assert processor is not None assert processor._agent_name_default == "OpenAI Agent" assert processor._agent_id_default == "agent" assert processor._agent_description_default == "OpenAI Agents instrumentation" assert processor.base_url == "https://api.openai.com" assert processor.server_address == "api.openai.com" assert processor.server_port == 443 finally: instrumentor.uninstrument() set_trace_processors([]) test_z_span_processor_unit.py000066400000000000000000000401531527070072300444320ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=wrong-import-position,wrong-import-order,import-error,no-name-in-module,unexpected-keyword-arg,no-value-for-parameter,redefined-outer-name,too-many-locals,too-many-statements,too-many-branches from __future__ import annotations import importlib import json from dataclasses import dataclass from datetime import datetime, timezone from enum import Enum from types import SimpleNamespace from typing import Any import pytest from agents.tracing import ( AgentSpanData, FunctionSpanData, GenerationSpanData, ResponseSpanData, ) import opentelemetry.semconv._incubating.attributes.gen_ai_attributes as _gen_ai_attributes from opentelemetry.sdk.trace import TracerProvider from opentelemetry.semconv._incubating.attributes import ( server_attributes as _server_attributes, ) from opentelemetry.trace import SpanKind from opentelemetry.trace.status import StatusCode def _ensure_semconv_enums() -> None: if not hasattr(_gen_ai_attributes, "GenAiProviderNameValues"): class _GenAiProviderNameValues(Enum): OPENAI = "openai" GCP_GEN_AI = "gcp.gen_ai" GCP_VERTEX_AI = "gcp.vertex_ai" GCP_GEMINI = "gcp.gemini" ANTHROPIC = "anthropic" COHERE = "cohere" AZURE_AI_INFERENCE = "azure.ai.inference" AZURE_AI_OPENAI = "azure.ai.openai" IBM_WATSONX_AI = "ibm.watsonx.ai" AWS_BEDROCK = "aws.bedrock" PERPLEXITY = "perplexity" X_AI = "x.ai" DEEPSEEK = "deepseek" GROQ = "groq" MISTRAL_AI = "mistral.ai" class _GenAiOperationNameValues(Enum): CHAT = "chat" GENERATE_CONTENT = "generate_content" TEXT_COMPLETION = "text_completion" EMBEDDINGS = "embeddings" CREATE_AGENT = "create_agent" INVOKE_AGENT = "invoke_agent" EXECUTE_TOOL = "execute_tool" class _GenAiOutputTypeValues(Enum): TEXT = "text" JSON = "json" IMAGE = "image" SPEECH = "speech" _gen_ai_attributes.GenAiProviderNameValues = _GenAiProviderNameValues _gen_ai_attributes.GenAiOperationNameValues = _GenAiOperationNameValues _gen_ai_attributes.GenAiOutputTypeValues = _GenAiOutputTypeValues if not hasattr(_server_attributes, "SERVER_ADDRESS"): _server_attributes.SERVER_ADDRESS = "server.address" if not hasattr(_server_attributes, "SERVER_PORT"): _server_attributes.SERVER_PORT = "server.port" _ensure_semconv_enums() ServerAttributes = _server_attributes sp = importlib.import_module("opentelemetry.instrumentation.openai_agents.span_processor") try: from opentelemetry.sdk.trace.export import ( # type: ignore[attr-defined] InMemorySpanExporter, SimpleSpanProcessor, ) except ImportError: # pragma: no cover from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) def _collect(iterator) -> dict[str, Any]: return dict(iterator) @pytest.fixture def processor_setup(): provider = TracerProvider() exporter = InMemorySpanExporter() provider.add_span_processor(SimpleSpanProcessor(exporter)) tracer = provider.get_tracer(__name__) processor = sp.GenAISemanticProcessor(tracer=tracer, system_name="openai") yield processor, exporter processor.shutdown() exporter.clear() def test_time_helpers(): dt = datetime(2024, 1, 1, tzinfo=timezone.utc) assert sp._as_utc_nano(dt) == 1704067200 * 1_000_000_000 class Fallback: def __str__(self) -> str: return "fallback" assert sp.safe_json_dumps({"foo": "bar"}) == '{"foo":"bar"}' assert sp.safe_json_dumps(Fallback()) == "fallback" def test_infer_server_attributes_variants(monkeypatch): assert sp._infer_server_attributes(None) == {} assert sp._infer_server_attributes(123) == {} attrs = sp._infer_server_attributes("https://api.example.com:8080/v1") assert attrs[ServerAttributes.SERVER_ADDRESS] == "api.example.com" assert attrs[ServerAttributes.SERVER_PORT] == 8080 def boom(_: str): raise ValueError("unparsable url") monkeypatch.setattr(sp, "urlparse", boom) assert sp._infer_server_attributes("bad") == {} def test_operation_and_span_naming(processor_setup): processor, _ = processor_setup generation = GenerationSpanData(input=[{"role": "user"}], model="gpt-4o") assert processor._get_operation_name(generation) == sp.GenAIOperationName.CHAT completion = GenerationSpanData(input=[]) assert processor._get_operation_name(completion) == sp.GenAIOperationName.TEXT_COMPLETION embeddings = GenerationSpanData(input=None) embeddings.embedding_dimension = 128 assert processor._get_operation_name(embeddings) == sp.GenAIOperationName.EMBEDDINGS # AgentSpanData always maps to invoke_agent (no operation field in OpenAI Agents SDK) agent_data = AgentSpanData(name="bot") assert processor._get_operation_name(agent_data) == sp.GenAIOperationName.INVOKE_AGENT agent_default = AgentSpanData() assert processor._get_operation_name(agent_default) == sp.GenAIOperationName.INVOKE_AGENT function_data = FunctionSpanData() assert processor._get_operation_name(function_data) == sp.GenAIOperationName.EXECUTE_TOOL response_data = ResponseSpanData() assert processor._get_operation_name(response_data) == sp.GenAIOperationName.CHAT class UnknownSpanData: pass unknown = UnknownSpanData() assert processor._get_operation_name(unknown) == "unknown" assert processor._get_span_kind(GenerationSpanData()) is SpanKind.CLIENT assert processor._get_span_kind(FunctionSpanData()) is SpanKind.INTERNAL assert sp.get_span_name(sp.GenAIOperationName.CHAT, model="gpt-4o") == "chat gpt-4o" assert sp.get_span_name(sp.GenAIOperationName.EXECUTE_TOOL, tool_name="weather") == "execute_tool weather" assert sp.get_span_name(sp.GenAIOperationName.INVOKE_AGENT, agent_name=None) == "invoke_agent" assert sp.get_span_name(sp.GenAIOperationName.CREATE_AGENT, agent_name=None) == "create_agent" def test_attribute_builders(processor_setup): processor, _ = processor_setup payload = sp.ContentPayload( input_messages=[ { "role": "user", "parts": [{"type": "text", "content": "hi"}], } ], output_messages=[ { "role": "assistant", "parts": [{"type": "text", "content": "hello"}], } ], system_instructions=[{"type": "text", "content": "be helpful"}], ) model_config = { "base_url": "https://api.openai.com:443/v1", "temperature": 0.2, "top_p": 0.9, "top_k": 3, "frequency_penalty": 0.1, "presence_penalty": 0.4, "seed": 1234, "n": 2, "max_tokens": 128, "stop": ["foo", None, "bar"], } generation_span = GenerationSpanData( input=[{"role": "user"}], output=[{"finish_reason": "stop"}], model="gpt-4o", model_config=model_config, usage={ "prompt_tokens": 10, "completion_tokens": 3, "total_tokens": 13, }, ) gen_attrs = _collect(processor._get_attributes_from_generation_span_data(generation_span, payload)) assert gen_attrs[sp.GEN_AI_REQUEST_MODEL] == "gpt-4o" assert gen_attrs[sp.GEN_AI_REQUEST_MAX_TOKENS] == 128 assert gen_attrs[sp.GEN_AI_REQUEST_STOP_SEQUENCES] == [ "foo", None, "bar", ] assert gen_attrs[ServerAttributes.SERVER_ADDRESS] == "api.openai.com" assert gen_attrs[ServerAttributes.SERVER_PORT] == 443 assert gen_attrs[sp.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert gen_attrs[sp.GEN_AI_USAGE_OUTPUT_TOKENS] == 3 assert gen_attrs[sp.GEN_AI_RESPONSE_FINISH_REASONS] == ["stop"] assert json.loads(gen_attrs[sp.GEN_AI_INPUT_MESSAGES])[0]["role"] == "user" assert json.loads(gen_attrs[sp.GEN_AI_OUTPUT_MESSAGES])[0]["role"] == "assistant" assert json.loads(gen_attrs[sp.GEN_AI_SYSTEM_INSTRUCTIONS])[0]["content"] == "be helpful" assert gen_attrs[sp.GEN_AI_OUTPUT_TYPE] == sp.GenAIOutputType.TEXT class _Usage: def __init__(self) -> None: self.input_tokens = None self.prompt_tokens = 7 self.output_tokens = None self.completion_tokens = 2 self.total_tokens = 9 class _Response: def __init__(self) -> None: self.id = "resp-1" self.model = "gpt-4o" self.usage = _Usage() self.output = [{"finish_reason": "stop"}] response_span = ResponseSpanData(response=_Response()) response_attrs = _collect(processor._get_attributes_from_response_span_data(response_span, sp.ContentPayload())) assert response_attrs[sp.GEN_AI_RESPONSE_ID] == "resp-1" assert response_attrs[sp.GEN_AI_RESPONSE_MODEL] == "gpt-4o" assert response_attrs[sp.GEN_AI_RESPONSE_FINISH_REASONS] == ["stop"] assert response_attrs[sp.GEN_AI_USAGE_INPUT_TOKENS] == 7 assert response_attrs[sp.GEN_AI_USAGE_OUTPUT_TOKENS] == 2 assert response_attrs[sp.GEN_AI_OUTPUT_TYPE] == sp.GenAIOutputType.TEXT agent_span = AgentSpanData( name="helper", output_type="json", ) agent_attrs = _collect(processor._get_attributes_from_agent_span_data(agent_span, None)) assert agent_attrs[sp.GEN_AI_AGENT_NAME] == "helper" assert sp.GEN_AI_AGENT_ID not in agent_attrs assert sp.GEN_AI_REQUEST_MODEL not in agent_attrs assert agent_attrs[sp.GEN_AI_OUTPUT_TYPE] == sp.GenAIOutputType.TEXT # Fallback to aggregated model when span data lacks it agent_span_no_model = AgentSpanData( name="helper-2", output_type="json", ) agent_content = { "input_messages": [], "output_messages": [], "system_instructions": [], "request_model": "gpt-fallback", } agent_attrs_fallback = _collect(processor._get_attributes_from_agent_span_data(agent_span_no_model, agent_content)) assert agent_attrs_fallback[sp.GEN_AI_REQUEST_MODEL] == "gpt-fallback" function_span = FunctionSpanData(name="lookup_weather") function_span.tool_type = "extension" function_span.call_id = "call-42" function_span.description = "desc" function_payload = sp.ContentPayload( tool_arguments={"city": "seattle"}, tool_result={"temperature": 70}, ) function_attrs = _collect(processor._get_attributes_from_function_span_data(function_span, function_payload)) assert function_attrs[sp.GEN_AI_TOOL_NAME] == "lookup_weather" assert function_attrs[sp.GEN_AI_TOOL_TYPE] == "extension" assert function_attrs[sp.GEN_AI_TOOL_CALL_ID] == "call-42" assert function_attrs[sp.GEN_AI_TOOL_DESCRIPTION] == "desc" assert function_attrs[sp.GEN_AI_TOOL_CALL_ARGUMENTS] == {"city": "seattle"} assert function_attrs[sp.GEN_AI_TOOL_CALL_RESULT] == {"temperature": 70} assert function_attrs[sp.GEN_AI_OUTPUT_TYPE] == sp.GenAIOutputType.JSON def test_extract_genai_attributes_unknown_type(processor_setup): processor, _ = processor_setup class UnknownSpanData: pass class StubSpan: def __init__(self) -> None: self.span_data = UnknownSpanData() attrs = _collect(processor._extract_genai_attributes(StubSpan(), sp.ContentPayload(), None)) assert attrs[sp.GEN_AI_PROVIDER_NAME] == "openai" assert attrs[sp.GEN_AI_SYSTEM_KEY] == "openai" assert sp.GEN_AI_OPERATION_NAME not in attrs def test_span_status_helper(): status = sp._get_span_status(SimpleNamespace(error={"message": "boom", "data": "bad"})) assert status.status_code is StatusCode.ERROR assert status.description == "boom: bad" ok_status = sp._get_span_status(SimpleNamespace(error=None)) assert ok_status.status_code is StatusCode.OK @dataclass class FakeTrace: name: str trace_id: str started_at: str | None = None ended_at: str | None = None @dataclass class FakeSpan: trace_id: str span_id: str span_data: Any parent_id: str | None = None started_at: str | None = None ended_at: str | None = None error: dict[str, Any] | None = None def test_span_lifecycle_and_shutdown(processor_setup): processor, exporter = processor_setup trace = FakeTrace( name="workflow", trace_id="trace-1", started_at="not-a-timestamp", ended_at="2024-01-01T00:00:05Z", ) processor.on_trace_start(trace) parent_span = FakeSpan( trace_id="trace-1", span_id="span-1", span_data=AgentSpanData(name="agent"), started_at="2024-01-01T00:00:00Z", ended_at="2024-01-01T00:00:02Z", ) processor.on_span_start(parent_span) missing_span = FakeSpan( trace_id="trace-1", span_id="missing", span_data=FunctionSpanData(name="lookup"), started_at="2024-01-01T00:00:01Z", ended_at="2024-01-01T00:00:02Z", ) processor.on_span_end(missing_span) child_span = FakeSpan( trace_id="trace-1", span_id="span-2", parent_id="span-1", span_data=FunctionSpanData(name="lookup"), started_at="2024-01-01T00:00:02Z", ended_at="2024-01-01T00:00:03Z", error={"message": "boom", "data": "bad"}, ) processor.on_span_start(child_span) processor.on_span_end(child_span) processor.on_span_end(parent_span) processor.on_trace_end(trace) linger_trace = FakeTrace( name="linger", trace_id="trace-2", started_at="2024-01-01T00:00:06Z", ) processor.on_trace_start(linger_trace) linger_span = FakeSpan( trace_id="trace-2", span_id="span-3", span_data=AgentSpanData(), started_at="2024-01-01T00:00:06Z", ) processor.on_span_start(linger_span) assert processor.force_flush() is None processor.shutdown() finished = exporter.get_finished_spans() statuses = {span.name: span.status for span in finished} assert ( statuses["execute_tool lookup"].status_code is StatusCode.ERROR and statuses["execute_tool lookup"].description == "boom: bad" ) assert statuses["invoke_agent agent"].status_code is StatusCode.OK assert statuses["workflow"].status_code is StatusCode.OK assert ( statuses["invoke_agent"].status_code is StatusCode.ERROR and statuses["invoke_agent"].description == "Application shutdown" ) assert ( statuses["linger"].status_code is StatusCode.ERROR and statuses["linger"].description == "Application shutdown" ) workflow_span = next(span for span in finished if span.name == "workflow") assert workflow_span.attributes[sp.GEN_AI_OPERATION_NAME] == sp.GenAIOperationName.INVOKE_AGENT def test_chat_span_renamed_with_model(processor_setup): processor, exporter = processor_setup trace = FakeTrace(name="workflow", trace_id="trace-rename") processor.on_trace_start(trace) agent = FakeSpan( trace_id=trace.trace_id, span_id="agent-span", span_data=AgentSpanData( name="Agent", ), started_at="2025-01-01T00:00:00Z", ended_at="2025-01-01T00:00:02Z", ) processor.on_span_start(agent) generation_data = GenerationSpanData( input=[{"role": "user", "content": "question"}], output=[{"finish_reason": "stop"}], usage={"prompt_tokens": 1, "completion_tokens": 1}, ) generation_span = FakeSpan( trace_id=trace.trace_id, span_id="child-span", parent_id=agent.span_id, span_data=generation_data, started_at="2025-01-01T00:00:00Z", ended_at="2025-01-01T00:00:01Z", ) processor.on_span_start(generation_span) # Model becomes available before span end (e.g., once response arrives) generation_data.model = "gpt-4o" processor.on_span_end(generation_span) processor.on_span_end(agent) processor.on_trace_end(trace) span_names = {span.name for span in exporter.get_finished_spans()} assert "chat gpt-4o" in span_names test_zz_coverage_improvements.py000066400000000000000000000771441527070072300451420ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=wrong-import-position,wrong-import-order,import-error,no-name-in-module,unexpected-keyword-arg,no-value-for-parameter,redefined-outer-name,too-many-locals,too-many-statements,protected-access,import-outside-toplevel,no-self-use,invalid-name # ruff: noqa: PLC0415 """ Tests for improving coverage of OpenAI Agents instrumentation. This file targets uncovered lines identified in the coverage report: - __init__.py: _resolve_system, _resolve_bool, _resolve_content_mode - span_processor.py: normalization utilities, span data handlers, message normalization, output type inference, metrics recording, and various edge cases. NOTE: Imports are done inside functions to avoid module-level import conflicts that can occur when pytest collects tests across multiple files. """ from __future__ import annotations def _get_modules(): """Lazy import to avoid module conflicts.""" import importlib sp = importlib.import_module("opentelemetry.instrumentation.openai_agents.span_processor") init_module = importlib.import_module("opentelemetry.instrumentation.openai_agents") return sp, init_module def _get_span_data_classes(): """Lazy import span data classes.""" from agents.tracing import ( AgentSpanData, FunctionSpanData, GenerationSpanData, ResponseSpanData, ) return ( AgentSpanData, FunctionSpanData, GenerationSpanData, ResponseSpanData, ) def _get_otel_test_fixtures(): """Get OpenTelemetry test fixtures.""" from opentelemetry.sdk.trace import TracerProvider try: from opentelemetry.sdk.trace.export import ( InMemorySpanExporter, SimpleSpanProcessor, ) except ImportError: from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) return TracerProvider, InMemorySpanExporter, SimpleSpanProcessor # ============================================================================ # Tests for __init__.py functions # ============================================================================ class TestResolveSystem: """Tests for _resolve_system function.""" def test_resolve_system_returns_openai_for_none(self): _, init_module = _get_modules() result = init_module._resolve_system(None) assert result == "openai" def test_resolve_system_returns_openai_for_empty_string(self): _, init_module = _get_modules() result = init_module._resolve_system("") assert result == "openai" def test_resolve_system_matches_enum_value(self): _, init_module = _get_modules() result = init_module._resolve_system("openai") assert result == "openai" def test_resolve_system_matches_enum_name(self): _, init_module = _get_modules() result = init_module._resolve_system("OPENAI") assert result == "openai" def test_resolve_system_matches_enum_name_when_name_differs(self): _, init_module = _get_modules() from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAI, ) candidate = next( (member for member in GenAI.GenAiSystemValues if member.name.lower() != member.value), None, ) assert candidate is not None assert init_module._resolve_system(candidate.name.lower()) == candidate.value def test_resolve_system_matches_with_whitespace(self): _, init_module = _get_modules() result = init_module._resolve_system(" openai ") assert result == "openai" def test_resolve_system_returns_custom_value_when_not_found(self): _, init_module = _get_modules() result = init_module._resolve_system("custom_provider") assert result == "custom_provider" class TestResolveBool: """Tests for _resolve_bool function.""" def test_resolve_bool_returns_default_for_none(self): _, init_module = _get_modules() assert init_module._resolve_bool(None, True) is True assert init_module._resolve_bool(None, False) is False def test_resolve_bool_returns_value_for_bool(self): _, init_module = _get_modules() assert init_module._resolve_bool(True, False) is True assert init_module._resolve_bool(False, True) is False def test_resolve_bool_parses_true_strings(self): _, init_module = _get_modules() for value in ["true", "TRUE", "1", "yes", "on", " YES "]: assert init_module._resolve_bool(value, False) is True, f"Failed for {value}" def test_resolve_bool_parses_false_strings(self): _, init_module = _get_modules() for value in ["false", "FALSE", "0", "no", "off", " NO "]: assert init_module._resolve_bool(value, True) is False, f"Failed for {value}" def test_resolve_bool_returns_default_for_unknown(self): _, init_module = _get_modules() assert init_module._resolve_bool("maybe", True) is True assert init_module._resolve_bool("maybe", False) is False class TestResolveContentMode: """Tests for _resolve_content_mode function.""" def test_resolve_content_mode_returns_value_for_enum(self): sp, init_module = _get_modules() result = init_module._resolve_content_mode(sp.ContentCaptureMode.SPAN_ONLY) assert result == sp.ContentCaptureMode.SPAN_ONLY def test_resolve_content_mode_for_bool_true(self): sp, init_module = _get_modules() result = init_module._resolve_content_mode(True) assert result == sp.ContentCaptureMode.SPAN_AND_EVENT def test_resolve_content_mode_for_bool_false(self): sp, init_module = _get_modules() result = init_module._resolve_content_mode(False) assert result == sp.ContentCaptureMode.NO_CONTENT def test_resolve_content_mode_for_none(self): sp, init_module = _get_modules() result = init_module._resolve_content_mode(None) assert result == sp.ContentCaptureMode.SPAN_AND_EVENT def test_resolve_content_mode_for_empty_string(self): sp, init_module = _get_modules() result = init_module._resolve_content_mode("") assert result == sp.ContentCaptureMode.SPAN_AND_EVENT def test_resolve_content_mode_span_only_variants(self): sp, init_module = _get_modules() for value in ["span_only", "span-only", "span"]: result = init_module._resolve_content_mode(value) assert result == sp.ContentCaptureMode.SPAN_ONLY, f"Failed for {value}" def test_resolve_content_mode_event_only_variants(self): sp, init_module = _get_modules() for value in ["event_only", "event-only", "event"]: result = init_module._resolve_content_mode(value) assert result == sp.ContentCaptureMode.EVENT_ONLY, f"Failed for {value}" def test_resolve_content_mode_span_and_event_variants(self): sp, init_module = _get_modules() for value in [ "span_and_event", "span-and-event", "span_and_events", "all", "true", "1", "yes", ]: result = init_module._resolve_content_mode(value) assert result == sp.ContentCaptureMode.SPAN_AND_EVENT, f"Failed for {value}" def test_resolve_content_mode_no_content_variants(self): sp, init_module = _get_modules() for value in ["no_content", "false", "0", "no", "none"]: result = init_module._resolve_content_mode(value) assert result == sp.ContentCaptureMode.NO_CONTENT, f"Failed for {value}" # ============================================================================ # Tests for span_processor.py normalization utilities # ============================================================================ class TestNormalizeProvider: """Tests for normalize_provider function.""" def test_normalize_provider_returns_none_for_none(self): sp, _ = _get_modules() assert sp.normalize_provider(None) is None def test_normalize_provider_returns_none_for_empty(self): sp, _ = _get_modules() assert sp.normalize_provider("") is None def test_normalize_provider_normalizes_valid_provider(self): sp, _ = _get_modules() result = sp.normalize_provider("OPENAI") assert result == "openai" def test_normalize_provider_passes_through_unknown(self): sp, _ = _get_modules() result = sp.normalize_provider("MyCustomProvider") assert result == "MyCustomProvider" class TestValidateToolType: """Tests for validate_tool_type function.""" def test_validate_tool_type_returns_default_for_none(self): sp, _ = _get_modules() assert sp.validate_tool_type(None) == "function" def test_validate_tool_type_returns_default_for_empty(self): sp, _ = _get_modules() assert sp.validate_tool_type("") == "function" def test_validate_tool_type_normalizes_valid_types(self): sp, _ = _get_modules() assert sp.validate_tool_type("FUNCTION") == "function" assert sp.validate_tool_type("extension") == "extension" assert sp.validate_tool_type("DATASTORE") == "datastore" def test_validate_tool_type_returns_default_for_unknown(self): sp, _ = _get_modules() assert sp.validate_tool_type("unknown_type") == "function" class TestNormalizeOutputType: """Tests for normalize_output_type function.""" def test_normalize_output_type_returns_text_for_none(self): sp, _ = _get_modules() assert sp.normalize_output_type(None) == "text" def test_normalize_output_type_returns_text_for_empty(self): sp, _ = _get_modules() assert sp.normalize_output_type("") == "text" def test_normalize_output_type_normalizes_known_types(self): sp, _ = _get_modules() assert sp.normalize_output_type("TEXT") == "text" assert sp.normalize_output_type("json") == "json" assert sp.normalize_output_type("image") == "image" assert sp.normalize_output_type("speech") == "speech" def test_normalize_output_type_handles_mappings(self): sp, _ = _get_modules() assert sp.normalize_output_type("json_object") == "json" assert sp.normalize_output_type("jsonschema") == "json" assert sp.normalize_output_type("speech_audio") == "speech" assert sp.normalize_output_type("audio_speech") == "speech" assert sp.normalize_output_type("image_png") == "image" assert sp.normalize_output_type("function_arguments_json") == "json" assert sp.normalize_output_type("tool_call") == "json" assert sp.normalize_output_type("transcription_json") == "json" def test_normalize_output_type_returns_text_for_unknown(self): sp, _ = _get_modules() assert sp.normalize_output_type("unknown_type") == "text" # ============================================================================ # Tests for span naming and output type inference # ============================================================================ class TestGetSpanName: """Tests for get_span_name function.""" def test_span_name_handoff(self): sp, _ = _get_modules() name = sp.get_span_name("agent_handoff", agent_name="target_agent") assert name == "agent_handoff target_agent" def test_span_name_handoff_no_agent(self): sp, _ = _get_modules() name = sp.get_span_name("agent_handoff") assert name == "agent_handoff" def test_span_name_transcription(self): sp, _ = _get_modules() name = sp.get_span_name("transcription", model="whisper-1") assert name == "transcription whisper-1" def test_span_name_speech(self): sp, _ = _get_modules() name = sp.get_span_name("speech_generation", model="tts-1") assert name == "speech_generation tts-1" def test_span_name_unknown_operation(self): sp, _ = _get_modules() name = sp.get_span_name("unknown_operation") assert name == "unknown_operation" class TestInferOutputType: """Tests for _infer_output_type method.""" def _make_processor(self): sp, _ = _get_modules() TracerProvider, _, _ = _get_otel_test_fixtures() provider = TracerProvider() tracer = provider.get_tracer(__name__) return sp.GenAISemanticProcessor(tracer=tracer, system_name="openai", metrics_enabled=False) def test_infer_output_type_function_span(self): _, FunctionSpanData, _, _ = _get_span_data_classes() processor = self._make_processor() span_data = FunctionSpanData() assert processor._infer_output_type(span_data) == "json" def test_infer_output_type_embeddings(self): _, _, GenerationSpanData, _ = _get_span_data_classes() processor = self._make_processor() span_data = GenerationSpanData() span_data.embedding_dimension = 128 assert processor._infer_output_type(span_data) == "text" def test_infer_output_type_image_output(self): _, _, GenerationSpanData, _ = _get_span_data_classes() processor = self._make_processor() span_data = GenerationSpanData(output=[{"type": "image"}]) assert processor._infer_output_type(span_data) == "image" def test_infer_output_type_audio_output(self): _, _, GenerationSpanData, _ = _get_span_data_classes() processor = self._make_processor() span_data = GenerationSpanData(output=[{"type": "audio"}]) assert processor._infer_output_type(span_data) == "speech" def test_infer_output_type_json_output(self): _, _, GenerationSpanData, _ = _get_span_data_classes() processor = self._make_processor() span_data = GenerationSpanData(output=[{"type": "json_object"}]) assert processor._infer_output_type(span_data) == "json" def test_infer_output_type_text_output(self): _, _, GenerationSpanData, _ = _get_span_data_classes() processor = self._make_processor() span_data = GenerationSpanData(output=[{"type": "text"}]) assert processor._infer_output_type(span_data) == "text" def test_infer_output_type_json_like_keys(self): _, _, GenerationSpanData, _ = _get_span_data_classes() processor = self._make_processor() span_data = GenerationSpanData(output=[{"schema": {}, "properties": {}}]) assert processor._infer_output_type(span_data) == "json" # ============================================================================ # Tests for content capture mode properties # ============================================================================ class TestContentCaptureMode: """Tests for ContentCaptureMode properties.""" def test_capture_in_span(self): sp, _ = _get_modules() assert sp.ContentCaptureMode.NO_CONTENT.capture_in_span is False assert sp.ContentCaptureMode.SPAN_ONLY.capture_in_span is True assert sp.ContentCaptureMode.EVENT_ONLY.capture_in_span is False assert sp.ContentCaptureMode.SPAN_AND_EVENT.capture_in_span is True def test_capture_in_event(self): sp, _ = _get_modules() assert sp.ContentCaptureMode.NO_CONTENT.capture_in_event is False assert sp.ContentCaptureMode.SPAN_ONLY.capture_in_event is False assert sp.ContentCaptureMode.EVENT_ONLY.capture_in_event is True assert sp.ContentCaptureMode.SPAN_AND_EVENT.capture_in_event is True # ============================================================================ # Tests for sanitize usage payload # ============================================================================ class TestSanitizeUsagePayload: """Tests for _sanitize_usage_payload method.""" def test_sanitize_dict_usage(self): sp, _ = _get_modules() usage = {"input_tokens": 10, "output_tokens": 5, "total_tokens": 15} sp.GenAISemanticProcessor._sanitize_usage_payload(usage) assert "total_tokens" not in usage assert usage["input_tokens"] == 10 def test_sanitize_object_usage(self): sp, _ = _get_modules() class Usage: def __init__(self): self.input_tokens = 10 self.output_tokens = 5 self.total_tokens = 15 usage = Usage() sp.GenAISemanticProcessor._sanitize_usage_payload(usage) assert usage.total_tokens is None def test_sanitize_none_usage(self): sp, _ = _get_modules() # Should not raise sp.GenAISemanticProcessor._sanitize_usage_payload(None) # ============================================================================ # Tests for message normalization # ============================================================================ class TestNormalizeMessagesToRoleParts: """Tests for _normalize_messages_to_role_parts method.""" def _make_processor(self, sensitive=True): sp, _ = _get_modules() TracerProvider, _, _ = _get_otel_test_fixtures() provider = TracerProvider() tracer = provider.get_tracer(__name__) return sp.GenAISemanticProcessor( tracer=tracer, system_name="openai", include_sensitive_data=sensitive, metrics_enabled=False, ) def test_normalize_non_dict_message(self): processor = self._make_processor() messages = ["plain text message"] normalized = processor._normalize_messages_to_role_parts(messages) assert len(normalized) == 1 assert normalized[0]["role"] == "user" assert normalized[0]["parts"][0]["type"] == "text" def test_normalize_with_redaction(self): processor = self._make_processor(sensitive=False) messages = [{"role": "user", "content": "Secret message"}] normalized = processor._normalize_messages_to_role_parts(messages) assert normalized[0]["parts"][0]["content"] == "readacted" def test_normalize_empty_messages_returns_empty_list(self): processor = self._make_processor() normalized = processor._normalize_messages_to_role_parts([]) assert normalized == [] def test_normalize_parts_tool_calls_and_tool_responses(self): processor = self._make_processor(sensitive=False) messages = [ { "role": "assistant", "parts": [ {"type": "text", "content": "hello"}, { "type": "tool_call", "id": "call_1", "name": "weather", "arguments": {"city": "Paris"}, }, { "type": "tool_call_response", "id": "call_1", "result": {"temp": 70}, }, {"type": "weird", "payload": {"x": 1}}, 123, ], "content": [ {"type": "text", "text": "from-content"}, {"type": "image", "url": "http://example.com"}, "raw", ], "tool_calls": [ { "id": "call_2", "function": {"name": "calc", "arguments": '{"x":1}'}, }, "not-dict", ], }, {"role": "tool", "tool_call_id": "call_2", "content": {"x": 1}}, ] normalized = processor._normalize_messages_to_role_parts(messages) assert normalized[0]["role"] == "assistant" assistant_part_types = {p["type"] for p in normalized[0]["parts"]} assert "tool_call" in assistant_part_types assert "tool_call_response" in assistant_part_types tool_call = next(p for p in normalized[0]["parts"] if p["type"] == "tool_call") assert tool_call["arguments"] == "readacted" tool_response = next(p for p in normalized[0]["parts"] if p["type"] == "tool_call_response") assert tool_response["result"] == "readacted" assert normalized[1]["role"] == "tool" assert normalized[1]["parts"][0]["type"] == "tool_call_response" assert normalized[1]["parts"][0]["result"] == "readacted" # ============================================================================ # Tests for helper functions # ============================================================================ class TestHelperFunctions: """Tests for various helper functions.""" def test_is_instance_of_single_class(self): sp, _ = _get_modules() assert sp._is_instance_of("hello", str) is True assert sp._is_instance_of(123, str) is False def test_is_instance_of_tuple_classes(self): sp, _ = _get_modules() assert sp._is_instance_of("hello", (str, int)) is True assert sp._is_instance_of(123, (str, int)) is True assert sp._is_instance_of(3.14, (str, int)) is False def test_span_status_helper(self): from types import SimpleNamespace from opentelemetry.trace.status import StatusCode sp, _ = _get_modules() status = sp._get_span_status(SimpleNamespace(error={"message": "boom", "data": "bad"})) assert status.status_code is StatusCode.ERROR assert status.description == "boom: bad" ok_status = sp._get_span_status(SimpleNamespace(error=None)) assert ok_status.status_code is StatusCode.OK # ============================================================================ # Tests for processor initialization and configuration # ============================================================================ class TestProcessorConfiguration: """Tests for GenAISemanticProcessor configuration.""" def test_processor_with_metrics_disabled(self): sp, _ = _get_modules() TracerProvider, _, _ = _get_otel_test_fixtures() provider = TracerProvider() tracer = provider.get_tracer(__name__) processor = sp.GenAISemanticProcessor( tracer=tracer, system_name="openai", metrics_enabled=False, ) assert processor._metrics_enabled is False assert processor._duration_histogram is None assert processor._token_usage_histogram is None processor.shutdown() def test_processor_with_custom_system_name(self): sp, _ = _get_modules() TracerProvider, _, _ = _get_otel_test_fixtures() provider = TracerProvider() tracer = provider.get_tracer(__name__) processor = sp.GenAISemanticProcessor( tracer=tracer, system_name="azure.ai.inference", metrics_enabled=False, ) assert processor.system_name == "azure.ai.inference" processor.shutdown() def test_processor_force_flush_is_noop(self): sp, _ = _get_modules() TracerProvider, _, _ = _get_otel_test_fixtures() provider = TracerProvider() tracer = provider.get_tracer(__name__) processor = sp.GenAISemanticProcessor( tracer=tracer, system_name="openai", metrics_enabled=False, ) # Should not raise result = processor.force_flush() assert result is None processor.shutdown() def test_processor_infers_server_from_base_url(self): sp, _ = _get_modules() TracerProvider, _, _ = _get_otel_test_fixtures() provider = TracerProvider() tracer = provider.get_tracer(__name__) processor = sp.GenAISemanticProcessor( tracer=tracer, system_name="openai", metrics_enabled=False, base_url="https://api.example.com:8443/v1", ) assert processor.server_address == "api.example.com" assert processor.server_port == 8443 server_attrs = processor._get_server_attributes() assert server_attrs[sp.ServerAttributes.SERVER_ADDRESS] == "api.example.com" assert server_attrs[sp.ServerAttributes.SERVER_PORT] == 8443 processor.shutdown() class TestRecordMetrics: def test_record_metrics_noop_when_disabled(self): sp, _ = _get_modules() processor = sp.GenAISemanticProcessor(metrics_enabled=False) processor._record_metrics(object(), {}) def test_record_metrics_records_duration_and_tokens(self): from types import SimpleNamespace sp, _ = _get_modules() TracerProvider, _, _ = _get_otel_test_fixtures() provider = TracerProvider() tracer = provider.get_tracer(__name__) class _Histogram: def __init__(self) -> None: self.records = [] def record(self, value, attributes) -> None: self.records.append((value, attributes)) duration_histogram = _Histogram() token_histogram = _Histogram() processor = sp.GenAISemanticProcessor(tracer=tracer, system_name="openai", metrics_enabled=False) processor._metrics_enabled = True processor._duration_histogram = duration_histogram processor._token_usage_histogram = token_histogram span = SimpleNamespace( started_at="2024-01-01T00:00:00+00:00", ended_at="2024-01-01T00:00:02+00:00", error={"type": "timeout"}, ) attributes = { sp.GEN_AI_PROVIDER_NAME: "openai", sp.GEN_AI_OPERATION_NAME: sp.GenAIOperationName.CHAT, sp.GEN_AI_REQUEST_MODEL: "gpt-4o-mini", sp.ServerAttributes.SERVER_ADDRESS: "api.example.com", sp.ServerAttributes.SERVER_PORT: 443, sp.GEN_AI_USAGE_INPUT_TOKENS: 2, sp.GEN_AI_USAGE_OUTPUT_TOKENS: 3, } processor._record_metrics(span, attributes) assert duration_histogram.records duration_value, duration_attrs = duration_histogram.records[0] assert duration_value == 2.0 assert duration_attrs["error.type"] == "timeout" assert len(token_histogram.records) == 2 token_types = {token_attrs[sp.GEN_AI_TOKEN_TYPE] for _, token_attrs in token_histogram.records} assert token_types == {"input", "output"} def test_record_metrics_swallows_exceptions(self): from types import SimpleNamespace sp, _ = _get_modules() TracerProvider, _, _ = _get_otel_test_fixtures() provider = TracerProvider() tracer = provider.get_tracer(__name__) class _BoomHistogram: def record(self, value, attributes) -> None: raise RuntimeError("boom") processor = sp.GenAISemanticProcessor(tracer=tracer, system_name="openai", metrics_enabled=False) processor._metrics_enabled = True processor._duration_histogram = _BoomHistogram() span = SimpleNamespace( started_at="2024-01-01T00:00:00+00:00", ended_at="2024-01-01T00:00:02+00:00", ) attributes = { sp.GEN_AI_PROVIDER_NAME: "openai", sp.GEN_AI_OPERATION_NAME: sp.GenAIOperationName.CHAT, sp.GEN_AI_REQUEST_MODEL: "gpt-4o-mini", } processor._record_metrics(span, attributes) class TestVersionModule: def test_version_importable(self): import importlib version_module = importlib.import_module("opentelemetry.instrumentation.openai_agents.version") assert isinstance(version_module.__version__, str) assert version_module.__version__ # ============================================================================ # Tests for normalize_to_text_parts # ============================================================================ class TestNormalizeToTextParts: """Tests for _normalize_to_text_parts method.""" def _make_processor(self): sp, _ = _get_modules() TracerProvider, _, _ = _get_otel_test_fixtures() provider = TracerProvider() tracer = provider.get_tracer(__name__) return sp.GenAISemanticProcessor(tracer=tracer, system_name="openai", metrics_enabled=False) def test_normalize_string_content(self): processor = self._make_processor() parts = processor._normalize_to_text_parts("Hello") assert len(parts) == 1 assert parts[0] == {"type": "text", "content": "Hello"} def test_normalize_list_content(self): processor = self._make_processor() parts = processor._normalize_to_text_parts(["Hello", "World"]) assert len(parts) == 2 assert parts[0]["content"] == "Hello" assert parts[1]["content"] == "World" def test_normalize_list_with_dict_missing_text_and_other_types(self): processor = self._make_processor() parts = processor._normalize_to_text_parts([{"nope": "missing"}, 123, "ok"]) assert [part["content"] for part in parts] == [ "{'nope': 'missing'}", "123", "ok", ] def test_normalize_dict_content_with_text(self): processor = self._make_processor() parts = processor._normalize_to_text_parts({"text": "From text key"}) assert len(parts) == 1 assert parts[0]["content"] == "From text key" def test_normalize_dict_without_text_or_content(self): processor = self._make_processor() parts = processor._normalize_to_text_parts({"nope": "missing"}) assert parts[0]["content"] == "{'nope': 'missing'}" def test_normalize_none_content(self): processor = self._make_processor() parts = processor._normalize_to_text_parts(None) assert parts == [] def test_normalize_other_type_content(self): processor = self._make_processor() parts = processor._normalize_to_text_parts(123) assert len(parts) == 1 assert parts[0]["content"] == "123" # ============================================================================ # Tests for collect_system_instructions # ============================================================================ class TestCollectSystemInstructions: """Tests for _collect_system_instructions method.""" def _make_processor(self): sp, _ = _get_modules() TracerProvider, _, _ = _get_otel_test_fixtures() provider = TracerProvider() tracer = provider.get_tracer(__name__) return sp.GenAISemanticProcessor(tracer=tracer, system_name="openai", metrics_enabled=False) def test_collect_system_role_message(self): processor = self._make_processor() messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello"}, ] collected = processor._collect_system_instructions(messages) assert len(collected) == 1 assert collected[0]["type"] == "text" assert collected[0]["content"] == "You are a helpful assistant." def test_collect_ai_role_message(self): processor = self._make_processor() messages = [ {"role": "ai", "content": "AI assistant instructions."}, ] collected = processor._collect_system_instructions(messages) assert len(collected) == 1 assert collected[0]["content"] == "AI assistant instructions." def test_collect_empty_messages(self): processor = self._make_processor() collected = processor._collect_system_instructions([]) assert collected == [] def test_collect_none_messages(self): processor = self._make_processor() collected = processor._collect_system_instructions(None) assert collected == [] def test_collect_skips_non_dict_messages(self): processor = self._make_processor() collected = processor._collect_system_instructions(["not-a-dict", {"role": "system", "content": "hi"}]) assert collected[0]["content"] == "hi" opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/000077500000000000000000000000001527070072300340245ustar00rootroot00000000000000.changelog/000077500000000000000000000000001527070072300357525ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2.gitignore000066400000000000000000000000001527070072300377300ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/.changelog4500.changed000066400000000000000000000001271527070072300376550ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/.changelogRefactor chat completion stream wrappers to use shared GenAI stream lifecycle helpers. 4554.changed000066400000000000000000000002451527070072300376670ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/.changelogPass tool definitions from `tools` kwarg to `InferenceInvocation.tool_definitions` so `gen_ai.tool.definitions` span attribute is populated on chat completion spans 4751.fixed000066400000000000000000000003471527070072300373770ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/.changelog`opentelemetry-instrumentation-openai-v2`: await `AsyncStream.close` when closing an async streaming chat completion on the default semantic convention path so the underlying httpx response/connection is released instead of leaked CHANGELOG.md000066400000000000000000000140701527070072300355600ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Version 2.4b0 (2026-05-01) - Migrate experimental path from deprecated `LLMInvocation` to `InferenceInvocation`, using `handler.start_inference()` and `invocation.stop()`/`invocation.fail()` directly ([#4502](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4502)) - Use `create_duration_histogram` and `create_token_histogram` from `opentelemetry-util-genai` instead of defining bucket boundaries locally ([#4501](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4501)) - Import `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` from `opentelemetry.util.genai.environment_variables` instead of re-defining it locally, making `opentelemetry-util-genai` the single source of truth for this constant. ([#4455](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4455)) - Fix compatibility with wrapt 2.x by using positional arguments in `wrap_function_wrapper()` calls ([#4445](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4445)) - Fix `ChoiceBuffer` crash on streaming tool-call deltas with `arguments=None` ([#4350](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4350)) - Fix `StreamWrapper` missing `.headers` and other attributes when using `with_raw_response` streaming ([#4113](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/4113)) - Add opt-in support for latest experimental semantic conventions (v1.37.0). Set `OTEL_SEMCONV_STABILITY_OPT_IN` to `gen_ai_latest_experimental` to enable. Add dependency on `opentelemetry-util-genai` pypi package. ([#3715](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3715)) - Add wrappers for OpenAI Responses API streams and response stream managers ([#4280](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4280)) - Add async wrappers for OpenAI Responses API streams and response stream managers ([#4325](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4325)) - Add strongly typed Responses API extractors with validation and content extraction improvements ([#4337](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4337)) - Add instrumentation for OpenAI Responses API `create` ([#4474](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4474)) - Add completion hook support. ([#4315](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4315)) - Fix `response_format` handling: map `json_object`/`json_schema` to `json` output type. ([#4315](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4315)) - Skip attribute values with `openai.Omit` value. ([#4315](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4315)) - Default empty string for `gen_ai.request.model` attribute on missing model. ([#4494](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4494)) ## Version 2.3b0 (2025-12-24) - Fix `AttributeError` when handling `LegacyAPIResponse` (from `with_raw_response`) ([#4017](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4017)) - Add support for chat completions choice count and stop sequences span attributes ([#4028](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4028)) - Fix crash with streaming `with_raw_response` ([#4033](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4033)) - Bump to 1.30.0 semconv schema: `gen_ai.request.seed` instead of `gen_ai.openai.request.seed` ([#4036](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4036)) ## Version 2.2b0 (2025-11-25) - Fix service tier attribute names: use `GEN_AI_OPENAI_REQUEST_SERVICE_TIER` for request attributes and `GEN_AI_OPENAI_RESPONSE_SERVICE_TIER` for response attributes. ([#3920](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3920)) - Added support for OpenAI embeddings instrumentation ([#3461](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3461)) - Record prompt and completion events regardless of span sampling decision. ([#3226](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3226)) - Filter out attributes with the value of NotGiven instances ([#3760](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3760)) - Migrate off the deprecated events API to use the logs API ([#3625](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3628)) ## Version 2.1b0 (2025-01-18) - Coerce openai response_format to semconv format ([#3073](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3073)) - Add example to `opentelemetry-instrumentation-openai-v2` ([#3006](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3006)) - Support for `AsyncOpenAI/AsyncCompletions` ([#2984](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2984)) - Add metrics ([#3180](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3180)) ## Version 2.0b0 (2024-11-08) - Use generic `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable to control if content of prompt, completion, and other messages is captured. ([#2947](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2947)) - Update OpenAI instrumentation to Semantic Conventions v1.28.0: add new attributes and switch prompts and completions to log-based events. ([#2925](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2925)) - Initial OpenAI instrumentation ([#2759](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2759)) LICENSE000066400000000000000000000261351527070072300347610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000151701527070072300354400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2OpenTelemetry OpenAI Instrumentation ==================================== .. warning:: **This package is deprecated.** Use the `opentelemetry-instrumentation-genai-openai `_ package instead. ``opentelemetry-instrumentation-openai-v2`` now only receives security patches. Please migrate by replacing it with ``opentelemetry-instrumentation-genai-openai``: .. code-block:: console pip uninstall opentelemetry-instrumentation-openai-v2 pip install opentelemetry-instrumentation-genai-openai The replacement package contains **breaking changes** relative to this one. Review the `CHANGELOG `_ before upgrading. |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-openai-v2.svg :target: https://pypi.org/project/opentelemetry-instrumentation-openai-v2/ This library allows tracing LLM requests and logging of messages made by the `OpenAI Python API library `_. It also captures the duration of the operations and the number of tokens used as metrics. Many LLM platforms support the OpenAI SDK. This means systems such as the following are observable with this instrumentation when accessed using it: .. list-table:: OpenAI Compatible Platforms :widths: 40 25 :header-rows: 1 * - Name - gen_ai.system * - `Azure OpenAI `_ - ``azure.ai.openai`` * - `Gemini `_ - ``gemini`` * - `Perplexity `_ - ``perplexity`` * - `xAI `_ (Compatible with Anthropic) - ``xai`` * - `DeepSeek `_ - ``deepseek`` * - `Groq `_ - ``groq`` * - `MistralAI `_ - ``mistral_ai`` Installation ------------ If your application is already instrumented with OpenTelemetry, add this package to your requirements. :: pip install opentelemetry-instrumentation-openai-v2 If you don't have an OpenAI application, yet, try our `examples `_ which only need a valid OpenAI API key. Check out `zero-code example `_ for a quick start. Usage ----- This section describes how to set up OpenAI instrumentation if you're setting OpenTelemetry up manually. Check out the `manual example `_ for more details. Instrumenting all clients ************************* When using the instrumentor, all clients will automatically trace OpenAI operations including chat completions and embeddings. You can also optionally capture prompts and completions as log events. Make sure to configure OpenTelemetry tracing, logging, and events to capture all telemetry emitted by the instrumentation. .. code-block:: python from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor OpenAIInstrumentor().instrument() client = OpenAI() # Chat completion example response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "user", "content": "Write a short poem on open telemetry."}, ], ) # Embeddings example embedding_response = client.embeddings.create( model="text-embedding-3-small", input="Generate vector embeddings for this text" ) Enabling message content ************************* Message content such as the contents of the prompt, completion, function arguments and return values are not captured by default. To capture message content as log events, set the environment variable ``OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`` to one of the following values: - ``true`` - Legacy. Used to enable content capturing on ``gen_ai.{role}.message`` and ``gen_ai.choice`` events when `latest experimental features <#enabling-the-latest-experimental-features>`_ are *not* enabled. - ``span_only`` - Used to enable content capturing on *span* attributes when `latest experimental features <#enabling-the-latest-experimental-features>`_ are enabled. - ``event_only`` - Used to enable content capturing on *event* attributes when `latest experimental features <#enabling-the-latest-experimental-features>`_ are enabled. - ``span_and_event`` - Used to enable content capturing on both *span* and *event* attributes when `latest experimental features <#enabling-the-latest-experimental-features>`_ are enabled. Uploading prompts and completions ********************************* To enable the built-in upload hook, set: - ``OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK=upload`` - ``OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH`` to an ``fsspec``-compatible URI/path (e.g. ``/path/to/prompts`` or ``gs://my_bucket``). Install the ``upload`` extra to pull in ``fsspec``:: pip install opentelemetry-util-genai[upload] See the `opentelemetry-util-genai `_ for additional options. Enabling the latest experimental features *********************************************** To enable the latest experimental features, set the environment variable ``OTEL_SEMCONV_STABILITY_OPT_IN`` to ``gen_ai_latest_experimental``. Or, if you use ``OTEL_SEMCONV_STABILITY_OPT_IN`` to enable other features, append ``,gen_ai_latest_experimental`` to its value. Without this setting, OpenAI instrumentation aligns with `Semantic Conventions v1.30.0 `_ and would not capture additional details introduced in later versions. .. note:: Generative AI semantic conventions are still evolving. The latest experimental features will introduce breaking changes in future releases. Uninstrument ************ To uninstrument clients, call the uninstrument method: .. code-block:: python from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor OpenAIInstrumentor().instrument() # ... # Uninstrument all clients OpenAIInstrumentor().uninstrument() References ---------- * `OpenTelemetry OpenAI Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ examples/000077500000000000000000000000001527070072300355635ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2embeddings/000077500000000000000000000000001527070072300376645ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/examples.env000066400000000000000000000011221527070072300404510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/examples/embeddings# Update this with your real OpenAI API key OPENAI_API_KEY=sk-YOUR_API_KEY # Uncomment to use Ollama instead of OpenAI # OPENAI_BASE_URL=http://localhost:11434/v1 # OPENAI_API_KEY=unused # CHAT_MODEL=qwen2.5:0.5b # Uncomment and change to your OTLP endpoint # OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 # OTEL_EXPORTER_OTLP_PROTOCOL=grpc OTEL_SERVICE_NAME=opentelemetry-python-openai # Uncomment if your OTLP endpoint doesn't support logs # OTEL_LOGS_EXPORTER=console # Change to 'false' to hide prompt and completion content OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true README.rst000066400000000000000000000025441527070072300413600ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/examples/embeddingsOpenTelemetry OpenAI Embeddings API Instrumentation Example =========================================================== This is an example of how to instrument OpenAI Embeddings API calls with zero code changes, using ``opentelemetry-instrument``. When ``main.py`` is run, it exports traces and metrics to an OTLP compatible endpoint. Traces include details such as the model used, dimensions of embeddings, and the duration of the embedding request. Metrics capture token usage and performance data. Note: ``.env`` file configures additional environment variables: - ``OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true`` configures OpenAI instrumentation to capture content on events. - ``OTEL_LOGS_EXPORTER=otlp`` to specify exporter type. Setup ----- Minimally, update the ``.env`` file with your ``OPENAI_API_KEY``. An OTLP compatible endpoint should be listening for traces and logs on http://localhost:4317. If not, update ``OTEL_EXPORTER_OTLP_ENDPOINT`` as well. Next, set up a virtual environment like this: :: python3 -m venv .venv source .venv/bin/activate pip install "python-dotenv[cli]" pip install -r requirements.txt Run --- Run the example like this: :: dotenv run -- opentelemetry-instrument python main.py You should see embedding information printed while traces and metrics export to your configured observability tool. main.py000066400000000000000000000015721527070072300411670ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/examples/embeddings# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os from openai import OpenAI def main(): client = OpenAI() # Create embeddings with OpenAI API embedding_response = client.embeddings.create( model=os.getenv("EMBEDDING_MODEL", "text-embedding-3-small"), input="OpenTelemetry provides observability for your applications.", ) # Print embedding information print(f"Model: {embedding_response.model}") print(f"Dimensions: {len(embedding_response.data[0].embedding)}") print( f"Token usage - Prompt: {embedding_response.usage.prompt_tokens}, Total: {embedding_response.usage.total_tokens}" ) # Print a sample of the embedding vector (first 5 dimensions) print(f"Embedding sample (first 5 dimensions): {embedding_response.data[0].embedding[:5]}") if __name__ == "__main__": main() requirements.txt000066400000000000000000000002451527070072300431510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/examples/embeddingsopenai~=1.57.3 opentelemetry-sdk~=1.36.0 opentelemetry-exporter-otlp-proto-grpc~=1.36.0 opentelemetry-distro~=0.57b0 opentelemetry-instrumentation-openai-v2~=2.2b0 manual/000077500000000000000000000000001527070072300370405ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/examples.env000066400000000000000000000030221527070072300376260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/examples/manual# Update this with your real OpenAI API key OPENAI_API_KEY=sk-YOUR_API_KEY # Uncomment to use Ollama instead of OpenAI # OPENAI_BASE_URL=http://localhost:11434/v1 # OPENAI_API_KEY=unused # CHAT_MODEL=qwen2.5:0.5b # Uncomment and change to your OTLP endpoint # OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 # OTEL_EXPORTER_OTLP_PROTOCOL=grpc OTEL_SERVICE_NAME=opentelemetry-python-openai # Remove to hide prompt and completion content # Possible values (case insensitive): # - `span_only` - record content on span attibutes # - `event_only` - record content on event attributes # - `span_and_event` - record content on both span and event attributes # - `true` - only used for backward compatibility when # `gen_ai_latest_experimental` is not set in the # `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. # - everything else - don't record content on any signal OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=span_only # Enables latest and greatest features available in GenAI semantic conventions. # Note: since conventions are still in development, using this flag would # likely result in having breaking changes. # # Comment out if you want to use semantic conventions of version 1.30.0. OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental # Uncomment to upload prompts and responses to an fsspec-compatible # destination instead of or in addition to recording them inline on spans/events. # OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK=upload # OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH=/path/to/prompts README.rst000066400000000000000000000044141527070072300405320ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/examples/manualOpenTelemetry OpenAI Instrumentation Example ============================================ This is an example of how to instrument OpenAI calls when configuring OpenTelemetry SDK and Instrumentations manually. When `main.py `_ is run, it exports traces and logs to an OTLP compatible endpoint. Traces include details such as the model used and the duration of the chat request. Logs capture the chat request and the generated response, providing a comprehensive view of the performance and behavior of your OpenAI requests. Note: `.env <.env>`_ file configures additional environment variables: - ``OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=span_only`` configures OpenAI instrumentation to capture prompt and completion contents on *span* attributes. - ``OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental`` enables latest experimental features. - ``OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK`` (commented out) - uncomment along with ``OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH`` to upload prompts and completions to an ``fsspec``-compatible destination instead of recording them inline. Also uncomment the ``opentelemetry-util-genai[upload]`` line in `requirements.txt `_ and reinstall. Setup ----- Minimally, update the `.env <.env>`_ file with your ``OPENAI_API_KEY``. An OTLP compatible endpoint should be listening for traces and logs on http://localhost:4317. If not, update ``OTEL_EXPORTER_OTLP_ENDPOINT`` as well. Next, set up a virtual environment like this: :: python3 -m venv .venv source .venv/bin/activate pip install "python-dotenv[cli]" pip install -r requirements.txt Run --- Run the example like this: :: dotenv run -- python main.py You should see a poem generated by OpenAI while traces and logs export to your configured observability tool. Custom completion hook ---------------------- `custom_hook.py `_ is a variant of ``main.py`` that passes a custom ``CompletionHook`` implementation programmatically via ``OpenAIInstrumentor().instrument(completion_hook=...)``. The example hook prints prompts and completions to stdout; real hooks typically forward content to external storage and record reference URIs on the span/log_record. Run it the same way: :: dotenv run -- python custom_hook.py custom_hook.py000066400000000000000000000053571527070072300417560ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/examples/manual# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: skip-file """Same as main.py, but instruments OpenAI with a custom CompletionHook that prints prompts and completions to stdout. Run with: dotenv run -- python custom_hook.py """ import os from openai import OpenAI from opentelemetry import _logs, metrics, trace from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( OTLPLogExporter, ) from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( OTLPMetricExporter, ) from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import BatchLogRecordProcessor from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.util.genai.completion_hook import CompletionHook from opentelemetry.util.genai.types import ( InputMessage, MessagePart, OutputMessage, ToolDefinition, ) class PrintCompletionHook(CompletionHook): """Minimal CompletionHook that prints inputs/outputs to stdout. Real hooks typically forward content to external storage (object store, database, etc.) and record reference URIs on the span/log_record. """ def on_completion( self, *, inputs: list[InputMessage], outputs: list[OutputMessage], system_instruction: list[MessagePart], tool_definitions: list[ToolDefinition] | None = None, span=None, log_record=None, ) -> None: print(f"[hook] inputs: {inputs}") print(f"[hook] outputs: {outputs}") trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(OTLPSpanExporter())) _logs.set_logger_provider(LoggerProvider()) _logs.get_logger_provider().add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter())) metrics.set_meter_provider( MeterProvider( metric_readers=[ PeriodicExportingMetricReader(OTLPMetricExporter()), ] ) ) OpenAIInstrumentor().instrument(completion_hook=PrintCompletionHook()) def main(): client = OpenAI() chat_completion = client.chat.completions.create( model=os.getenv("CHAT_MODEL", "gpt-4o-mini"), messages=[ { "role": "user", "content": "Write a short poem on OpenTelemetry.", }, ], ) print(chat_completion.choices[0].message.content) if __name__ == "__main__": main() main.py000066400000000000000000000035741527070072300403470ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/examples/manual# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: skip-file import os from openai import OpenAI # NOTE: OpenTelemetry Python Logs API is in beta from opentelemetry import _logs, metrics, trace from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( OTLPLogExporter, ) from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( OTLPMetricExporter, ) from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import BatchLogRecordProcessor from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor # configure tracing trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(OTLPSpanExporter())) # configure logging _logs.set_logger_provider(LoggerProvider()) _logs.get_logger_provider().add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter())) # configure metrics metrics.set_meter_provider( MeterProvider( metric_readers=[ PeriodicExportingMetricReader( OTLPMetricExporter(), ), ] ) ) # instrument OpenAI OpenAIInstrumentor().instrument() def main(): client = OpenAI() chat_completion = client.chat.completions.create( model=os.getenv("CHAT_MODEL", "gpt-4o-mini"), messages=[ { "role": "user", "content": "Write a short poem on OpenTelemetry.", }, ], ) print(chat_completion.choices[0].message.content) if __name__ == "__main__": main() requirements.txt000066400000000000000000000004631527070072300423270ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/examples/manualopenai~=1.57.3 opentelemetry-sdk~=1.36.0 opentelemetry-exporter-otlp-proto-grpc~=1.36.0 opentelemetry-instrumentation-openai-v2~=2.1b0 # Uncomment to enable the upload completion hook (pulls in fsspec). # Required when OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK=upload. # opentelemetry-util-genai[upload] zero-code/000077500000000000000000000000001527070072300374525ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/examples.env000066400000000000000000000031471527070072300402500ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/examples/zero-code# Update this with your real OpenAI API key OPENAI_API_KEY=sk-YOUR_API_KEY # Uncomment to use Ollama instead of OpenAI # OPENAI_BASE_URL=http://localhost:11434/v1 # OPENAI_API_KEY=unused # CHAT_MODEL=qwen2.5:0.5b # Uncomment and change to your OTLP endpoint # OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 # OTEL_EXPORTER_OTLP_PROTOCOL=grpc OTEL_SERVICE_NAME=opentelemetry-python-openai # Uncomment if your OTLP endpoint doesn't support logs # OTEL_LOGS_EXPORTER=console # Remove to hide prompt and completion content # Possible values (case insensitive): # - `span_only` - record content on span attibutes # - `event_only` - record content on event attributes # - `span_and_event` - record content on both span and event attributes # - `true` - only used for backward compatibility when # `gen_ai_latest_experimental` is not set in the # `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. # - everything else - don't record content on any signal OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=span_only # Enables latest and greatest features available in GenAI semantic conventions. # Note: since conventions are still in development, using this flag would # likely result in having breaking changes. # # Comment out if you want to use semantic conventions of version 1.30.0. OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental # Uncomment to upload prompts and responses to an fsspec-compatible # destination instead of or in addition to recording them inline on spans/events. # OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK=upload # OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH=/path/to/prompts README.rst000066400000000000000000000036131527070072300411440ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/examples/zero-codeOpenTelemetry OpenAI Zero-Code Instrumentation Example ====================================================== This is an example of how to instrument OpenAI calls with zero code changes, using `opentelemetry-instrument`. When `main.py `_ is run, it exports traces and logs to an OTLP compatible endpoint. Traces include details such as the model used and the duration of the chat request. Logs capture the chat request and the generated response, providing a comprehensive view of the performance and behavior of your OpenAI requests. Note: `.env <.env>`_ file configures additional environment variables: - ``OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=span_only`` configures OpenAI instrumentation to capture prompt and completion contents on *span* attributes. - ``OTEL_LOGS_EXPORTER=otlp`` to specify exporter type. - ``OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental`` enables latest experimental features. - ``OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK`` (commented out) - uncomment along with ``OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH`` to upload prompts and completions to an ``fsspec``-compatible destination instead of recording them inline. Also uncomment the ``opentelemetry-util-genai[upload]`` line in `requirements.txt `_ and reinstall. Setup ----- Minimally, update the `.env <.env>`_ file with your ``OPENAI_API_KEY``. An OTLP compatible endpoint should be listening for traces and logs on http://localhost:4317. If not, update ``OTEL_EXPORTER_OTLP_ENDPOINT`` as well. Next, set up a virtual environment like this: :: python3 -m venv .venv source .venv/bin/activate pip install "python-dotenv[cli]" pip install -r requirements.txt Run --- Run the example like this: :: dotenv run -- opentelemetry-instrument python main.py You should see a poem generated by OpenAI while traces and logs export to your configured observability tool. main.py000066400000000000000000000010041527070072300407430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/examples/zero-code# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os from openai import OpenAI def main(): client = OpenAI() chat_completion = client.chat.completions.create( model=os.getenv("CHAT_MODEL", "gpt-4o-mini"), messages=[ { "role": "user", "content": "Write a short poem on OpenTelemetry.", }, ], ) print(chat_completion.choices[0].message.content) if __name__ == "__main__": main() requirements.txt000066400000000000000000000005201527070072300427330ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/examples/zero-codeopenai~=1.57.3 opentelemetry-sdk~=1.36.0 opentelemetry-exporter-otlp-proto-grpc~=1.36.0 opentelemetry-distro~=0.57b0 opentelemetry-instrumentation-openai-v2~=2.1b0 # Uncomment to enable the upload completion hook (pulls in fsspec). # Required when OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK=upload. # opentelemetry-util-genai[upload] pyproject.toml000066400000000000000000000046001527070072300366610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-openai-v2" dynamic = ["version"] description = "OpenTelemetry Official OpenAI instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.40", "opentelemetry-instrumentation ~= 0.61b0", "opentelemetry-semantic-conventions ~= 0.61b0", "opentelemetry-util-genai > 0.4b0, < 0.6b0", "typing-extensions >= 4.5.0", ] [project.optional-dependencies] instruments = ["openai >= 1.26.0"] [project.entry-points.opentelemetry_instrumentor] openai = "opentelemetry.instrumentation.openai_v2:OpenAIInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation-genai/opentelemetry-instrumentation-openai-v2" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/openai_v2/version.py" [tool.hatch.build.targets.sdist] include = ["/src", "/tests"] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] [tool.towncrier] directory = ".changelog" filename = "CHANGELOG.md" start_string = "\n" template = "../../scripts/changelog_template.j2" issue_format = "[#{issue}](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/{issue})" wrap = true issue_pattern = "^(\\d+)" [[tool.towncrier.type]] directory = "added" name = "Added" showcontent = true [[tool.towncrier.type]] directory = "changed" name = "Changed" showcontent = true [[tool.towncrier.type]] directory = "deprecated" name = "Deprecated" showcontent = true [[tool.towncrier.type]] directory = "removed" name = "Removed" showcontent = true [[tool.towncrier.type]] directory = "fixed" name = "Fixed" showcontent = true src/000077500000000000000000000000001527070072300345345ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2opentelemetry/000077500000000000000000000000001527070072300374305ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/srcinstrumentation/000077500000000000000000000000001527070072300426735ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetryopenai_v2/000077500000000000000000000000001527070072300445555ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation__init__.py000066400000000000000000000174751527070072300467040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ OpenAI client instrumentation supporting `openai`, it can be enabled by using ``OpenAIInstrumentor``. .. _openai: https://pypi.org/project/openai/ Usage ----- .. code:: python from openai import OpenAI from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor OpenAIInstrumentor().instrument() client = OpenAI() response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "user", "content": "Write a short poem on open telemetry."}, ], ) Configuration ------------- By default, the instrumentation aligns with `Semantic Conventions v1.30.0 `_ and does not capture prompt or completion content. Behavior is controlled via environment variables: - ``OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental`` - opt into the latest GenAI semantic conventions. Required to access the newer attributes and the ``span_only`` / ``event_only`` / ``span_and_event`` content modes. Without this flag, the instrumentation stays on v1.30.0 conventions. - ``OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`` - enable capture of prompts, completions, tool arguments, and return values. Set to ``true`` on the legacy path, or one of ``span_only``, ``event_only``, ``span_and_event`` when experimental conventions are enabled. - ``OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK=upload`` together with ``OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH=`` - upload prompts and completions to an ``fsspec``-compatible destination (local filesystem, ``gs://``, ``s3://``, etc.) and record reference URIs as ``gen_ai.input.messages.ref`` / ``gen_ai.output.messages.ref`` attributes. Inline content is not captured unless ``OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`` is also set. See the `opentelemetry-util-genai README `_ for the full list of GenAI configuration variables. A custom ``CompletionHook`` implementation can also be passed programmatically:: OpenAIInstrumentor().instrument(completion_hook=my_hook) When provided, this takes precedence over the hook resolved from ``OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK``. API --- """ from collections.abc import Collection from importlib import import_module from typing_extensions import deprecated from wrapt import wrap_function_wrapper from opentelemetry._logs import get_logger from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.openai_v2.package import _instruments from opentelemetry.instrumentation.openai_v2.utils import is_content_enabled from opentelemetry.instrumentation.utils import unwrap from opentelemetry.metrics import get_meter from opentelemetry.semconv.schemas import Schemas from opentelemetry.trace import get_tracer from opentelemetry.util.genai.completion_hook import load_completion_hook from opentelemetry.util.genai.handler import ( TelemetryHandler, ) from opentelemetry.util.genai.utils import is_experimental_mode from .instruments import Instruments from .patch import ( async_chat_completions_create_v_new, async_chat_completions_create_v_old, async_embeddings_create, chat_completions_create_v_new, chat_completions_create_v_old, embeddings_create, ) from .patch_responses import ( responses_create, ) @deprecated( "opentelemetry-instrumentation-openai-v2 is deprecated. Use the " "opentelemetry-instrumentation-genai-openai package instead. This package " "only receives security patches." ) class OpenAIInstrumentor(BaseInstrumentor): """OpenTelemetry instrumentation for the OpenAI Python client. .. deprecated:: 2.5b0 Use the ``opentelemetry-instrumentation-genai-openai`` package instead. This package only receives security patches. """ def __init__(self): self._meter = None def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): """Enable OpenAI instrumentation.""" latest_experimental_enabled = is_experimental_mode() tracer_provider = kwargs.get("tracer_provider") tracer = get_tracer( __name__, "", tracer_provider, schema_url=Schemas.V1_30_0.value, # only used on the legacy path ) logger_provider = kwargs.get("logger_provider") logger = get_logger( __name__, "", logger_provider=logger_provider, schema_url=Schemas.V1_30_0.value, # only used on the legacy path ) meter_provider = kwargs.get("meter_provider") self._meter = get_meter( __name__, "", meter_provider, schema_url=Schemas.V1_30_0.value, # only used on the legacy path ) instruments = Instruments(self._meter) handler = TelemetryHandler( tracer_provider=tracer_provider, meter_provider=meter_provider, logger_provider=logger_provider, completion_hook=kwargs.get("completion_hook") or load_completion_hook(), ) wrap_function_wrapper( "openai.resources.chat.completions", "Completions.create", ( chat_completions_create_v_new(handler) if latest_experimental_enabled else chat_completions_create_v_old(tracer, logger, instruments, is_content_enabled()) ), ) wrap_function_wrapper( "openai.resources.chat.completions", "AsyncCompletions.create", ( async_chat_completions_create_v_new(handler) if latest_experimental_enabled else async_chat_completions_create_v_old(tracer, logger, instruments, is_content_enabled()) ), ) # Add instrumentation for the embeddings API wrap_function_wrapper( "openai.resources.embeddings", "Embeddings.create", embeddings_create(tracer, instruments, latest_experimental_enabled), ) wrap_function_wrapper( "openai.resources.embeddings", "AsyncEmbeddings.create", async_embeddings_create(tracer, instruments, latest_experimental_enabled), ) responses_module = _get_responses_module() # Responses instrumentation is intentionally limited to the latest # experimental semconv path. Unlike chat completions, we do not carry # a second legacy wrapper here; the current implementation is built on # the inference handler lifecycle and would need a separate old-path # implementation to support legacy semconv mode. if responses_module is not None and latest_experimental_enabled: wrap_function_wrapper( "openai.resources.responses.responses", "Responses.create", responses_create(handler), ) def _uninstrument(self, **kwargs): import openai # pylint: disable=import-outside-toplevel # noqa: PLC0415 unwrap(openai.resources.chat.completions.Completions, "create") unwrap(openai.resources.chat.completions.AsyncCompletions, "create") unwrap(openai.resources.embeddings.Embeddings, "create") unwrap(openai.resources.embeddings.AsyncEmbeddings, "create") responses_module = _get_responses_module() if responses_module is not None: unwrap(responses_module.Responses, "create") def _get_responses_module(): try: return import_module("openai.resources.responses.responses") except ImportError: return None chat_buffers.py000066400000000000000000000031351527070072300475640ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from openai.types.chat.chat_completion_chunk import ChoiceDeltaToolCall class ToolCallBuffer: def __init__( self, index: int, tool_call_id: str | None, function_name: str | None, ) -> None: self.index: int = index self.function_name: str | None = function_name self.tool_call_id: str | None = tool_call_id self.arguments: list[str] = [] def append_arguments(self, arguments: str | None) -> None: if arguments is not None: self.arguments.append(arguments) class ChoiceBuffer: def __init__(self, index: int) -> None: self.index: int = index self.finish_reason: str | None = None self.text_content: list[str] = [] self.tool_calls_buffers: list[ToolCallBuffer | None] = [] def append_text_content(self, content: str) -> None: self.text_content.append(content) def append_tool_call(self, tool_call: ChoiceDeltaToolCall) -> None: idx = tool_call.index for _ in range(len(self.tool_calls_buffers), idx + 1): self.tool_calls_buffers.append(None) function = tool_call.function buffer = self.tool_calls_buffers[idx] if buffer is None: buffer = ToolCallBuffer( idx, tool_call.id, function.name if function else None, ) self.tool_calls_buffers[idx] = buffer if function: buffer.append_arguments(function.arguments) chat_wrappers.py000066400000000000000000000155601527070072300500000ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import json from openai import AsyncStream, Stream from openai.types.chat import ChatCompletionChunk from opentelemetry.semconv._incubating.attributes import ( openai_attributes as OpenAIAttributes, ) from opentelemetry.util.genai.invocation import InferenceInvocation from opentelemetry.util.genai.stream import ( AsyncStreamWrapper, SyncStreamWrapper, ) from opentelemetry.util.genai.types import ( OutputMessage, Text, ToolCallRequest, ) from .chat_buffers import ChoiceBuffer class _ChatStreamMixin: """Chat-specific hooks shared by sync and async stream wrappers.""" _self_invocation: InferenceInvocation _self_capture_content: bool _self_choice_buffers: list[ChoiceBuffer] _self_response_id: str | None _self_response_model: str | None _self_service_tier: str | None _self_prompt_tokens: int | None _self_completion_tokens: int | None def _set_response_model(self, chunk: ChatCompletionChunk) -> None: if self._self_response_model: return if chunk.model: self._self_response_model = chunk.model def _set_response_id(self, chunk: ChatCompletionChunk) -> None: if self._self_response_id: return if chunk.id: self._self_response_id = chunk.id def _set_response_service_tier(self, chunk: ChatCompletionChunk) -> None: if self._self_service_tier: return service_tier = getattr(chunk, "service_tier", None) if service_tier: self._self_service_tier = service_tier def _build_streaming_response(self, chunk: ChatCompletionChunk) -> None: if chunk.choices is None: return for choice in chunk.choices: if not choice.delta: continue for idx in range(len(self._self_choice_buffers), choice.index + 1): self._self_choice_buffers.append(ChoiceBuffer(idx)) if choice.finish_reason: self._self_choice_buffers[choice.index].finish_reason = choice.finish_reason if choice.delta.content is not None: self._self_choice_buffers[choice.index].append_text_content(choice.delta.content) if choice.delta.tool_calls is not None: for tool_call in choice.delta.tool_calls: self._self_choice_buffers[choice.index].append_tool_call(tool_call) def _set_usage(self, chunk: ChatCompletionChunk) -> None: usage = getattr(chunk, "usage", None) if usage: self._self_completion_tokens = usage.completion_tokens self._self_prompt_tokens = usage.prompt_tokens def _process_chunk(self, chunk: ChatCompletionChunk) -> None: self._set_response_id(chunk) self._set_response_model(chunk) self._set_response_service_tier(chunk) self._build_streaming_response(chunk) self._set_usage(chunk) def _set_output_messages(self) -> None: if not self._self_capture_content: # optimization return output_messages = [] for choice in self._self_choice_buffers: message = OutputMessage( role="assistant", finish_reason=choice.finish_reason or "error", parts=[], ) if choice.text_content: message.parts.append(Text(content="".join(choice.text_content))) if choice.tool_calls_buffers: tool_calls = [] for tool_call in filter(None, choice.tool_calls_buffers): arguments = None arguments_str = "".join(tool_call.arguments) if arguments_str: try: arguments = json.loads(arguments_str) except json.JSONDecodeError: arguments = arguments_str tool_call_part = ToolCallRequest( name=tool_call.function_name, id=tool_call.tool_call_id, arguments=arguments, ) tool_calls.append(tool_call_part) message.parts.extend(tool_calls) output_messages.append(message) self._self_invocation.output_messages = output_messages def _on_stream_end(self) -> None: self._cleanup() def _on_stream_error(self, error: BaseException) -> None: self._cleanup(error) def parse(self) -> _ChatStreamMixin: """Called when using with_raw_response with stream=True.""" return self def _cleanup(self, error: BaseException | None = None) -> None: self._self_invocation.response_model_name = self._self_response_model self._self_invocation.response_id = self._self_response_id self._self_invocation.input_tokens = self._self_prompt_tokens self._self_invocation.output_tokens = self._self_completion_tokens finish_reasons = [choice.finish_reason for choice in self._self_choice_buffers if choice.finish_reason] if finish_reasons: self._self_invocation.finish_reasons = finish_reasons if self._self_service_tier: self._self_invocation.attributes.update( {OpenAIAttributes.OPENAI_RESPONSE_SERVICE_TIER: self._self_service_tier}, ) self._set_output_messages() if error: self._self_invocation.fail(error) else: self._self_invocation.stop() class ChatStreamWrapper( _ChatStreamMixin, SyncStreamWrapper[ChatCompletionChunk], ): def __init__( self, stream: Stream[ChatCompletionChunk], invocation: InferenceInvocation, capture_content: bool, ) -> None: super().__init__(stream) self._self_invocation = invocation self._self_choice_buffers = [] self._self_capture_content = capture_content self._self_response_id = None self._self_response_model = None self._self_service_tier = None self._self_prompt_tokens = None self._self_completion_tokens = None class AsyncChatStreamWrapper( _ChatStreamMixin, AsyncStreamWrapper[ChatCompletionChunk], ): def __init__( self, stream: AsyncStream[ChatCompletionChunk], invocation: InferenceInvocation, capture_content: bool, ) -> None: super().__init__(stream) self._self_invocation = invocation self._self_choice_buffers = [] self._self_capture_content = capture_content self._self_response_id = None self._self_response_model = None self._self_service_tier = None self._self_prompt_tokens = None self._self_completion_tokens = None __all__ = [ "AsyncChatStreamWrapper", "ChatStreamWrapper", ] instruments.py000066400000000000000000000007211527070072300475220ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.metrics import Histogram, Meter from opentelemetry.util.genai.instruments import ( create_duration_histogram, create_token_histogram, ) class Instruments: def __init__(self, meter: Meter): self.operation_duration_histogram: Histogram = create_duration_histogram(meter) self.token_usage_histogram: Histogram = create_token_histogram(meter) package.py000066400000000000000000000002151527070072300465200ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("openai >= 1.26.0",) _supports_metrics = True patch.py000066400000000000000000000636141527070072300462400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from timeit import default_timer from typing import Any from openai import AsyncStream, Stream from opentelemetry._logs import Logger, LogRecord from opentelemetry.context import get_current from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, ) from opentelemetry.semconv._incubating.attributes import ( openai_attributes as OpenAIAttributes, ) from opentelemetry.semconv._incubating.attributes import ( server_attributes as ServerAttributes, ) from opentelemetry.trace import Span, SpanKind, Tracer from opentelemetry.trace.propagation import set_span_in_context from opentelemetry.util.genai.handler import TelemetryHandler from opentelemetry.util.genai.invocation import InferenceInvocation from opentelemetry.util.genai.types import ( Error, ) from .chat_buffers import ChoiceBuffer from .chat_wrappers import AsyncChatStreamWrapper, ChatStreamWrapper from .instruments import Instruments from .utils import ( _prepare_output_messages, choice_to_event, create_chat_invocation, get_llm_request_attributes, handle_span_exception, is_streaming, message_to_event, set_span_attribute, ) def chat_completions_create_v_old( tracer: Tracer, logger: Logger, instruments: Instruments, capture_content: bool, ): """Wrap the `create` method of the `ChatCompletion` class to trace it.""" def traced_method(wrapped, instance, args, kwargs): span_attributes = {**get_llm_request_attributes(kwargs, instance, False)} operation_name = span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME] model = span_attributes.get(GenAIAttributes.GEN_AI_REQUEST_MODEL) span_name = f"{operation_name} {model}" if model else operation_name with tracer.start_as_current_span( name=span_name, kind=SpanKind.CLIENT, attributes=span_attributes, end_on_exit=False, ) as span: for message in kwargs.get("messages", []): logger.emit(message_to_event(message, capture_content)) start = default_timer() result = None error_type = None try: result = wrapped(*args, **kwargs) if hasattr(result, "parse"): # result is of type LegacyAPIResponse, call parse to get the actual response parsed_result = result.parse() else: parsed_result = result if is_streaming(kwargs): return LegacyChatStreamWrapper(parsed_result, span, logger, capture_content) if span.is_recording(): _set_response_attributes(span, parsed_result) for choice in getattr(parsed_result, "choices", []): logger.emit(choice_to_event(choice, capture_content)) span.end() return result except Exception as error: error_type = type(error).__qualname__ handle_span_exception(span, error) raise finally: duration = max((default_timer() - start), 0) _record_metrics( instruments, duration, result, span_attributes, error_type, GenAIAttributes.GenAiOperationNameValues.CHAT.value, ) return traced_method def chat_completions_create_v_new( handler: TelemetryHandler, ): """Wrap the `create` method of the `ChatCompletion` class to trace it.""" capture_content = handler.should_capture_content() def traced_method(wrapped, instance, args, kwargs): chat_invocation = create_chat_invocation(handler, kwargs, instance, capture_content=capture_content) try: result = wrapped(*args, **kwargs) if hasattr(result, "parse"): # result is of type LegacyAPIResponse, call parse to get the actual response parsed_result = result.parse() else: parsed_result = result if is_streaming(kwargs): return ChatStreamWrapper(parsed_result, chat_invocation, capture_content) _set_response_properties(chat_invocation, parsed_result, capture_content) chat_invocation.stop() return result except Exception as error: chat_invocation.fail(Error(type=type(error), message=str(error))) raise return traced_method def async_chat_completions_create_v_old( tracer: Tracer, logger: Logger, instruments: Instruments, capture_content: bool, ): """Wrap the `create` method of the `AsyncChatCompletion` class to trace it.""" async def traced_method(wrapped, instance, args, kwargs): span_attributes = {**get_llm_request_attributes(kwargs, instance, False)} operation_name = span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME] model = span_attributes.get(GenAIAttributes.GEN_AI_REQUEST_MODEL) span_name = f"{operation_name} {model}" if model else operation_name with tracer.start_as_current_span( name=span_name, kind=SpanKind.CLIENT, attributes=span_attributes, end_on_exit=False, ) as span: for message in kwargs.get("messages", []): logger.emit(message_to_event(message, capture_content)) start = default_timer() result = None error_type = None try: result = await wrapped(*args, **kwargs) if hasattr(result, "parse"): # result is of type LegacyAPIResponse, calling parse to get the actual response parsed_result = result.parse() else: parsed_result = result if is_streaming(kwargs): return AsyncLegacyChatStreamWrapper(parsed_result, span, logger, capture_content) if span.is_recording(): _set_response_attributes(span, parsed_result) for choice in getattr(parsed_result, "choices", []): logger.emit(choice_to_event(choice, capture_content)) span.end() return result except Exception as error: error_type = type(error).__qualname__ handle_span_exception(span, error) raise finally: duration = max((default_timer() - start), 0) _record_metrics( instruments, duration, result, span_attributes, error_type, GenAIAttributes.GenAiOperationNameValues.CHAT.value, ) return traced_method def async_chat_completions_create_v_new( handler: TelemetryHandler, ): """Wrap the `create` method of the `AsyncChatCompletion` class to trace it.""" capture_content = handler.should_capture_content() async def traced_method(wrapped, instance, args, kwargs): chat_invocation = create_chat_invocation(handler, kwargs, instance, capture_content=capture_content) try: result = await wrapped(*args, **kwargs) if hasattr(result, "parse"): # result is of type LegacyAPIResponse, calling parse to get the actual response parsed_result = result.parse() else: parsed_result = result if is_streaming(kwargs): return AsyncChatStreamWrapper(parsed_result, chat_invocation, capture_content) _set_response_properties(chat_invocation, parsed_result, capture_content) chat_invocation.stop() return result except Exception as error: chat_invocation.fail(Error(type=type(error), message=str(error))) raise return traced_method def embeddings_create( tracer: Tracer, instruments: Instruments, latest_experimental_enabled: bool, ): """Wrap the `create` method of the `Embeddings` class to trace it.""" def traced_method(wrapped, instance, args, kwargs): span_attributes = get_llm_request_attributes( kwargs, instance, latest_experimental_enabled, GenAIAttributes.GenAiOperationNameValues.EMBEDDINGS.value, ) span_name = _get_embeddings_span_name(span_attributes) with tracer.start_as_current_span( name=span_name, kind=SpanKind.CLIENT, attributes=span_attributes, end_on_exit=True, ) as span: start = default_timer() result = None error_type = None try: result = wrapped(*args, **kwargs) if span.is_recording(): _set_embeddings_response_attributes(span, result) return result except Exception as error: error_type = type(error).__qualname__ handle_span_exception(span, error) raise finally: duration = max((default_timer() - start), 0) _record_metrics( instruments, duration, result, span_attributes, error_type, GenAIAttributes.GenAiOperationNameValues.EMBEDDINGS.value, ) return traced_method def async_embeddings_create( tracer: Tracer, instruments: Instruments, latest_experimental_enabled: bool, ): """Wrap the `create` method of the `AsyncEmbeddings` class to trace it.""" async def traced_method(wrapped, instance, args, kwargs): span_attributes = get_llm_request_attributes( kwargs, instance, latest_experimental_enabled, GenAIAttributes.GenAiOperationNameValues.EMBEDDINGS.value, ) span_name = _get_embeddings_span_name(span_attributes) with tracer.start_as_current_span( name=span_name, kind=SpanKind.CLIENT, attributes=span_attributes, end_on_exit=True, ) as span: start = default_timer() result = None error_type = None try: result = await wrapped(*args, **kwargs) if span.is_recording(): _set_embeddings_response_attributes(span, result) return result except Exception as error: error_type = type(error).__qualname__ handle_span_exception(span, error) raise finally: duration = max((default_timer() - start), 0) _record_metrics( instruments, duration, result, span_attributes, error_type, GenAIAttributes.GenAiOperationNameValues.EMBEDDINGS.value, ) return traced_method def _get_embeddings_span_name(span_attributes): """Get span name for embeddings operations.""" operation_name = span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME] model = span_attributes.get(GenAIAttributes.GEN_AI_REQUEST_MODEL) return f"{operation_name} {model}" if model else operation_name def _record_metrics( instruments: Instruments, duration: float, result, request_attributes: dict, error_type: str | None, operation_name: str, ): common_attributes = { GenAIAttributes.GEN_AI_OPERATION_NAME: operation_name, GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.OPENAI.value, GenAIAttributes.GEN_AI_REQUEST_MODEL: request_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL], } if "gen_ai.embeddings.dimension.count" in request_attributes: common_attributes["gen_ai.embeddings.dimension.count"] = request_attributes["gen_ai.embeddings.dimension.count"] if error_type: common_attributes["error.type"] = error_type if result and getattr(result, "model", None): common_attributes[GenAIAttributes.GEN_AI_RESPONSE_MODEL] = result.model if result and getattr(result, "service_tier", None): common_attributes[GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER] = result.service_tier if result and getattr(result, "system_fingerprint", None): common_attributes[GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SYSTEM_FINGERPRINT] = result.system_fingerprint if ServerAttributes.SERVER_ADDRESS in request_attributes: common_attributes[ServerAttributes.SERVER_ADDRESS] = request_attributes[ServerAttributes.SERVER_ADDRESS] if ServerAttributes.SERVER_PORT in request_attributes: common_attributes[ServerAttributes.SERVER_PORT] = request_attributes[ServerAttributes.SERVER_PORT] instruments.operation_duration_histogram.record( duration, attributes=common_attributes, ) if result and getattr(result, "usage", None): # Always record input tokens input_attributes = { **common_attributes, GenAIAttributes.GEN_AI_TOKEN_TYPE: GenAIAttributes.GenAiTokenTypeValues.INPUT.value, } instruments.token_usage_histogram.record( result.usage.prompt_tokens, attributes=input_attributes, ) # For embeddings, don't record output tokens as all tokens are input tokens if operation_name != GenAIAttributes.GenAiOperationNameValues.EMBEDDINGS.value: output_attributes = { **common_attributes, GenAIAttributes.GEN_AI_TOKEN_TYPE: GenAIAttributes.GenAiTokenTypeValues.COMPLETION.value, } instruments.token_usage_histogram.record(result.usage.completion_tokens, attributes=output_attributes) def _set_response_attributes(span, result): if getattr(result, "model", None): set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, result.model) if getattr(result, "choices", None): finish_reasons = [] for choice in result.choices: finish_reasons.append(choice.finish_reason or "error") set_span_attribute( span, GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons, ) if getattr(result, "id", None): set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_ID, result.id) if getattr(result, "service_tier", None): set_span_attribute( span, GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER, result.service_tier, ) # Get the usage if getattr(result, "usage", None): set_span_attribute( span, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, result.usage.prompt_tokens, ) set_span_attribute( span, GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, result.usage.completion_tokens, ) def _set_response_properties( chat_invocation: InferenceInvocation, result, capture_content: bool ) -> InferenceInvocation: if getattr(result, "model", None): chat_invocation.response_model_name = result.model if getattr(result, "choices", None): finish_reasons = [] for choice in result.choices: finish_reasons.append(choice.finish_reason or "error") chat_invocation.finish_reasons = finish_reasons if capture_content: # optimization chat_invocation.output_messages = _prepare_output_messages(result.choices) if getattr(result, "id", None): chat_invocation.response_id = result.id if getattr(result, "service_tier", None): chat_invocation.attributes.update( {OpenAIAttributes.OPENAI_RESPONSE_SERVICE_TIER: result.service_tier}, ) chat_invocation.metric_attributes.update( {OpenAIAttributes.OPENAI_RESPONSE_SERVICE_TIER: result.service_tier}, ) if getattr(result, "usage", None): chat_invocation.input_tokens = result.usage.prompt_tokens chat_invocation.output_tokens = result.usage.completion_tokens if getattr(result, "system_fingerprint", None): chat_invocation.attributes.update( {OpenAIAttributes.OPENAI_RESPONSE_SYSTEM_FINGERPRINT: result.system_fingerprint}, ) chat_invocation.metric_attributes.update( {OpenAIAttributes.OPENAI_RESPONSE_SYSTEM_FINGERPRINT: result.system_fingerprint}, ) return chat_invocation def _set_embeddings_response_attributes( span: Span, result: Any, ): set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, result.model) # Set embeddings dimensions if we can determine it from the response if getattr(result, "data", None) and len(result.data) > 0: first_embedding = result.data[0] if getattr(first_embedding, "embedding", None): set_span_attribute( span, "gen_ai.embeddings.dimension.count", len(first_embedding.embedding), ) # Get the usage if getattr(result, "usage", None): set_span_attribute( span, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, result.usage.prompt_tokens, ) # Don't set output tokens for embeddings as all tokens are input tokens class BaseStreamWrapper: response_id: str | None = None response_model: str | None = None service_tier: str | None = None finish_reasons: list = [] prompt_tokens: int | None = 0 completion_tokens: int | None = 0 def __init__( self, stream: Stream, capture_content: bool, ): self.stream = stream self.choice_buffers = [] self._started = False self.capture_content = capture_content self._setup() def _setup(self): if not self._started: self._started = True def cleanup(self, error: BaseException | None = None): pass def __enter__(self): self._setup() return self def __exit__(self, exc_type, exc_val, exc_tb): error = exc_val if exc_type else None self.cleanup(error) return False # Propagate the exception async def __aenter__(self): self._setup() return self async def __aexit__(self, exc_type, exc_val, exc_tb): error = exc_val if exc_type else None self.cleanup(error) return False # Propagate the exception def close(self): self.stream.close() self.cleanup() def __iter__(self): return self def __aiter__(self): return self def __next__(self): try: chunk = next(self.stream) self.process_chunk(chunk) return chunk except StopIteration: self.cleanup() raise except Exception as error: self.cleanup(error) raise async def __anext__(self): try: chunk = await self.stream.__anext__() self.process_chunk(chunk) return chunk except StopAsyncIteration: self.cleanup() raise except Exception as error: self.cleanup(error) raise def set_response_model(self, chunk): if self.response_model: return if getattr(chunk, "model", None): self.response_model = chunk.model def set_response_id(self, chunk): if self.response_id: return if getattr(chunk, "id", None): self.response_id = chunk.id def set_response_service_tier(self, chunk): if self.service_tier: return if getattr(chunk, "service_tier", None): self.service_tier = chunk.service_tier def build_streaming_response(self, chunk): if getattr(chunk, "choices", None) is None: return choices = chunk.choices for choice in choices: if not choice.delta: continue # make sure we have enough choice buffers for idx in range(len(self.choice_buffers), choice.index + 1): self.choice_buffers.append(ChoiceBuffer(idx)) if choice.finish_reason: self.choice_buffers[choice.index].finish_reason = choice.finish_reason if choice.delta.content is not None: self.choice_buffers[choice.index].append_text_content(choice.delta.content) if choice.delta.tool_calls is not None: for tool_call in choice.delta.tool_calls: self.choice_buffers[choice.index].append_tool_call(tool_call) def set_usage(self, chunk): if getattr(chunk, "usage", None): self.completion_tokens = chunk.usage.completion_tokens self.prompt_tokens = chunk.usage.prompt_tokens def process_chunk(self, chunk): self.set_response_id(chunk) self.set_response_model(chunk) self.set_response_service_tier(chunk) self.build_streaming_response(chunk) self.set_usage(chunk) def __getattr__(self, name): return getattr(self.stream, name) def parse(self): """Called when using with_raw_response with stream=True""" return self class LegacyChatStreamWrapper(BaseStreamWrapper): span: Span response_id: str | None = None response_model: str | None = None service_tier: str | None = None finish_reasons: list = [] prompt_tokens: int | None = 0 completion_tokens: int | None = 0 def __init__( self, stream: Stream, span: Span, logger: Logger, capture_content: bool, ): super().__init__(stream, capture_content=capture_content) self.span = span self.logger = logger def cleanup(self, error: BaseException | None = None): if not self._started: return if self.span.is_recording(): if self.response_model: set_span_attribute( self.span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, self.response_model, ) if self.response_id: set_span_attribute( self.span, GenAIAttributes.GEN_AI_RESPONSE_ID, self.response_id, ) set_span_attribute( self.span, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, self.prompt_tokens, ) set_span_attribute( self.span, GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, self.completion_tokens, ) if self.service_tier: set_span_attribute( self.span, GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER, self.service_tier, ) set_span_attribute( self.span, GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS, self.finish_reasons, ) for idx, choice in enumerate(self.choice_buffers): message = {"role": "assistant"} if self.capture_content and choice.text_content: message["content"] = "".join(choice.text_content) if choice.tool_calls_buffers: tool_calls = [] for tool_call in filter(None, choice.tool_calls_buffers): function = {"name": tool_call.function_name} if self.capture_content: function["arguments"] = "".join(tool_call.arguments) tool_call_dict = { "id": tool_call.tool_call_id, "type": "function", "function": function, } tool_calls.append(tool_call_dict) message["tool_calls"] = tool_calls body = { "index": idx, "finish_reason": choice.finish_reason or "error", "message": message, } event_attributes = {GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.OPENAI.value} context = set_span_in_context(self.span, get_current()) self.logger.emit( LogRecord( event_name="gen_ai.choice", attributes=event_attributes, body=body, context=context, ) ) if error: handle_span_exception(self.span, error) else: self.span.end() self._started = False class AsyncLegacyChatStreamWrapper(LegacyChatStreamWrapper): """Async variant of :class:`LegacyChatStreamWrapper`. Wraps an ``openai.AsyncStream`` whose ``close`` is a coroutine. ``close`` and ``__aexit__`` are therefore overridden as awaitables so the underlying ``AsyncStream.close`` is awaited, releasing the httpx response/connection instead of leaking it (and avoiding the ``coroutine ... was never awaited`` warning). """ stream: AsyncStream async def close(self) -> None: # Finalize the span even if the underlying close raises, and re-raise # the original exception unmodified. try: await self.stream.close() finally: self.cleanup() async def __aexit__(self, exc_type, exc_val, exc_tb): # The wrapper replaces the underlying AsyncStream as the async context # manager, so it must await AsyncStream.close on exit to release the # httpx response/connection instead of leaking it. error = exc_val if exc_type else None try: await self.stream.close() finally: self.cleanup(error) return False # Propagate the exception patch_responses.py000066400000000000000000000032071527070072300503310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from opentelemetry.util.genai.handler import TelemetryHandler from opentelemetry.util.genai.types import Error from .response_extractors import ( apply_request_attributes, extract_params, get_inference_creation_kwargs, set_invocation_response_attributes, ) from .response_wrappers import ResponseStreamWrapper from .utils import is_streaming def responses_create(handler: TelemetryHandler): """Wrap the `create` method of the `Responses` class to trace it.""" capture_content = handler.should_capture_content() def traced_method(wrapped, instance, args, kwargs): params = extract_params(**kwargs) invocation = handler.start_inference(**get_inference_creation_kwargs(params, instance)) apply_request_attributes(invocation, params, capture_content) try: result = wrapped(*args, **kwargs) parsed_result = _get_response_stream_result(result) if is_streaming(kwargs): return ResponseStreamWrapper( parsed_result, invocation, capture_content, ) set_invocation_response_attributes(invocation, parsed_result, capture_content) invocation.stop() return result except Exception as error: invocation.fail(Error(type=type(error), message=str(error))) raise return traced_method def _get_response_stream_result(result): if hasattr(result, "parse"): return result.parse() return result response_extractors.py000066400000000000000000000323161527070072300512500ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import json from collections.abc import Mapping, Sequence from dataclasses import dataclass from typing import TYPE_CHECKING from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, ) from opentelemetry.semconv._incubating.attributes import ( openai_attributes as OpenAIAttributes, ) from .utils import ( _openai_response_format_to_output_type, get_server_address_and_port, ) if TYPE_CHECKING: from openai.types.responses.response import Response from openai.types.responses.response_usage import ResponseUsage from opentelemetry.util.genai.types import ( InputMessage, OutputMessage, Text, ) try: from openai.types.responses.response import Response from openai.types.responses.response_function_tool_call import ( ResponseFunctionToolCall, ) from openai.types.responses.response_output_message import ( ResponseOutputMessage, ) from openai.types.responses.response_output_refusal import ( ResponseOutputRefusal, ) from openai.types.responses.response_output_text import ResponseOutputText from openai.types.responses.response_reasoning_item import ( ResponseReasoningItem, ) from openai.types.responses.response_usage import ResponseUsage except ImportError: Response = None ResponseFunctionToolCall = None ResponseOutputMessage = None ResponseOutputRefusal = None ResponseOutputText = None ResponseReasoningItem = None ResponseUsage = None try: from opentelemetry.util.genai.types import ( InputMessage, OutputMessage, Reasoning, Text, ) from opentelemetry.util.genai.types import ( ToolCallRequest as ToolCall, ) except ImportError: InputMessage = None OutputMessage = None Reasoning = None Text = None ToolCall = None @dataclass class ResponseRequestParams: model: str | None = None instructions: str | None = None input: str | Sequence[object] | None = None max_output_tokens: int | None = None service_tier: str | None = None temperature: float | None = None output_type: str | None = None top_p: float | None = None @dataclass class UsageTokens: input_tokens: int | None = None output_tokens: int | None = None cache_creation_input_tokens: int | None = None cache_read_input_tokens: int | None = None def _get_field(value: object, field_name: str) -> object | None: if isinstance(value, Mapping): return value.get(field_name) return getattr(value, field_name, None) def _get_sequence(value: object) -> Sequence[object]: if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): return value return () def _get_int(value: object) -> int | None: if isinstance(value, bool) or not isinstance(value, int): return None return value def _get_float(value: object) -> float | None: if isinstance(value, bool) or not isinstance(value, (int, float)): return None return float(value) def _extract_output_type_from_value(text_config: object) -> str | None: format_config = _get_field(text_config, "format") if format_config is None: return None format_type = _get_field(format_config, "type") if isinstance(format_type, str): return _openai_response_format_to_output_type(format_type) return None def extract_params( *, model: str | None = None, instructions: str | None = None, input_items: str | Sequence[object] | None = None, max_output_tokens: int | None = None, service_tier: str | None = None, temperature: float | None = None, text: object | None = None, top_p: float | None = None, **_kwargs: object, ) -> ResponseRequestParams: if input_items is None and "input" in _kwargs: input_items = _kwargs["input"] return ResponseRequestParams( model=model if isinstance(model, str) else None, instructions=instructions if isinstance(instructions, str) else None, input=( input_items if isinstance(input_items, str) or (isinstance(input_items, Sequence) and not isinstance(input_items, (str, bytes, bytearray))) else None ), max_output_tokens=_get_int(max_output_tokens), service_tier=(service_tier if isinstance(service_tier, str) and service_tier != "auto" else None), temperature=_get_float(temperature), output_type=_extract_output_type_from_value(text), top_p=_get_float(top_p), ) def get_system_instruction(instructions: str | None) -> list[Text]: if Text is None or instructions is None: return [] return [Text(content=instructions)] def get_input_messages( input_value: str | Sequence[object] | None, ) -> list[InputMessage]: if InputMessage is None or Text is None: return [] if isinstance(input_value, str): return [InputMessage(role="user", parts=[Text(content=input_value)])] messages: list[InputMessage] = [] for item in _get_sequence(input_value): role = _get_field(item, "role") if not isinstance(role, str): continue content = _get_field(item, "content") if isinstance(content, str): messages.append(InputMessage(role=role, parts=[Text(content=content)])) continue parts = [] for part in _get_sequence(content): text = _get_field(part, "text") if isinstance(text, str): parts.append(Text(content=text)) if parts: messages.append(InputMessage(role=role, parts=parts)) return messages def _extract_output_parts(content_blocks: Sequence[object]) -> list[Text]: if Text is None or ResponseOutputText is None or ResponseOutputRefusal is None: return [] parts: list[Text] = [] for block in content_blocks: if isinstance(block, ResponseOutputText): parts.append(Text(content=block.text)) elif isinstance(block, ResponseOutputRefusal): parts.append(Text(content=block.refusal)) return parts def _parse_tool_call_arguments(arguments: str | None) -> object: if arguments is None: return None try: return json.loads(arguments) except (TypeError, ValueError): return arguments def _extract_reasoning_parts( item: ResponseReasoningItem, ) -> list[Reasoning]: if Reasoning is None: return [] parts: list[Reasoning] = [] for block in item.summary: if isinstance(block.text, str): parts.append(Reasoning(content=block.text)) for block in item.content or []: if getattr(block, "type", None) == "reasoning_text" and isinstance(getattr(block, "text", None), str): parts.append(Reasoning(content=block.text)) return parts def _finish_reason_from_status(status: str | None) -> str | None: if status == "completed": return "stop" if status in {"failed", "cancelled", "incomplete"}: return status return None def _response_types_available() -> bool: return ( Response is not None and ResponseOutputMessage is not None and ResponseFunctionToolCall is not None and ResponseReasoningItem is not None ) def get_output_messages_from_response( response: Response | None, ) -> list[OutputMessage]: if not _response_types_available() or not isinstance(response, Response) or OutputMessage is None or Text is None: return [] messages: list[OutputMessage] = [] for item in response.output: if isinstance(item, ResponseOutputMessage): finish_reason = _finish_reason_from_status(item.status) if finish_reason is None: continue messages.append( OutputMessage( role=item.role, parts=_extract_output_parts(item.content), finish_reason=finish_reason, ) ) continue if isinstance(item, ResponseFunctionToolCall): if ToolCall is None or item.status not in { "completed", "incomplete", }: continue messages.append( OutputMessage( role="assistant", parts=[ ToolCall( id=item.call_id if item.call_id else item.id, name=item.name, arguments=_parse_tool_call_arguments(item.arguments), ) ], finish_reason="tool_calls", ) ) continue if isinstance(item, ResponseReasoningItem): finish_reason = _finish_reason_from_status(item.status) if finish_reason is None: continue parts = _extract_reasoning_parts(item) if parts: messages.append( OutputMessage( role="assistant", parts=parts, finish_reason=finish_reason, ) ) return messages def extract_finish_reasons(response: Response | None) -> list[str]: if ( Response is None or ResponseOutputMessage is None or ResponseFunctionToolCall is None or not isinstance(response, Response) ): return [] finish_reasons: list[str] = [] for item in response.output: if isinstance(item, ResponseFunctionToolCall) and item.status in { "completed", "incomplete", }: finish_reasons.append("tool_calls") continue if not isinstance(item, ResponseOutputMessage): continue finish_reason = _finish_reason_from_status(item.status) if finish_reason is not None: finish_reasons.append(finish_reason) return list(dict.fromkeys(finish_reasons)) def get_inference_creation_kwargs( params: ResponseRequestParams, client_instance: object, ) -> dict[str, object]: address, port = get_server_address_and_port(client_instance) creation_kwargs: dict[str, object] = { "provider": GenAIAttributes.GenAiProviderNameValues.OPENAI.value, } if params.model is not None: creation_kwargs["request_model"] = params.model if address is not None: creation_kwargs["server_address"] = address if port is not None: creation_kwargs["server_port"] = port return creation_kwargs def apply_request_attributes( invocation, params: ResponseRequestParams, capture_content: bool, ) -> None: invocation.temperature = params.temperature invocation.top_p = params.top_p invocation.max_tokens = params.max_output_tokens if params.service_tier is not None: invocation.attributes[OpenAIAttributes.OPENAI_REQUEST_SERVICE_TIER] = params.service_tier if params.output_type is not None: invocation.attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] = params.output_type if capture_content: invocation.system_instruction = get_system_instruction(params.instructions) invocation.input_messages = get_input_messages(params.input) def extract_usage_tokens(usage: ResponseUsage | None) -> UsageTokens: if ResponseUsage is None or usage is None or not isinstance(usage, ResponseUsage): return UsageTokens() details = usage.input_tokens_details return UsageTokens( input_tokens=usage.input_tokens, output_tokens=usage.output_tokens, # `cache_creation_input_tokens` is not present on every SDK version's # input token details model, so keep this attribute access guarded for # compatibility across the supported OpenAI range. cache_creation_input_tokens=( details.cache_creation_input_tokens if details is not None and hasattr(details, "cache_creation_input_tokens") else None ), cache_read_input_tokens=(details.cached_tokens if details is not None else None), ) def set_invocation_response_attributes( invocation, response: Response | None, capture_content: bool, ) -> None: if Response is None or not isinstance(response, Response): return invocation.response_model_name = response.model invocation.response_id = response.id if response.service_tier is not None: invocation.attributes[OpenAIAttributes.OPENAI_RESPONSE_SERVICE_TIER] = response.service_tier tokens = extract_usage_tokens(response.usage) invocation.input_tokens = tokens.input_tokens invocation.output_tokens = tokens.output_tokens invocation.cache_creation_input_tokens = tokens.cache_creation_input_tokens invocation.cache_read_input_tokens = tokens.cache_read_input_tokens finish_reasons = extract_finish_reasons(response) if finish_reasons: invocation.finish_reasons = finish_reasons if capture_content: output_messages = get_output_messages_from_response(response) if output_messages: invocation.output_messages = output_messages response_wrappers.py000066400000000000000000000321511527070072300507120ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Wrappers for OpenAI Responses API streams and stream managers.""" from __future__ import annotations import logging from collections.abc import Callable, Generator from contextlib import AsyncExitStack, ExitStack, contextmanager from types import TracebackType from typing import TYPE_CHECKING, Generic, TypeVar from opentelemetry.util.genai.types import Error try: from opentelemetry.instrumentation.openai_v2.response_extractors import ( # pylint: disable=no-name-in-module set_invocation_response_attributes, ) except ImportError: set_invocation_response_attributes = None if TYPE_CHECKING: from openai.lib.streaming.responses._events import ( # pylint: disable=no-name-in-module ResponseStreamEvent, ) from openai.lib.streaming.responses._responses import ( AsyncResponseStream, AsyncResponseStreamManager, ResponseStream, ResponseStreamManager, ) # pylint: disable=no-name-in-module from openai.types.responses import ( # pylint: disable=no-name-in-module ParsedResponse, Response, ) from opentelemetry.util.genai._invocation import GenAIInvocation _logger = logging.getLogger(__name__) TextFormatT = TypeVar("TextFormatT") ResponseT = TypeVar("ResponseT") def _set_response_attributes( invocation: GenAIInvocation, result: ParsedResponse[TextFormatT] | Response | None, capture_content: bool, ) -> None: if set_invocation_response_attributes is None: return set_invocation_response_attributes(invocation, result, capture_content) def _get_stream_response(stream): try: return stream._response except AttributeError: try: return stream.response except AttributeError: return None class _ResponseProxy(Generic[ResponseT]): def __init__(self, response: ResponseT, finalize: Callable[[], None]): self._response = response self._finalize = finalize def close(self) -> None: try: self._response.close() finally: self._finalize() def __getattr__(self, name: str): return getattr(self._response, name) class _AsyncResponseProxy(Generic[ResponseT]): def __init__(self, response: ResponseT, finalize: Callable[[], None]): self._response = response self._finalize = finalize async def aclose(self) -> None: try: await self._response.aclose() finally: self._finalize() def __getattr__(self, name: str): return getattr(self._response, name) class ResponseStreamWrapper(Generic[TextFormatT]): """Wrapper for OpenAI Responses API stream objects. Wraps ResponseStream from the OpenAI SDK: https://github.com/openai/openai-python/blob/656e3cab4a18262a49b961d41293367e45ee71b9/src/openai/_streaming.py#L55 """ def __init__( self, stream: ResponseStream[TextFormatT], invocation: GenAIInvocation, capture_content: bool, ): self.stream = stream self.invocation = invocation self._capture_content = capture_content self._finalized = False def __enter__(self) -> ResponseStreamWrapper: return self def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool: try: if exc_type is not None: self._fail(str(exc_val), type(exc_val) if exc_val else Exception) finally: self.close() return False def close(self) -> None: try: self.stream.close() finally: self._stop(None) def __iter__(self) -> ResponseStreamWrapper: return self def __next__(self) -> ResponseStreamEvent[TextFormatT]: try: event = next(self.stream) except StopIteration: self._stop(None) raise except Exception as error: self._fail(str(error), type(error)) raise with self._safe_instrumentation("event processing"): self.process_event(event) return event def get_final_response(self) -> ParsedResponse[TextFormatT]: self.until_done() return self.stream.get_final_response() def until_done(self) -> ResponseStreamWrapper: for _ in self: pass return self def parse(self) -> ResponseStreamWrapper: """Called when using with_raw_response with stream=True.""" return self # TODO: Replace __getattr__ passthrough with wrapt.ObjectProxy in a future # cleanup once wrapt 2 typing support is available (wrapt PR #3903). def __getattr__(self, name: str): return getattr(self.stream, name) @property def response(self): response = _get_stream_response(self.stream) if response is None: return None return _ResponseProxy(response, lambda: self._stop(None)) def _stop(self, result: ParsedResponse[TextFormatT] | Response | None) -> None: if self._finalized: return with self._safe_instrumentation("response attribute extraction"): _set_response_attributes(self.invocation, result, self._capture_content) with self._safe_instrumentation("inference.stop"): self.invocation.stop() self._finalized = True def _fail(self, message: str, error_type: type[BaseException]) -> None: if self._finalized: return with self._safe_instrumentation("inference.fail"): self.invocation.fail(Error(message=message, type=error_type)) self._finalized = True @staticmethod @contextmanager def _safe_instrumentation(context: str) -> Generator[None, None, None]: try: yield except Exception: # pylint: disable=broad-exception-caught _logger.debug( "OpenAI responses instrumentation error during %s", context, exc_info=True, stacklevel=2, ) def process_event(self, event: ResponseStreamEvent[TextFormatT]) -> None: event_type = event.type response: ParsedResponse[TextFormatT] | Response | None = getattr(event, "response", None) if response and not self.invocation.request_model: model = response.model if model: self.invocation.request_model = model if event_type == "response.completed": self._stop(response) return if event_type in {"response.failed", "response.incomplete"}: with self._safe_instrumentation("response attribute extraction"): _set_response_attributes(self.invocation, response, self._capture_content) self._fail(event_type, RuntimeError) return if event_type == "response.error": error_type = getattr(event, "code", None) or "response.error" message = getattr(event, "message", None) or error_type self._fail(message, RuntimeError) class ResponseStreamManagerWrapper(Generic[TextFormatT]): """Wrapper for OpenAI Responses API stream managers. Wraps ResponseStreamManager from the OpenAI SDK: https://github.com/openai/openai-python/blob/656e3cab4a18262a49b961d41293367e45ee71b9/src/openai/lib/streaming/responses/_responses.py#L95 """ def __init__( self, manager: ResponseStreamManager[TextFormatT], invocation, capture_content: bool, ): self._manager = manager self._invocation = invocation self._capture_content = capture_content self._stream_wrapper: ResponseStreamWrapper[TextFormatT] | None = None def __enter__(self) -> ResponseStreamWrapper[TextFormatT]: stream = self._manager.__enter__() self._stream_wrapper = ResponseStreamWrapper( stream, self._invocation, self._capture_content, ) return self._stream_wrapper def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool: suppressed = False stream_wrapper = self._stream_wrapper self._stream_wrapper = None with ExitStack() as cleanup: if stream_wrapper is not None: def finalize_stream_wrapper() -> None: if suppressed: stream_wrapper.__exit__(None, None, None) else: stream_wrapper.__exit__(exc_type, exc_val, exc_tb) cleanup.callback(finalize_stream_wrapper) suppressed = self._manager.__exit__(exc_type, exc_val, exc_tb) return suppressed def parse(self) -> ResponseStreamManagerWrapper[TextFormatT]: raise NotImplementedError("ResponseStreamManagerWrapper.parse() is not implemented") # TODO: Replace __getattr__ passthrough with wrapt.ObjectProxy in a future # cleanup once wrapt 2 typing support is available (wrapt PR #3903). def __getattr__(self, name: str): return getattr(self._manager, name) class AsyncResponseStreamWrapper(ResponseStreamWrapper[TextFormatT]): """Wrapper for async OpenAI Responses API stream objects.""" stream: AsyncResponseStream[TextFormatT] async def __aenter__(self) -> AsyncResponseStreamWrapper[TextFormatT]: return self async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool: try: if exc_type is not None: self._fail(str(exc_val), type(exc_val) if exc_val else Exception) finally: await self.close() return False async def close(self) -> None: try: await self.stream.close() finally: self._stop(None) def __aiter__(self) -> AsyncResponseStreamWrapper[TextFormatT]: return self async def __anext__(self) -> ResponseStreamEvent[TextFormatT]: try: event = await self.stream.__anext__() except StopAsyncIteration: self._stop(None) raise except Exception as error: self._fail(str(error), type(error)) raise with self._safe_instrumentation("event processing"): self.process_event(event) return event async def get_final_response(self) -> ParsedResponse[TextFormatT]: await self.until_done() return await self.stream.get_final_response() async def until_done(self) -> AsyncResponseStreamWrapper[TextFormatT]: async for _ in self: pass return self def parse(self) -> AsyncResponseStreamWrapper[TextFormatT]: """Called when using with_raw_response with stream=True.""" return self @property def response(self): response = _get_stream_response(self.stream) if response is None: return None return _AsyncResponseProxy(response, lambda: self._stop(None)) class AsyncResponseStreamManagerWrapper(Generic[TextFormatT]): """Wrapper for async OpenAI Responses API stream managers.""" def __init__( self, manager: AsyncResponseStreamManager[TextFormatT], invocation, capture_content: bool, ): self._manager = manager self._invocation = invocation self._capture_content = capture_content self._stream_wrapper: AsyncResponseStreamWrapper[TextFormatT] | None = None async def __aenter__(self) -> AsyncResponseStreamWrapper[TextFormatT]: stream = await self._manager.__aenter__() self._stream_wrapper = AsyncResponseStreamWrapper( stream, self._invocation, self._capture_content, ) return self._stream_wrapper async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool: suppressed = False stream_wrapper = self._stream_wrapper self._stream_wrapper = None async with AsyncExitStack() as cleanup: if stream_wrapper is not None: async def finalize_stream_wrapper() -> None: if suppressed: await stream_wrapper.__aexit__(None, None, None) else: await stream_wrapper.__aexit__(exc_type, exc_val, exc_tb) cleanup.push_async_callback(finalize_stream_wrapper) suppressed = await self._manager.__aexit__(exc_type, exc_val, exc_tb) return suppressed def parse(self) -> AsyncResponseStreamManagerWrapper[TextFormatT]: raise NotImplementedError("AsyncResponseStreamManagerWrapper.parse() is not implemented") # TODO: Replace __getattr__ passthrough with wrapt.ObjectProxy in a future # cleanup once wrapt 2 typing support is available (wrapt PR #3903). def __getattr__(self, name: str): return getattr(self._manager, name) utils.py000066400000000000000000000406661527070072300463030ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import json from collections.abc import Iterable, Mapping from os import environ from typing import Any from urllib.parse import urlparse import openai from httpx import URL from openai import NotGiven from opentelemetry._logs import LogRecord from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, ) from opentelemetry.semconv._incubating.attributes import ( openai_attributes as OpenAIAttributes, ) from opentelemetry.semconv._incubating.attributes import ( server_attributes as ServerAttributes, ) from opentelemetry.semconv.attributes import ( error_attributes as ErrorAttributes, ) from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util.genai.environment_variables import ( OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, ) from opentelemetry.util.genai.handler import TelemetryHandler from opentelemetry.util.genai.invocation import InferenceInvocation from opentelemetry.util.genai.types import ( FunctionToolDefinition, InputMessage, OutputMessage, Text, ToolCallRequest, ToolCallResponse, ToolDefinition, ) _OpenAIOmit = getattr(openai, "Omit", None) def is_content_enabled() -> bool: capture_content = environ.get(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "false") return capture_content.lower() == "true" def extract_tool_calls(item, capture_content): tool_calls = get_property_value(item, "tool_calls") if tool_calls is None: return None calls = [] for tool_call in tool_calls: tool_call_dict = {} call_id = get_property_value(tool_call, "id") if call_id: tool_call_dict["id"] = call_id tool_type = get_property_value(tool_call, "type") if tool_type: tool_call_dict["type"] = tool_type func = get_property_value(tool_call, "function") if func: tool_call_dict["function"] = {} name = get_property_value(func, "name") if name: tool_call_dict["function"]["name"] = name arguments = get_property_value(func, "arguments") if capture_content and arguments: if isinstance(arguments, str): arguments = arguments.replace("\n", "") tool_call_dict["function"]["arguments"] = arguments calls.append(tool_call_dict) return calls def get_server_address_and_port( client_instance, ) -> tuple[str | None, int | None]: base_client = getattr(client_instance, "_client", None) base_url = getattr(base_client, "base_url", None) if not base_url: return None, None address = None port = None if isinstance(base_url, URL): address = base_url.host port = base_url.port elif isinstance(base_url, str): url = urlparse(base_url) address = url.hostname port = url.port if port == 443: port = None return address, port def get_property_value(obj, property_name): if isinstance(obj, dict): return obj.get(property_name, None) return getattr(obj, property_name, None) def message_to_event(message, capture_content): attributes = {GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.OPENAI.value} role = get_property_value(message, "role") content = get_property_value(message, "content") body = {} if capture_content and content: body["content"] = content if role == "assistant": tool_calls = extract_tool_calls(message, capture_content) if tool_calls: body = {"tool_calls": tool_calls} elif role == "tool": tool_call_id = get_property_value(message, "tool_call_id") if tool_call_id: body["id"] = tool_call_id return LogRecord( event_name=f"gen_ai.{role}.message", attributes=attributes, body=body if body else None, ) def choice_to_event(choice, capture_content): attributes = {GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.OPENAI.value} body = { "index": choice.index, "finish_reason": choice.finish_reason or "error", } if choice.message: message = {"role": (choice.message.role if choice.message and choice.message.role else None)} tool_calls = extract_tool_calls(choice.message, capture_content) if tool_calls: message["tool_calls"] = tool_calls content = get_property_value(choice.message, "content") if capture_content and content: message["content"] = content body["message"] = message return LogRecord( event_name="gen_ai.choice", attributes=attributes, body=body, ) def set_span_attributes(span, attributes: dict): for field, value in attributes.model_dump(by_alias=True).items(): set_span_attribute(span, field, value) def set_span_attribute(span, name, value): if non_numerical_value_is_set(value) is False: return span.set_attribute(name, value) def is_streaming(kwargs): return non_numerical_value_is_set(kwargs.get("stream")) def non_numerical_value_is_set(value: bool | str | NotGiven | None): return bool(value) and value_is_set(value) def value_is_set(value): if _OpenAIOmit is not None and isinstance(value, _OpenAIOmit): return False return value is not None and not isinstance(value, NotGiven) def _openai_response_format_to_output_type(response_format_type: str) -> str: if response_format_type in ("json_object", "json_schema"): return GenAIAttributes.GenAiOutputTypeValues.JSON.value return response_format_type def get_llm_request_attributes( kwargs, client_instance, latest_experimental_enabled, operation_name=GenAIAttributes.GenAiOperationNameValues.CHAT.value, ): # pylint: disable=too-many-branches,too-many-locals attributes = { GenAIAttributes.GEN_AI_OPERATION_NAME: operation_name, } model = kwargs.get("model") if model: attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] = model if latest_experimental_enabled: attributes.update( { GenAIAttributes.GEN_AI_PROVIDER_NAME: (GenAIAttributes.GenAiProviderNameValues.OPENAI.value), } ) else: attributes.update( { GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiProviderNameValues.OPENAI.value, } ) # Add chat-specific attributes only for chat operations if operation_name == GenAIAttributes.GenAiOperationNameValues.CHAT.value: attributes.update( { GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE: kwargs.get("temperature"), GenAIAttributes.GEN_AI_REQUEST_TOP_P: kwargs.get("p") or kwargs.get("top_p"), GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS: kwargs.get("max_tokens"), GenAIAttributes.GEN_AI_REQUEST_PRESENCE_PENALTY: kwargs.get("presence_penalty"), GenAIAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY: kwargs.get("frequency_penalty"), GenAIAttributes.GEN_AI_REQUEST_SEED: kwargs.get("seed"), } ) if (choice_count := kwargs.get("n")) is not None: # Only add non default, meaningful values if isinstance(choice_count, int) and choice_count != 1: attributes[GenAIAttributes.GEN_AI_REQUEST_CHOICE_COUNT] = choice_count if (stop_sequences := kwargs.get("stop")) is not None: if isinstance(stop_sequences, str): stop_sequences = [stop_sequences] attributes[GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES] = stop_sequences request_response_format_attr_key = ( GenAIAttributes.GEN_AI_OUTPUT_TYPE if latest_experimental_enabled else GenAIAttributes.GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT ) if (response_format := kwargs.get("response_format")) is not None: # response_format may be string or object with a string in the `type` key if isinstance(response_format, Mapping): if (response_format_type := response_format.get("type")) is not None: attributes[request_response_format_attr_key] = response_format_type elif isinstance(response_format, str): attributes[request_response_format_attr_key] = response_format # service_tier can be passed directly or in extra_body (in SDK 1.26.0 it's via extra_body) service_tier = kwargs.get("service_tier") if service_tier is None: extra_body = kwargs.get("extra_body") if isinstance(extra_body, Mapping): service_tier = extra_body.get("service_tier") request_service_tier_attr_key = ( OpenAIAttributes.OPENAI_REQUEST_SERVICE_TIER if latest_experimental_enabled else GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER ) attributes[request_service_tier_attr_key] = service_tier if service_tier != "auto" else None # Add embeddings-specific attributes elif operation_name == GenAIAttributes.GenAiOperationNameValues.EMBEDDINGS.value: # Add embedding dimensions if specified if (dimensions := kwargs.get("dimensions")) is not None: # TODO: move to GEN_AI_EMBEDDINGS_DIMENSION_COUNT when 1.39.0 is baseline attributes["gen_ai.embeddings.dimension.count"] = dimensions # Add encoding format if specified if "encoding_format" in kwargs: attributes[GenAIAttributes.GEN_AI_REQUEST_ENCODING_FORMATS] = [kwargs["encoding_format"]] address, port = get_server_address_and_port(client_instance) if address: attributes[ServerAttributes.SERVER_ADDRESS] = address if port: attributes[ServerAttributes.SERVER_PORT] = port # filter out values not set return {k: v for k, v in attributes.items() if value_is_set(v)} def create_chat_invocation( handler: TelemetryHandler, kwargs, client_instance, capture_content: bool, ) -> InferenceInvocation: # pylint: disable=too-many-branches address, port = get_server_address_and_port(client_instance) invocation = handler.start_inference( GenAIAttributes.GenAiProviderNameValues.OPENAI.value, request_model=kwargs.get("model", ""), server_address=address if address else None, server_port=port if port else None, ) invocation.temperature = get_value(kwargs.get("temperature")) invocation.top_p = get_value(kwargs.get("p") or kwargs.get("top_p")) invocation.max_tokens = get_value(kwargs.get("max_tokens")) invocation.presence_penalty = get_value(kwargs.get("presence_penalty")) invocation.frequency_penalty = get_value(kwargs.get("frequency_penalty")) invocation.seed = get_value(kwargs.get("seed")) if (stop_sequences := get_value(kwargs.get("stop"))) is not None: if isinstance(stop_sequences, str): stop_sequences = [stop_sequences] invocation.stop_sequences = stop_sequences if (choice_count := get_value(kwargs.get("n"))) is not None: # Only add non default, meaningful values if isinstance(choice_count, int) and choice_count != 1: invocation.attributes[GenAIAttributes.GEN_AI_REQUEST_CHOICE_COUNT] = choice_count if (response_format := get_value(kwargs.get("response_format"))) is not None: # response_format may be string or object with a string in the `type` key if isinstance(response_format, Mapping): if (response_format_type := get_value(response_format.get("type"))) is not None: invocation.attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] = _openai_response_format_to_output_type( response_format_type ) elif isinstance(response_format, str): invocation.attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] = _openai_response_format_to_output_type( response_format ) # service_tier can be passed directly or in extra_body (in SDK 1.26.0 it's via extra_body) service_tier = get_value(kwargs.get("service_tier")) if service_tier is None: extra_body = get_value(kwargs.get("extra_body")) if isinstance(extra_body, Mapping): service_tier = get_value(extra_body.get("service_tier")) if service_tier is not None and service_tier != "auto": invocation.attributes[OpenAIAttributes.OPENAI_REQUEST_SERVICE_TIER] = service_tier if capture_content: # optimization invocation.input_messages = _prepare_input_messages(kwargs.get("messages", [])) invocation.tool_definitions = _prepare_tool_definitions(kwargs.get("tools")) return invocation def get_value(v: Any): if value_is_set(v): return v return None def handle_span_exception(span, error: BaseException): span.set_status(Status(StatusCode.ERROR, str(error))) if span.is_recording(): span.set_attribute(ErrorAttributes.ERROR_TYPE, type(error).__qualname__) span.end() def _is_text_part(content: Any) -> bool: return isinstance(content, str) or ( isinstance(content, Iterable) and all(isinstance(part, str) for part in content) ) def _prepare_input_messages(messages) -> list[InputMessage]: chat_messages = [] for message in messages: role = get_property_value(message, "role") chat_message = InputMessage(role=str(role), parts=[]) chat_messages.append(chat_message) content = get_property_value(message, "content") if role == "assistant": tool_calls = get_property_value(message, "tool_calls") if tool_calls: chat_message.parts += extract_tool_calls_new(tool_calls) if _is_text_part(content): chat_message.parts.append(Text(content=str(content))) elif role == "tool": tool_call_id = get_property_value(message, "tool_call_id") chat_message.parts.append(ToolCallResponse(id=tool_call_id, response=content)) else: # system, developer, user, fallback if _is_text_part(content): chat_message.parts.append(Text(content=str(content))) return chat_messages def extract_tool_calls_new(tool_calls) -> list[ToolCallRequest]: parts = [] for tool_call in tool_calls: call_id = get_property_value(tool_call, "id") func_name = "" arguments = None func = get_property_value(tool_call, "function") if func: func_name = get_property_value(func, "name") or "" arguments_str = get_property_value(func, "arguments") if arguments_str: try: arguments = json.loads(arguments_str) except json.JSONDecodeError: arguments = arguments_str # TODO: support custom parts.append(ToolCallRequest(id=call_id, name=func_name, arguments=arguments)) return parts def _prepare_tool_definitions(tools) -> list[ToolDefinition] | None: if not tools: return None definitions: list[ToolDefinition] = [] for tool in tools: tool_type = get_property_value(tool, "type") if tool_type == "function": func = get_property_value(tool, "function") if func: definitions.append( FunctionToolDefinition( name=get_property_value(func, "name") or "", description=get_property_value(func, "description"), parameters=get_property_value(func, "parameters"), ) ) return definitions def _prepare_output_messages(choices) -> list[OutputMessage]: output_messages = [] for choice in choices: if choice.message: parts = [] tool_calls = get_property_value(choice.message, "tool_calls") if tool_calls: parts += extract_tool_calls_new(tool_calls) content = get_property_value(choice.message, "content") if _is_text_part(content): parts.append(Text(content=str(content))) message = OutputMessage( finish_reason=choice.finish_reason or "error", role=(choice.message.role if choice.message and choice.message.role else ""), parts=parts, ) output_messages.append(message) return output_messages version.py000066400000000000000000000001471527070072300466160ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "2.5b0.dev" tests/000077500000000000000000000000001527070072300351075ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2README.md000066400000000000000000000005301527070072300363640ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests## Recording calls If you need to record calls you need to export the `OPENAI_API_KEY` as environment variable. Since tox blocks environment variables by default you need to override its configuration to let them pass: ``` export TOX_OVERRIDE=testenv.pass_env=OPENAI_API_KEY ``` We are not adding it to tox.ini because of security concerns. __init__.py000066400000000000000000000000001527070072300372060ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/testscassettes/000077500000000000000000000000001527070072300371055ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/teststest_async_chat_completion_404.yaml000066400000000000000000000037631527070072300457750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "this-model-does-not-exist" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '103' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "error": { "message": "The model `this-model-does-not-exist` does not exist or you do not have access to it.", "type": "invalid_request_error", "param": null, "code": "model_not_found" } } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1a80827a861852-MRS Connection: - keep-alive Content-Type: - application/json; charset=utf-8 Date: - Wed, 13 Nov 2024 00:04:01 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff alt-svc: - h3=":443"; ma=86400 content-length: - '231' openai-organization: test_openai_org_id strict-transport-security: - max-age=31536000; includeSubDomains; preload vary: - Origin x-request-id: - req_5cf06a7fabd45ebe21ee38c14c5b2f76 status: code: 404 message: Not Found version: 1 test_async_chat_completion_extra_params.yaml000066400000000000000000000065621527070072300501540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4o-mini", "max_tokens": 50, "response_format": { "type": "text" }, "seed": 42, "stream": false, "temperature": 0.5, "service_tier": "default" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '220' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.7 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-AbMH3rR6OBMN9hG5w0TRrezuiHLMr", "object": "chat.completion", "created": 1733467121, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "This is a test. How can I assist you further?", "refusal": null }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 12, "total_tokens": 24, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "service_tier": "default", "system_fingerprint": "fp_bba3c8e70b" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8eda4640ead3e535-KUL Connection: - keep-alive Content-Type: - application/json Date: - Fri, 06 Dec 2024 06:38:42 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '825' openai-organization: test_openai_org_id openai-processing-ms: - '835' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - '199943' x-ratelimit-reset-requests: - 8.64s x-ratelimit-reset-tokens: - 16ms x-request-id: - req_fea877c0a861ff92a6a5217247681f24 status: code: 200 message: OK version: 1 test_async_chat_completion_metrics.yaml000066400000000000000000000063071527070072300471310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4o-mini", "stream": false } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '106' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASv9R2E7Yhb2e7bj4Xl0qm9s3J42Y", "object": "chat.completion", "created": 1731456237, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "This is a test. How can I assist you further?", "refusal": null }, "logprobs": null, "finish_reason": "stop" } ], "service_tier": "default", "usage": { "prompt_tokens": 12, "completion_tokens": 12, "total_tokens": 24, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_0ba0d124f1" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1a80679a8311a6-MRS Connection: - keep-alive Content-Type: - application/json Date: - Wed, 13 Nov 2024 00:03:58 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '796' openai-organization: test_openai_org_id openai-processing-ms: - '359' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '30000' x-ratelimit-limit-tokens: - '150000000' x-ratelimit-remaining-requests: - '29999' x-ratelimit-remaining-tokens: - '149999978' x-ratelimit-reset-requests: - 2ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_41ea134c1fc450d4ca4cf8d0c6a7c53a status: code: 200 message: OK version: 1 test_async_chat_completion_multiple_choices.yaml000066400000000000000000000066601527070072300510150ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4o-mini", "n": 2, "stream": false } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '114' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASv9XLlMmT7H3cf50dNTesHDBDwX5", "object": "chat.completion", "created": 1731456243, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "This is a test.", "refusal": null }, "logprobs": null, "finish_reason": "stop" }, { "index": 1, "message": { "role": "assistant", "content": "This is a test.", "refusal": null }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 10, "total_tokens": 22, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_0ba0d124f1" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1a808f6d8e0d8b-MRS Connection: - keep-alive Content-Type: - application/json Date: - Wed, 13 Nov 2024 00:04:04 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '970' openai-organization: test_openai_org_id openai-processing-ms: - '306' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '30000' x-ratelimit-limit-tokens: - '150000000' x-ratelimit-remaining-requests: - '29999' x-ratelimit-remaining-tokens: - '149999962' x-ratelimit-reset-requests: - 2ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_1317908e0f9b73276b57d4e171c533ea status: code: 200 message: OK version: 1 test_async_chat_completion_multiple_choices_streaming.yaml000066400000000000000000001227741527070072300530730ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "system", "content": "You're a helpful assistant." }, { "role": "user", "content": "What's the weather in Seattle and San Francisco today?" } ], "model": "gpt-4o-mini", "n": 2, "stream": true, "stream_options": { "include_usage": true } } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '254' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |+ data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":"I'm"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":"I'm"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" unable"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" unable"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" provide"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" provide"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" real"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" real"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":"-time"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":"-time"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" weather"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" weather"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" updates"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" updates"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" as"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" as"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" my"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" my"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" knowledge"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" knowledge"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" was"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" only"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" last"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" extends"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" until"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" updated"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" October"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" October"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" "},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" "},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":"202"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":"202"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":"3"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":"1"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" I"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" I"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" don't"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" have"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" access"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" don't"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" have"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" access"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" live"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" live"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" data"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" data"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" However"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" However"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" can"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" can"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" easily"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" easily"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" check"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" check"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" current"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" current"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" weather"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" weather"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" for"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" Seattle"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" Seattle"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" San"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" San"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" Francisco"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" Francisco"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" by"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" using"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" visiting"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" weather"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" weather"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" website"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" website"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" using"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" mobile"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" app"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" weather"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" for"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" app"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" most"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" If"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" accurate"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" need"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" up"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" historical"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" weather"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" information"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" general"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" climate"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" data"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" for"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" those"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" cities"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" feel"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" free"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" ask"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":"!"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":"-to"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":"-date"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" information"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} data: {"id":"chatcmpl-ASv9hB9He94oQyZr1CDC8coqvmn5U","object":"chat.completion.chunk","created":1731456253,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[],"usage":{"prompt_tokens":26,"completion_tokens":133,"total_tokens":159,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} data: [DONE] headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1a80ceac3ce19a-MRS Connection: - keep-alive Content-Type: - text/event-stream; charset=utf-8 Date: - Wed, 13 Nov 2024 00:04:13 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 openai-organization: test_openai_org_id openai-processing-ms: - '126' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '30000' x-ratelimit-limit-tokens: - '150000000' x-ratelimit-remaining-requests: - '29999' x-ratelimit-remaining-tokens: - '149999945' x-ratelimit-reset-requests: - 2ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_5dd8b6845db59fa55cf226eda1f5a2c6 status: code: 200 message: OK version: 1 test_async_chat_completion_multiple_tools_streaming_no_content.yaml000066400000000000000000000220441527070072300550310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "system", "content": "You're a helpful assistant." }, { "role": "user", "content": "What's the weather in Seattle and San Francisco today?" } ], "model": "gpt-4o-mini", "stream": true, "stream_options": { "include_usage": true }, "tool_choice": "auto", "tools": [ { "type": "function", "function": { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. Boston, MA" } }, "required": [ "location" ], "additionalProperties": false } } } ] } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '602' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |+ data: {"id":"chatcmpl-ASv9l0RKJrq2fTx2dK5jhJoIr4rMI","object":"chat.completion.chunk","created":1731456257,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"role":"assistant","content":null},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9l0RKJrq2fTx2dK5jhJoIr4rMI","object":"chat.completion.chunk","created":1731456257,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_hqkL24CLEwnniv4GDrjk14Iu","type":"function","function":{"name":"get_current_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9l0RKJrq2fTx2dK5jhJoIr4rMI","object":"chat.completion.chunk","created":1731456257,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9l0RKJrq2fTx2dK5jhJoIr4rMI","object":"chat.completion.chunk","created":1731456257,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9l0RKJrq2fTx2dK5jhJoIr4rMI","object":"chat.completion.chunk","created":1731456257,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n\": \"S"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9l0RKJrq2fTx2dK5jhJoIr4rMI","object":"chat.completion.chunk","created":1731456257,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"eatt"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9l0RKJrq2fTx2dK5jhJoIr4rMI","object":"chat.completion.chunk","created":1731456257,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"le, W"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9l0RKJrq2fTx2dK5jhJoIr4rMI","object":"chat.completion.chunk","created":1731456257,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"A\"}"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9l0RKJrq2fTx2dK5jhJoIr4rMI","object":"chat.completion.chunk","created":1731456257,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"id":"call_0s1enkFttXjIR7ozHoGMcnUu","type":"function","function":{"name":"get_current_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9l0RKJrq2fTx2dK5jhJoIr4rMI","object":"chat.completion.chunk","created":1731456257,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9l0RKJrq2fTx2dK5jhJoIr4rMI","object":"chat.completion.chunk","created":1731456257,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9l0RKJrq2fTx2dK5jhJoIr4rMI","object":"chat.completion.chunk","created":1731456257,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"n\": \"S"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9l0RKJrq2fTx2dK5jhJoIr4rMI","object":"chat.completion.chunk","created":1731456257,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"an F"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9l0RKJrq2fTx2dK5jhJoIr4rMI","object":"chat.completion.chunk","created":1731456257,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"ranci"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9l0RKJrq2fTx2dK5jhJoIr4rMI","object":"chat.completion.chunk","created":1731456257,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"sco, C"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9l0RKJrq2fTx2dK5jhJoIr4rMI","object":"chat.completion.chunk","created":1731456257,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"A\"}"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9l0RKJrq2fTx2dK5jhJoIr4rMI","object":"chat.completion.chunk","created":1731456257,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null} data: {"id":"chatcmpl-ASv9l0RKJrq2fTx2dK5jhJoIr4rMI","object":"chat.completion.chunk","created":1731456257,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[],"usage":{"prompt_tokens":75,"completion_tokens":51,"total_tokens":126,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} data: [DONE] headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1a80e4cfb00d86-MRS Connection: - keep-alive Content-Type: - text/event-stream; charset=utf-8 Date: - Wed, 13 Nov 2024 00:04:19 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 openai-organization: test_openai_org_id openai-processing-ms: - '1597' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '30000' x-ratelimit-limit-tokens: - '150000000' x-ratelimit-remaining-requests: - '29999' x-ratelimit-remaining-tokens: - '149999960' x-ratelimit-reset-requests: - 2ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_487aef2347cb4d1f97077c488dd93628 status: code: 200 message: OK version: 1 test_async_chat_completion_multiple_tools_streaming_with_content.yaml000066400000000000000000000220441527070072300553700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "system", "content": "You're a helpful assistant." }, { "role": "user", "content": "What's the weather in Seattle and San Francisco today?" } ], "model": "gpt-4o-mini", "stream": true, "stream_options": { "include_usage": true }, "tool_choice": "auto", "tools": [ { "type": "function", "function": { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. Boston, MA" } }, "required": [ "location" ], "additionalProperties": false } } } ] } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '602' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |+ data: {"id":"chatcmpl-ASv9jfgm5JguNGaR9o9u94HpuhV7T","object":"chat.completion.chunk","created":1731456255,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"role":"assistant","content":null},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9jfgm5JguNGaR9o9u94HpuhV7T","object":"chat.completion.chunk","created":1731456255,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_oJL2dc4GjWVxqBtWlGLwjbsR","type":"function","function":{"name":"get_current_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9jfgm5JguNGaR9o9u94HpuhV7T","object":"chat.completion.chunk","created":1731456255,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9jfgm5JguNGaR9o9u94HpuhV7T","object":"chat.completion.chunk","created":1731456255,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9jfgm5JguNGaR9o9u94HpuhV7T","object":"chat.completion.chunk","created":1731456255,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n\": \"S"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9jfgm5JguNGaR9o9u94HpuhV7T","object":"chat.completion.chunk","created":1731456255,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"eatt"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9jfgm5JguNGaR9o9u94HpuhV7T","object":"chat.completion.chunk","created":1731456255,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"le, W"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9jfgm5JguNGaR9o9u94HpuhV7T","object":"chat.completion.chunk","created":1731456255,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"A\"}"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9jfgm5JguNGaR9o9u94HpuhV7T","object":"chat.completion.chunk","created":1731456255,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"id":"call_ON3lp1OWsbw2obNRD43KVDp6","type":"function","function":{"name":"get_current_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9jfgm5JguNGaR9o9u94HpuhV7T","object":"chat.completion.chunk","created":1731456255,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9jfgm5JguNGaR9o9u94HpuhV7T","object":"chat.completion.chunk","created":1731456255,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9jfgm5JguNGaR9o9u94HpuhV7T","object":"chat.completion.chunk","created":1731456255,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"n\": \"S"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9jfgm5JguNGaR9o9u94HpuhV7T","object":"chat.completion.chunk","created":1731456255,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"an F"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9jfgm5JguNGaR9o9u94HpuhV7T","object":"chat.completion.chunk","created":1731456255,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"ranci"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9jfgm5JguNGaR9o9u94HpuhV7T","object":"chat.completion.chunk","created":1731456255,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"sco, C"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9jfgm5JguNGaR9o9u94HpuhV7T","object":"chat.completion.chunk","created":1731456255,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"A\"}"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9jfgm5JguNGaR9o9u94HpuhV7T","object":"chat.completion.chunk","created":1731456255,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null} data: {"id":"chatcmpl-ASv9jfgm5JguNGaR9o9u94HpuhV7T","object":"chat.completion.chunk","created":1731456255,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[],"usage":{"prompt_tokens":75,"completion_tokens":51,"total_tokens":126,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} data: [DONE] headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1a80d8efb9e1c8-MRS Connection: - keep-alive Content-Type: - text/event-stream; charset=utf-8 Date: - Wed, 13 Nov 2024 00:04:16 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 openai-organization: test_openai_org_id openai-processing-ms: - '1162' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '30000' x-ratelimit-limit-tokens: - '150000000' x-ratelimit-remaining-requests: - '29999' x-ratelimit-remaining-tokens: - '149999960' x-ratelimit-reset-requests: - 2ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_0b6729aef347cecd61ba3b7b7a8d4719 status: code: 200 message: OK version: 1 test_async_chat_completion_no_content.yaml000066400000000000000000000062421527070072300476270ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4o-mini", "stream": false } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '106' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASv9R2E7Yhb2e7bj4Xl0qm9s3J42Y", "object": "chat.completion", "created": 1731456237, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "This is a test. How can I assist you further?", "refusal": null }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 12, "total_tokens": 24, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_0ba0d124f1" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1a80679a8311a6-MRS Connection: - keep-alive Content-Type: - application/json Date: - Wed, 13 Nov 2024 00:03:58 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '796' openai-organization: test_openai_org_id openai-processing-ms: - '359' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '30000' x-ratelimit-limit-tokens: - '150000000' x-ratelimit-remaining-requests: - '29999' x-ratelimit-remaining-tokens: - '149999978' x-ratelimit-reset-requests: - 2ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_41ea134c1fc450d4ca4cf8d0c6a7c53a status: code: 200 message: OK version: 1 test_async_chat_completion_streaming.yaml000066400000000000000000000106621527070072300474530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4", "stream": true, "stream_options": { "include_usage": true } } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '142' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |+ data: {"id":"chatcmpl-ASv9ejXDUtAhGOJJxWuw026zdinc4","object":"chat.completion.chunk","created":1731456250,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9ejXDUtAhGOJJxWuw026zdinc4","object":"chat.completion.chunk","created":1731456250,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"This"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9ejXDUtAhGOJJxWuw026zdinc4","object":"chat.completion.chunk","created":1731456250,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9ejXDUtAhGOJJxWuw026zdinc4","object":"chat.completion.chunk","created":1731456250,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9ejXDUtAhGOJJxWuw026zdinc4","object":"chat.completion.chunk","created":1731456250,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" test"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9ejXDUtAhGOJJxWuw026zdinc4","object":"chat.completion.chunk","created":1731456250,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9ejXDUtAhGOJJxWuw026zdinc4","object":"chat.completion.chunk","created":1731456250,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} data: {"id":"chatcmpl-ASv9ejXDUtAhGOJJxWuw026zdinc4","object":"chat.completion.chunk","created":1731456250,"model":"gpt-4-0613","system_fingerprint":null,"choices":[],"usage":{"prompt_tokens":12,"completion_tokens":5,"total_tokens":17,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} data: [DONE] headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1a80bd2f31e1e5-MRS Connection: - keep-alive Content-Type: - text/event-stream; charset=utf-8 Date: - Wed, 13 Nov 2024 00:04:11 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 openai-organization: test_openai_org_id openai-processing-ms: - '196' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '1000000' x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - '999977' x-ratelimit-reset-requests: - 6ms x-ratelimit-reset-tokens: - 1ms x-request-id: - req_cc9204ae23338b130df11c8c5b5f31af status: code: 200 message: OK version: 1 test_async_chat_completion_streaming_not_complete.yaml000066400000000000000000000075321527070072300522250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4", "stream": true } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '99' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |+ data: {"id":"chatcmpl-ASv9gROIIAvRs9QnmLP8Nzs3PGMCX","object":"chat.completion.chunk","created":1731456252,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9gROIIAvRs9QnmLP8Nzs3PGMCX","object":"chat.completion.chunk","created":1731456252,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"This"},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9gROIIAvRs9QnmLP8Nzs3PGMCX","object":"chat.completion.chunk","created":1731456252,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9gROIIAvRs9QnmLP8Nzs3PGMCX","object":"chat.completion.chunk","created":1731456252,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9gROIIAvRs9QnmLP8Nzs3PGMCX","object":"chat.completion.chunk","created":1731456252,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" test"},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9gROIIAvRs9QnmLP8Nzs3PGMCX","object":"chat.completion.chunk","created":1731456252,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASv9gROIIAvRs9QnmLP8Nzs3PGMCX","object":"chat.completion.chunk","created":1731456252,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]} data: [DONE] headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1a80c54d00e288-MRS Connection: - keep-alive Content-Type: - text/event-stream; charset=utf-8 Date: - Wed, 13 Nov 2024 00:04:12 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 openai-organization: test_openai_org_id openai-processing-ms: - '283' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '1000000' x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - '999977' x-ratelimit-reset-requests: - 6ms x-ratelimit-reset-tokens: - 1ms x-request-id: - req_e9e4ea6fd060391e8cc8cfea78ad9a15 status: code: 200 message: OK version: 1 test_async_chat_completion_streaming_unsampled.yaml000066400000000000000000000106621527070072300515230ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4", "stream": true, "stream_options": { "include_usage": true } } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '142' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |+ data: {"id":"chatcmpl-ASv9ejXDUtAhGOJJxWuw026zdinc4","object":"chat.completion.chunk","created":1731456250,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9ejXDUtAhGOJJxWuw026zdinc4","object":"chat.completion.chunk","created":1731456250,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"This"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9ejXDUtAhGOJJxWuw026zdinc4","object":"chat.completion.chunk","created":1731456250,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9ejXDUtAhGOJJxWuw026zdinc4","object":"chat.completion.chunk","created":1731456250,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9ejXDUtAhGOJJxWuw026zdinc4","object":"chat.completion.chunk","created":1731456250,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" test"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9ejXDUtAhGOJJxWuw026zdinc4","object":"chat.completion.chunk","created":1731456250,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASv9ejXDUtAhGOJJxWuw026zdinc4","object":"chat.completion.chunk","created":1731456250,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} data: {"id":"chatcmpl-ASv9ejXDUtAhGOJJxWuw026zdinc4","object":"chat.completion.chunk","created":1731456250,"model":"gpt-4-0613","system_fingerprint":null,"choices":[],"usage":{"prompt_tokens":12,"completion_tokens":5,"total_tokens":17,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} data: [DONE] headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1a80bd2f31e1e5-MRS Connection: - keep-alive Content-Type: - text/event-stream; charset=utf-8 Date: - Wed, 13 Nov 2024 00:04:11 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 openai-organization: test_openai_org_id openai-processing-ms: - '196' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '1000000' x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - '999977' x-ratelimit-reset-requests: - 6ms x-ratelimit-reset-tokens: - 1ms x-request-id: - req_cc9204ae23338b130df11c8c5b5f31af status: code: 200 message: OK version: 1 test_async_chat_completion_tool_calls_no_content.yaml000066400000000000000000000216521527070072300520440ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "system", "content": "You're a helpful assistant." }, { "role": "user", "content": "What's the weather in Seattle and San Francisco today?" } ], "model": "gpt-4o-mini", "tool_choice": "auto", "tools": [ { "type": "function", "function": { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. Boston, MA" } }, "required": [ "location" ], "additionalProperties": false } } } ] } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '543' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASv9bJqWatpvCC0YMsYRcTSIiXoxk", "object": "chat.completion", "created": 1731456247, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": null, "tool_calls": [ { "id": "call_vwOezSsB5j9ei1SSMlZjqx7g", "type": "function", "function": { "name": "get_current_weather", "arguments": "{\"location\": \"Seattle, WA\"}" } }, { "id": "call_LzeIYcKhHnVF60u4LmBpT1tv", "type": "function", "function": { "name": "get_current_weather", "arguments": "{\"location\": \"San Francisco, CA\"}" } } ], "refusal": null }, "logprobs": null, "finish_reason": "tool_calls" } ], "usage": { "prompt_tokens": 75, "completion_tokens": 51, "total_tokens": 126, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_0ba0d124f1" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1a80a9f8fbe1c9-MRS Connection: - keep-alive Content-Type: - application/json Date: - Wed, 13 Nov 2024 00:04:08 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '1308' openai-organization: test_openai_org_id openai-processing-ms: - '808' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '30000' x-ratelimit-limit-tokens: - '150000000' x-ratelimit-remaining-requests: - '29999' x-ratelimit-remaining-tokens: - '149999960' x-ratelimit-reset-requests: - 2ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_f1b9b75e4a73b542c9b1b992cd52c66f status: code: 200 message: OK - request: body: |- { "messages": [ { "role": "system", "content": "You're a helpful assistant." }, { "role": "user", "content": "What's the weather in Seattle and San Francisco today?" }, { "role": "assistant", "tool_calls": [ { "id": "call_vwOezSsB5j9ei1SSMlZjqx7g", "function": { "arguments": "{\"location\": \"Seattle, WA\"}", "name": "get_current_weather" }, "type": "function" }, { "id": "call_LzeIYcKhHnVF60u4LmBpT1tv", "function": { "arguments": "{\"location\": \"San Francisco, CA\"}", "name": "get_current_weather" }, "type": "function" } ] }, { "role": "tool", "content": "50 degrees and raining", "tool_call_id": "call_vwOezSsB5j9ei1SSMlZjqx7g" }, { "role": "tool", "content": "70 degrees and sunny", "tool_call_id": "call_LzeIYcKhHnVF60u4LmBpT1tv" } ], "model": "gpt-4o-mini" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '746' content-type: - application/json cookie: - test_cookie host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASv9dfXfIwGCZgeWzDTbCh0FuU9kh", "object": "chat.completion", "created": 1731456249, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Today, the weather in Seattle is 50 degrees and raining, while in San Francisco, it's 70 degrees and sunny.", "refusal": null }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 99, "completion_tokens": 25, "total_tokens": 124, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_0ba0d124f1" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1a80b3baade1c9-MRS Connection: - keep-alive Content-Type: - application/json Date: - Wed, 13 Nov 2024 00:04:10 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '859' openai-organization: test_openai_org_id openai-processing-ms: - '972' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '30000' x-ratelimit-limit-tokens: - '150000000' x-ratelimit-remaining-requests: - '29999' x-ratelimit-remaining-tokens: - '149999948' x-ratelimit-reset-requests: - 2ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_754e6b59f1d3da727e2210e3d8c56243 status: code: 200 message: OK version: 1 test_async_chat_completion_tool_calls_with_content.yaml000066400000000000000000000216661527070072300524100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "system", "content": "You're a helpful assistant." }, { "role": "user", "content": "What's the weather in Seattle and San Francisco today?" } ], "model": "gpt-4o-mini", "tool_choice": "auto", "tools": [ { "type": "function", "function": { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. Boston, MA" } }, "required": [ "location" ], "additionalProperties": false } } } ] } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '543' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASv9ZqgNAOJAOLYMgdmxouatKXJlk", "object": "chat.completion", "created": 1731456245, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": null, "tool_calls": [ { "id": "call_O8NOz8VlxosSASEsOY7LDUcP", "type": "function", "function": { "name": "get_current_weather", "arguments": "{\"location\": \"Seattle, WA\"}" } }, { "id": "call_3m7cyuckijnpiWr6tq0Tl8Mg", "type": "function", "function": { "name": "get_current_weather", "arguments": "{\"location\": \"San Francisco, CA\"}" } } ], "refusal": null }, "logprobs": null, "finish_reason": "tool_calls" } ], "usage": { "prompt_tokens": 75, "completion_tokens": 51, "total_tokens": 126, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_0ba0d124f1" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1a8098ac5ae167-MRS Connection: - keep-alive Content-Type: - application/json Date: - Wed, 13 Nov 2024 00:04:06 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '1308' openai-organization: test_openai_org_id openai-processing-ms: - '937' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '30000' x-ratelimit-limit-tokens: - '150000000' x-ratelimit-remaining-requests: - '29999' x-ratelimit-remaining-tokens: - '149999960' x-ratelimit-reset-requests: - 2ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_3cd7152d2c8c10b4f354b27165f6c2b5 status: code: 200 message: OK - request: body: |- { "messages": [ { "role": "system", "content": "You're a helpful assistant." }, { "role": "user", "content": "What's the weather in Seattle and San Francisco today?" }, { "role": "assistant", "tool_calls": [ { "id": "call_O8NOz8VlxosSASEsOY7LDUcP", "function": { "arguments": "{\"location\": \"Seattle, WA\"}", "name": "get_current_weather" }, "type": "function" }, { "id": "call_3m7cyuckijnpiWr6tq0Tl8Mg", "function": { "arguments": "{\"location\": \"San Francisco, CA\"}", "name": "get_current_weather" }, "type": "function" } ] }, { "role": "tool", "content": "50 degrees and raining", "tool_call_id": "call_O8NOz8VlxosSASEsOY7LDUcP" }, { "role": "tool", "content": "70 degrees and sunny", "tool_call_id": "call_3m7cyuckijnpiWr6tq0Tl8Mg" } ], "model": "gpt-4o-mini" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '746' content-type: - application/json cookie: - test_cookie host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASv9aQnGndy04lqKoPRagym1eEaQK", "object": "chat.completion", "created": 1731456246, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Today, Seattle is experiencing 50 degrees and raining, while San Francisco has a pleasant 70 degrees and sunny weather.", "refusal": null }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 99, "completion_tokens": 24, "total_tokens": 123, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_f59a81427f" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1a80a39c71e167-MRS Connection: - keep-alive Content-Type: - application/json Date: - Wed, 13 Nov 2024 00:04:07 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '871' openai-organization: test_openai_org_id openai-processing-ms: - '477' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '30000' x-ratelimit-limit-tokens: - '150000000' x-ratelimit-remaining-requests: - '29999' x-ratelimit-remaining-tokens: - '149999948' x-ratelimit-reset-requests: - 2ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_193c74758ea30e77e55afe931e89fd6c status: code: 200 message: OK version: 1 test_async_chat_completion_with_content.yaml000066400000000000000000000062421527070072300501660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4o-mini", "stream": false } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '106' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASv9R2E7Yhb2e7bj4Xl0qm9s3J42Y", "object": "chat.completion", "created": 1731456237, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "This is a test. How can I assist you further?", "refusal": null }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 12, "total_tokens": 24, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_0ba0d124f1" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1a80679a8311a6-MRS Connection: - keep-alive Content-Type: - application/json Date: - Wed, 13 Nov 2024 00:03:58 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '796' openai-organization: test_openai_org_id openai-processing-ms: - '359' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '30000' x-ratelimit-limit-tokens: - '150000000' x-ratelimit-remaining-requests: - '29999' x-ratelimit-remaining-tokens: - '149999978' x-ratelimit-reset-requests: - 2ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_41ea134c1fc450d4ca4cf8d0c6a7c53a status: code: 200 message: OK version: 1 test_async_chat_completion_with_raw_response.yaml000066400000000000000000000062621527070072300512250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4o-mini" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '90' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.54.3 x-stainless-arch: - arm64 x-stainless-async: - 'true' x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.54.3 x-stainless-raw-response: - 'true' x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.6 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASYMQRl3A3DXL9FWCK9tnGRcKIO7q", "object": "chat.completion", "created": 1731368630, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "This is a test.", "refusal": null }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 5, "total_tokens": 17, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_0ba0d124f1" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e122593ff368bc8-SIN Connection: - keep-alive Content-Type: - application/json Date: - Mon, 11 Nov 2024 23:43:50 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '765' openai-organization: test_openai_org_id openai-processing-ms: - '287' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - '199977' x-ratelimit-reset-requests: - 8.64s x-ratelimit-reset-tokens: - 6ms x-request-id: - req_58cff97afd0e7c0bba910ccf0b044a6f status: code: 200 message: OK version: 1test_async_embeddings_error_handling.yaml000066400000000000000000000036641527070072300474140ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "This is a test for async embeddings with error", "model": "non-existent-embedding-model" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '100' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - Linux x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.9.2 method: POST uri: https://api.openai.com/v1/embeddings response: body: string: |- { "error": { "message": "The model `non-existent-embedding-model` does not exist or you do not have access to it.", "type": "invalid_request_error", "param": null, "code": "model_not_found" } } headers: CF-RAY: - 939c1721df5f986e-NRT Connection: - keep-alive Content-Type: - application/json; charset=utf-8 Date: - Sat, 03 May 2025 01:47:27 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '234' openai-organization: test_openai_org_id strict-transport-security: - max-age=31536000; includeSubDomains; preload vary: - Origin x-request-id: - req_38df12bea294f0b00173735f303f8f5b status: code: 404 message: Not Found version: 1 test_async_embeddings_no_content.yaml000066400000000000000000001356151527070072300465670ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "This is a test for async embeddings", "model": "text-embedding-3-small" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '83' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - Linux x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.9.2 method: POST uri: https://api.openai.com/v1/embeddings response: body: string: |- { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": [ -0.0012588563, -0.029314144, -0.004568109, -0.021721158, -0.025343766, -0.0004159207, 0.04500728, 0.03663181, 0.009042029, -0.0049231243, 0.026416058, -0.030574812, 7.522572e-05, -0.031241372, -0.03683468, 0.0045789764, -0.017837722, -0.0065569207, 0.017272595, -0.0049231243, -0.008542109, -0.047383714, 0.015185973, 0.042804737, 0.0033654028, -0.019634536, 0.012171963, 0.041471615, 0.033994555, -0.034690093, 0.006172924, -0.038370665, 0.009599911, -0.028618604, -0.017069729, 0.06578656, -0.010759146, 0.036747735, -0.039819706, -0.0077886074, 0.0027314464, -0.03750124, -0.04993403, -0.0045898445, 0.013425386, 0.05135409, -0.03257449, -0.01773629, 0.018866543, 0.034516208, -0.048398044, 0.012041549, -0.0050209346, 0.047876388, -0.032081816, -0.007237971, -0.029792327, 0.030429907, -0.0102447355, 0.0064736004, 0.061787203, -0.031212391, -0.029256182, -0.018881032, 0.006412016, -0.00872324, -0.022416698, 0.025285805, -0.011730005, -0.0064736004, 0.05193371, 0.023720838, -0.009969417, 0.0034740812, -0.0029632933, 0.021025617, 0.012063284, 0.016519092, -0.016345207, -0.034516208, -0.030111117, 0.027531821, -0.029256182, -0.019040428, -0.01775078, -0.013179048, -0.07807445, -0.009136218, -0.0018765109, 0.0011746306, -0.020721318, -0.028256342, -0.07679929, -0.029328635, 0.029343124, 0.011845929, -0.001069575, -0.04593467, -0.03428436, 0.021721158, 0.01576559, 0.02543071, -0.017576894, 0.008230565, 0.0015187784, -0.016475622, 0.0012162906, -0.00537595, -0.04758658, -0.003651589, -0.05671555, -0.014193378, 0.0036751358, 0.003187895, 0.057150263, -0.022576094, -0.00452826, -0.013323952, -0.008563845, -0.021156032, -0.018576734, -0.007031482, 0.03628404, -0.007491553, -0.024590263, -0.046166517, -0.03553054, -0.047064923, -0.061787203, 0.0005922964, 0.02082275, -0.028270833, 0.0557302, 0.039153147, -0.06288847, -0.011164878, -0.077205025, 0.027010165, -0.0033291767, 0.022981824, -0.017910173, -0.04660123, 0.03654487, -0.022720998, -0.045673843, -0.010708429, -0.021996476, 0.047905367, 0.005970058, -0.008643542, -0.011338763, -0.02548867, -0.04494932, 0.042456966, -0.018953485, -0.031734046, -0.038370665, 3.5575144e-05, 0.010679448, -0.0052346685, -0.036515888, 0.019938834, 0.034139458, -0.0284737, 0.0017578705, 0.03083564, 0.01005636, -0.005129613, -0.022735488, -0.062830515, 0.004568109, -0.03750124, -0.054889757, 0.000167206, -0.012012568, -0.014867184, 0.028096948, -0.06943815, 0.012164718, -0.011186614, -0.014722279, 0.024039626, 0.022416698, 0.00039848688, -0.06074389, 0.06584452, 0.02246017, 0.06404771, 0.0010713863, 0.0025285804, -0.007006124, 0.058599308, 0.056918416, -0.008607317, 0.024213511, -0.031212391, 0.042891677, 0.003068349, 0.031618122, -0.01223717, 0.0075712507, 0.03990665, -0.0050173122, 0.0057671918, 0.05199167, -0.0251409, -0.023633895, -0.042312063, -0.009969417, 0.0045572408, 0.042456966, -0.019388199, 0.0028763507, 0.010643222, -0.015953965, 0.007810343, -0.034371305, 0.013505083, 0.037066527, -0.018055078, -0.030980544, 0.043326393, 0.054426063, 0.0402834, -0.026256664, -0.0015142502, -0.016026419, -0.0013657232, 0.017388519, 0.00015962117, -0.004473921, -0.023648385, -0.0016011928, -0.01677992, 0.03190793, -0.015968457, -0.001709871, -0.0135558, -0.02482211, 0.0091144815, -0.015838042, -0.034690093, 0.01391806, -0.021953005, -0.006654731, 0.023083258, -0.001220819, -0.028401246, 0.038167797, -0.019069409, -0.019344727, 0.02218485, -0.014570129, 0.0020576413, -0.04219614, -0.023271633, -0.030024175, 0.03929805, 0.0073176683, 0.005618665, -0.012910975, 0.042341042, -0.02146033, 0.007448082, 0.013968777, -0.05158594, 0.033009205, -0.008317508, -0.006031642, -0.047180846, -0.00988972, -0.0060026613, 0.057440072, -0.036747735, -0.057816826, 0.006129453, -0.036370985, -0.007364762, -0.007926267, 0.0067597865, 0.004408714, -0.046977982, 0.010983747, 0.0342264, -0.011309782, 0.007940757, 0.0013113841, -0.019518612, -0.0016138719, 0.011940116, -0.0052093104, -0.05868625, 0.051209185, 0.029009845, 0.0051513487, 0.004386978, 0.033443917, -0.010563525, 0.016533583, -0.010208509, 0.05222352, -0.04350028, 0.0322557, 0.02822736, -0.018634696, 0.033617802, -0.04865887, 0.040718116, 0.040399324, 0.001111235, -0.028430227, 0.01275158, -0.035646465, 0.023039786, -0.046717152, 0.008795693, 0.02447434, -0.015635176, 0.034371305, -0.047702502, -0.008998558, -0.036399964, 0.011621326, -0.045499958, -0.0063830353, -0.03118341, -0.00072678574, -0.035095826, 0.015301896, 0.029140258, -0.0068902005, 0.028444719, 0.03663181, -0.002318469, 0.023691857, -0.026633414, -0.00048769361, -0.023648385, 0.0496732, -0.0018620205, -0.04121079, -0.0017297954, 0.04100792, -0.025532141, 0.005415799, 0.052803133, 0.027039146, 0.030951563, 0.031734046, -0.017388519, 0.013447121, -0.0066112597, -0.0068865777, 0.003044802, 0.057845805, -0.05767192, -0.020243134, -0.0077596265, -0.010222999, -0.019634536, 0.025749497, -0.0053723278, -0.009962172, 0.066655986, 0.013505083, -0.031994876, -0.007803098, 0.021851571, -0.002628202, -0.015562724, -0.016982786, 0.049876068, 0.004289168, 0.009353574, -0.02586542, 0.02182259, -0.026705867, 0.03990665, 0.04590569, 0.054976698, 0.03153118, -0.032284684, -0.039182127, -0.07488655, -0.02143135, 0.005129613, -0.014657072, -0.01912737, -0.026213191, -0.011476423, 0.0125849405, -0.031994876, -0.025169881, -0.00569474, 0.024300454, 0.027604273, 0.058077652, -0.025720516, 0.009382554, -0.0035429106, -0.0051404806, -0.045876708, -0.010273716, 0.0016645883, 0.0022170362, 0.023097748, 0.02819838, -0.0016274566, 0.025271313, -0.0093898, -0.005988171, -0.0011338763, 0.044021934, -0.05630982, -0.0009980286, 0.019011447, 0.012758825, -0.043703143, 0.010976503, 0.06729357, -0.036747735, -0.0584544, -0.024445359, -0.013512328, 0.024039626, -0.010447602, 0.075929865, 0.0060859816, 0.018315906, -0.06132351, -0.043674164, -0.018996956, 0.043413334, 0.038428627, -0.021054598, -0.022938354, 0.036689773, 0.019272275, -0.029212711, -0.018489791, -0.0052165557, -0.007237971, -0.0584544, -0.010527299, -0.009462252, -0.011222839, -0.026024817, 0.0037566444, -0.006346809, -0.01291822, -0.008143622, -0.067931145, 0.009599911, -0.029256182, -0.009027539, -0.027415898, 0.010143302, 0.0031208768, 0.06735153, 0.007861059, 0.019388199, -0.050484665, -0.06010631, 0.035878308, 0.06004835, 0.017374028, -0.0028093325, 0.010947522, 0.045528937, 0.0018131153, -0.008237811, 0.04636938, -0.0013901758, 0.027357936, 0.037530217, -0.035443597, 0.024010645, -0.021532781, -0.00030226135, -0.022039948, -0.012106756, -0.021040108, -0.039124165, -0.036313023, -0.00093463284, 0.14397693, -0.043065563, -0.019359218, 0.008773956, -0.012505243, -0.026503, 0.017794251, -0.012222679, -0.018533263, 0.034371305, -0.047238808, -0.01676543, -0.05094836, 0.03950092, -0.027560802, -0.012824032, 0.0063721677, -0.020170681, 0.021547273, 0.01776527, -0.0010795372, -0.041761424, 0.009650628, -0.014033983, 0.0171132, -0.014041228, 0.01191838, -0.020243134, -0.010831598, -0.012592185, -0.004285545, -0.036168117, 0.054918736, 0.005593307, -0.04184837, -0.009230405, -0.01914186, -0.0036443437, -0.041529577, 0.012512488, 0.013193538, -0.01375142, -0.057208225, -0.034139458, -0.0052708946, -0.018084059, -0.015910495, 0.028430227, -0.026430547, -0.027937554, -0.00047275034, -0.020735808, 0.017243614, 0.018402848, -0.014772995, 0.03883436, 0.0044884114, 0.034719076, -0.02112705, -0.022836922, 0.043413334, 0.023488991, 0.015026578, -0.020243134, 0.0152439345, 0.022720998, -0.018996956, 0.028430227, -0.017388519, 0.01225166, 0.013722439, 0.04500728, -0.016895844, -0.018156512, -0.022228323, 0.021445839, 0.03686366, 0.06706172, 0.035008885, -0.033067167, 0.03689264, 0.017808741, -0.0019073031, 0.044050913, -0.0050245575, -0.05801969, 0.010317188, 0.021590743, 0.02143135, 0.014533903, 0.01982291, -0.023517972, 0.018852051, -0.034342322, -0.037385315, -0.017069729, -0.010817108, 0.02279345, 0.0118241925, -0.00887539, 0.054049313, 0.004546373, -0.019170841, -0.010679448, 0.032516528, 0.02882147, 0.0019471518, 0.019764949, -0.038109835, 0.025995836, -0.010715675, 0.048484985, 0.0021337161, -0.03263245, 0.010578016, 0.011266311, 0.0018746996, -0.008607317, -0.005437535, 0.02987927, 0.006506204, -0.035066847, 0.016519092, 0.04019646, -0.014041228, -0.009143462, -0.030806659, 0.017939154, -0.0146136, -0.0031353673, -0.007006124, 0.008795693, 0.0080277, 0.028096948, -0.020909693, 0.014780241, -0.03184997, -0.00056014577, 0.019735968, -0.024068607, 0.0015857967, 0.0045318827, 0.00562591, 0.0029614822, 0.025720516, -0.015229444, 0.0052346685, 0.014910654, -0.037153468, -0.037877988, -0.0160554, -0.02754631, 0.039442956, 0.011382234, -0.024126569, -0.0106939385, 0.0001393572, 0.02315571, 0.0080277, -0.010998238, -0.010933031, 0.014316547, 0.0033961951, -0.004571731, -0.013425386, 0.0059773033, -0.032313664, 0.003635287, -0.010462092, 0.02586542, -0.07088719, -0.019837402, -0.011650307, -0.013381914, -0.016649507, 0.0073973658, 0.00247243, -0.013476102, 0.0129544465, 0.008216075, 0.043094546, -0.043703143, 0.04636938, -0.0017823231, -0.008636298, 0.010027379, 0.012983427, 0.04384805, -0.02047498, 0.023329595, -0.039182127, -0.029560482, -0.016635016, 0.01743199, -0.017475462, -0.0052093104, 0.020706827, -0.008360979, -0.026314626, 0.036805697, -0.0044232043, 0.03889232, 0.0016365132, 0.0021463952, -0.01844632, -0.046804097, -0.028125929, 0.052542306, -0.0029940854, -0.0065170717, 0.035762385, 0.010911295, 0.02987927, -0.018866543, 0.036428947, 0.007650948, 0.017837722, 0.008201584, -0.025923382, -0.004933992, -0.02987927, 0.015881514, 0.029096788, 0.030024175, -0.005339724, -0.016707469, -0.035095826, 0.011701024, 0.026387077, -0.007455327, -0.01409919, 0.0060461327, -0.012548714, -0.029966213, -0.038022894, -0.036660794, 0.0026734846, -0.0030647265, -0.0086145615, -0.0077234004, -0.042312063, -0.010063605, 0.0073901205, -0.013628251, 0.003169782, -0.010483827, 0.00019460198, -0.016649507, -0.011259066, -0.018736128, 0.00089478417, 0.001361195, -0.010672203, -0.040428307, -0.011925626, 0.01712769, 0.009643382, 0.011483667, -0.0012733467, -0.03019806, 0.048484985, 0.02751733, 0.0006565977, 0.0149541255, -0.011498158, -0.022923864, 0.012700864, -0.012027059, 0.022880392, -0.041819386, -0.036370985, 0.024054118, -0.022445679, -0.0124545265, 0.013635497, 0.02147482, -0.007089444, -0.012440036, 0.017953645, 0.015316387, -0.0112445755, -0.018562244, -0.0052817627, 0.045470975, 0.024995996, 0.0036207966, -0.023720838, 0.016519092, 0.002394544, 0.051701862, -0.006810503, -0.02214138, 0.017200142, -0.034081496, 0.018576734, -0.0155047625, 0.021909533, -0.00032105364, -0.026126249, -0.010606997, -0.015490272, -0.01744648, 0.01673645, -0.0028147665, -0.043529257, -0.057874784, -0.0042638094, 0.037820026, -0.028575132, 0.00652794, -0.0021645082, -0.044833396, -0.0630044, -0.018721638, 0.029140258, -0.023648385, -0.0044232043, 0.018518772, -0.0029687274, 0.01543231, 0.00313899, 0.020214153, 0.038428627, -0.041326713, -0.035878308, -0.029705387, 0.03683468, -0.009882474, 0.024039626, -0.01843183, 0.0031317447, -0.0113749895, -0.02714058, 0.008998558, 0.03156016, 0.012374829, -0.036950603, -0.0088246735, 0.0077234004, 0.008759466, -0.010143302, -0.007948002, -0.018808581, -0.0038218515, 0.012679128, 0.01257045, -0.0077523813, 0.0023818647, 0.002454317, -0.00063622056, 0.025981344, -0.017939154, -0.003948643, -0.028415738, -0.010672203, -0.024923543, 0.054628927, 0.010447602, 0.017156672, 0.013693458, -0.003807361, -0.030893601, 0.012613921, 0.0013177237, 0.0074082334, -0.023228163, -0.00011943286, 0.026358096, 0.005850512, 0.027024657, 0.0057092304, 0.023691857, -0.026894242, 0.014512167, 0.008527619, -0.00301401, 0.0292272, 0.005165839, -0.0052056876, 0.021025617, -5.7905127e-05, -0.02009823, 0.025937874, -0.008578336, 0.061613318, 0.017895684, -0.017968135, 0.007549515, 0.013816627, 0.035675444, 0.011295292, 0.019156352, 0.050745495, 0.015707629, 0.021300934, 0.017649347, -0.023387557, -0.010049115, -0.03127035, -0.052252498, -0.031357296, 0.027749177, 0.043703143, -0.03663181, -0.0052455366, 0.014809222, 0.004223961, -0.00049176905, 0.014983106, -0.03083564, 0.045036264, -0.013765911, 0.02822736, -0.007585741, -0.02583644, -0.011215595, -0.030140098, -0.014577375, 0.008962332, 0.036805697, 0.0023365822, -0.015359858, 0.00755676, 0.024923543, -0.020953165, -0.037414297, -0.011505403, 0.011845929, 0.017692817, 0.037964933, 0.022213832, -0.0044050915, -0.0018837561, 0.04836906, -0.015577215, -0.00789004, -0.04663021, 0.0025901648, 0.0047999555, -0.019562082, -0.0153743485, 0.011309782, -0.016504603, 0.03118341, 0.0110562, -0.0428627, 0.008071171, 6.022586e-05, -0.008339244, 0.0013747797, -0.035675444, 0.0030574813, 0.022373227, -0.041036904, -0.014475942, -0.021866063, -0.028908413, 0.02547418, -0.02282243, 0.020417018, -0.014910654, 0.0027785404, 0.028082456, 0.0051151225, -0.021286445, 0.022039948, 0.04428276, -0.0054592704, -0.019054918, -0.017475462, -0.021909533, 0.0218081, 0.004785465, 0.034979902, -0.0042203385, 0.0055462127, 0.030053155, 0.0023076013, -0.040776078, 0.01912737, -0.026560962, -0.023083258, -0.030748697, 0.023271633, 0.008549355, 0.017316066, 0.022054438, 0.016330717, 0.0055751936, 0.027604273, 0.021286445, 0.008933351, 0.027705707, 0.046977982, -0.015330877, 0.007984228, 0.015591705, 0.012657393, -0.013200783, 0.0046985224, -0.008085661, 0.008752221, 0.0062055276, 0.036428947, -0.0012008946, 0.016577054, -0.015519253, -0.036168117, 0.0041189054, -0.002626391, -0.020706827, -0.0050318027, 0.0502818, -0.02882147, 0.010346169, -0.0019471518, -0.020257624, -0.0055860616, 0.004187735, 0.0048035784, -0.022692017, -0.0020413396, -0.01612785, -0.008491393, 0.029299654, 0.014287566, -0.02985029, 0.008092906, -0.060512044, -0.028386757, -0.02282243, 0.0048289364, -0.023054278, 0.02415555, -0.020561922, -0.01205604, -0.02543071, 0.026705867, 0.010947522, -0.010317188, -0.017808741, -0.005165839, -0.032806337, 0.01193287, 0.028676566, -0.010353413, 0.014005003, -0.0075929863, -0.025952363, -0.026169721, 0.045326073, -0.0023039787, 0.015881514, 0.009628892, -0.030893601, -0.023851251, -0.0017651158, -0.0004333545, 0.030342964, 0.04494932, -0.01777976, 0.015403329, 0.004571731, 0.010085341, 0.02511192, -0.018924505, 0.022010967, -0.0013892702, 0.0022061684, 0.0018122096, 0.023604915, 0.0018457188, 0.010338923, 0.011664798, 0.0064265067, -0.03863149, -0.00028822376, 0.010215755, -0.013063124, -0.0027332578, -0.009462252, 0.010266471, 0.025416218, 0.008252301, 0.029415578, 0.0032023855, -0.013294972, 0.014142661, -0.0006253527, 0.027952043, 0.013968777, -0.012258906, -0.025242332, -0.0147440145, 0.003050236, -0.01343263, 0.006248999, -0.0076002316, -0.004633316, 0.021011127, 0.007520534, 0.008049435, -0.0061040944, 0.025575612, -0.0024995995, -0.017649347, -0.017808741, 0.028806979, 0.0019036805, -0.016533583, 0.016794411, -0.017997116, -0.0153743485, 0.018562244, -0.007216235, -0.02854615, 0.01123733, -0.007962492, 0.020953165, 0.0063612997, 0.025952363, -0.013947041, -0.0009142557, 0.0015830797, 0.032226723, -0.012773315, 0.015707629, 0.030371945, -0.009925946, -0.02586542, -0.025198862, 0.01875062, -0.04500728, -0.023706347, -0.037617162, 0.032081816, 0.005821531, 0.025648065, 0.027386917, -0.0136644775, 0.008715995, 0.028111437, 0.005158594, -0.020547433, 0.013287726, -0.006136698, -0.037240412, 0.0160554, -0.026343606, 0.004075434, -0.04364518, 0.0040066047, -0.01473677, 0.014925145, -0.011541629, 0.015953965, -0.011816948, 0.0036316644, 0.0055208546, 0.032980222, -0.0036968715, 0.025633574, 0.10792474, -0.0030701603, 0.002318469, 0.00044671286, 0.016692977, -0.021924024, -0.027126089, -0.0036425323, -0.01914186, -0.025937874, -0.0026535604, 0.032023855, 0.010259226, -0.016229283, 0.0039341524, 0.008433431, 0.0010949332, 0.002121037, -0.001087688, -0.00042044895, 0.010201264, 0.025387237, -0.0027332578, 0.016504603, -0.053440712, -0.009788287, -0.025242332, 0.010882314, 0.014830957, 0.043790087, -0.013773155, 0.03663181, -0.03118341, 0.0101143215, 0.00050942926, 0.023373067, -0.053759504, 0.008411696, 0.023561442, -0.013215274, 0.005379573, -0.009541949, -0.010128812, -0.017287085, -0.009715835, -0.04257289, -0.0140847, -0.020764789, -0.0054665157, -0.006690957, 0.0024271475, 0.027604273, -0.028560642, 0.024532301, 0.042833716, -0.030603793, 0.008752221, 0.02954599, 0.05129613, -0.023271633, 0.017533423, 0.025691535, -0.0402834, 0.023416538, 0.005202065, -0.022634055, -0.02543071, -0.0071365377, -0.00060678687, 0.03176303, -0.015953965, 0.021735648, -0.014635337, -0.050687533, -0.000110319736, 0.02080826, 0.01914186, -0.012302377, -0.025343766, 0.027357936, -0.018518772, 0.0054013086, 0.033791687, 0.033646785, -0.016287245, 0.0044340724, 0.002318469, -0.001212668, -0.0008449733, 0.04523913, -0.035675444, -0.021677686, 0.011838683, -0.02882147, 0.002878162, -0.00016143247, 0.0021898665, -0.03376271, 0.012838523, 0.010447602, 0.013447121, 0.03054583, -0.007839324, 0.016577054, 0.016171321, 0.015185973, -0.054078292, 0.04126875, 0.014758505, 0.036457926, 0.011208349, -0.028357776, 0.013606516, -0.012353093, -0.016939316, 0.012077775, 3.0763866e-05, 0.001561344, 0.0073973658, -0.028270833, 0.0075277793, 0.0025847307, 0.0046079573, -0.016359698, 0.0045173923, -0.043877028, 0.0052093104, -0.00045237318, -0.014193378, -0.021373387, 0.037762064, -0.0061765467, 0.005745456, -0.0018837561, 0.039703783, 0.008194339, 0.014367264, -0.027923062, -0.01373693, 0.02314122, 0.018996956, 0.024662714, 0.012222679, -0.0046152025, 0.014381754, -0.016997278, 0.010396885, 0.0035229863, -0.03292226, 0.008744976, -0.0056150425, -0.041181806, 0.0016229284, -0.029024335, 0.026198702, 0.0039595105, 0.020286605, 0.008252301, 0.004473921, -0.039442956, -0.011295292, -0.002979595, -0.03990665, 0.016533583, 0.024532301, 0.0045173923, 0.013577535, 0.016620526, 0.014055719, 0.02277896, 0.028415738, -0.025735008, -0.0045789764, -0.0008984068, -0.00048135404, -0.024083098, 0.0006316923, 0.020764789, -0.05674453, -0.0098245125, 0.015403329, -0.029705387, -0.017634856, 0.036023214, -0.002251451, -0.00789004, 0.0067344285, -0.016533583, -0.004506524, -0.00176421, -0.009578176, -0.00029682743, -0.034835, -0.0073720072, -0.017707309, -0.017953645, -0.026285645, -0.034168437, 0.0020141702, 0.036718756, -0.0012244415, -0.014120926, 0.011382234, -0.002466996, 0.032052837, 0.05129613, -0.020243134, 0.0147440145, -0.013476102, -0.019562082, 0.039471935, -0.0047999555, 0.006321451, 0.0026734846, 0.055237528, -0.008629052, 0.0076944195, -0.004137018, -0.0033454786, 0.015852533, 0.0052056876, 0.012193698, 0.012729845, -0.033733726, -0.047412694, 0.020895204, 0.024083098, 0.009512968, -0.021648705, 0.0016174945, -0.060859814, -0.047035944, -0.0221124, -0.0013412706, -0.0034106853, -0.02046049, -0.017301576, 0.019170841, 0.026372585, 9.8433055e-05, -0.031936914, -0.011215595, 0.015041068, 0.06004835, -0.0091144815, 0.005531722, -0.0069517847, 0.02175014, -0.02918373, -0.0032512906, -0.00060905097, 0.046079572, -0.009433271, -0.020692337, -0.027444879, -0.0026426925, -0.0058613797, -0.019866383, -0.0096651185, -0.012766071, -0.020214153, 0.002615523, 0.049702182, 0.008237811, 0.007679929, -0.038080856, 0.013265991, 0.011193858, -0.0069083134, 0.008049435, 0.005165839, -0.0035084959, -0.030111117, -0.0058831153, -0.0035247975, 0.012526979, -0.010578016, -0.014041228, -0.0122444155, -0.03721143, -0.0057418337, -0.0076002316, 0.0023836761, 0.0056440234, 0.0015993814, -0.006495336, -0.0028799733, -0.0044123367, -0.01576559, -0.021315426, 0.01911288, -0.022068929, -0.037617162, -0.001587608, -0.0063794125, 0.017605875, 0.0054918737, -0.019866383, -0.041471615, -0.007274197, -0.032516528, 0.026416058, 0.020040268, -0.011882154, -0.0008101057, 0.009911455, -0.017272595, 0.028053477, -0.002573863, 0.05468689, -0.03689264, 0.004408714, -0.0013965154, -0.021286445, 0.017852213, 0.019214313, -0.04121079, 0.029908251, 0.003461402, 0.038515568, -0.009353574, 0.029125769, -0.0027459369, 0.02985029, -0.0030339342, -0.018678168, 0.040689133, -0.006488091, 0.029676406, 0.03625506, -0.009788287, -0.019083899, 0.012744335, -0.01090405, 0.044137858, -0.005995416, -0.016837882, 0.022025457, 0.012389319, -0.046253458, -0.018388359, 0.017316066, -0.012171963, 0.00987523, 0.0040500755, 0.001626551, -0.009440516, -0.028879432, -0.002937935, -0.017316066, -0.005951945, 0.011983587, 0.04094996, -0.01975046, -0.016649507, 0.013715194, 0.029343124, -0.00313899, 0.013381914, -0.041181806, -0.0321108, -0.011317028, 0.033414938, -0.011230085, -0.022605075, -0.008049435, -0.013483347, -0.0008010492, 0.0010125189, -0.0443697, -0.034690093, 0.011990832, 0.0013385536, 0.0040283403, -0.008629052, -0.044137858, -0.04828212, -0.022213832, -0.024677206, -0.069206305, 0.018214474, -0.0010704807, 0.0005184858, 0.009773796, 0.013519573, 0.018721638, 0.003521175, -0.0237788, -0.012831277 ] } ], "model": "text-embedding-3-small", "usage": { "prompt_tokens": 7, "total_tokens": 7 } } headers: CF-RAY: - 939c170ecd39a608-NRT Connection: - keep-alive Content-Type: - application/json Date: - Sat, 03 May 2025 01:47:23 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-allow-origin: - '*' access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '33338' openai-model: - text-embedding-3-small openai-organization: test_openai_org_id openai-processing-ms: - '110' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload via: - envoy-router-6d66f695d7-fnmlz x-envoy-upstream-service-time: - '66' x-ratelimit-limit-requests: - '5000' x-ratelimit-limit-tokens: - '5000000' x-ratelimit-remaining-requests: - '4999' x-ratelimit-remaining-tokens: - '4999992' x-ratelimit-reset-requests: - 12ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_245fc8e62865f5396656bd94248d10fe status: code: 200 message: OK version: 1 test_async_embeddings_token_metrics.yaml000066400000000000000000001355461527070072300472720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "This is a test for async embeddings token metrics", "model": "text-embedding-3-small" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '97' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - Linux x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.9.2 method: POST uri: https://api.openai.com/v1/embeddings response: body: string: |- { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": [ 0.0033986655, -0.027189324, 0.0054322644, -0.026195275, -0.006202302, 0.0033024107, 0.018718911, 0.031977557, 0.045110196, -0.014574709, 0.029513437, -0.017948873, -0.029625442, -0.035673738, 0.0059677907, 0.0135736605, -0.032845598, -0.026251279, 0.013461655, -0.0014490705, -0.0103605045, -0.018396895, 0.020721009, 0.03757783, 0.013622663, -0.0011856827, 0.01929294, 0.0067308275, 0.031053511, -0.014490706, 0.02014698, -0.02074901, 0.00924045, -0.037465826, -0.029933458, 0.05768281, -0.010437508, 0.03346163, -0.016352797, -0.027973361, -0.01787887, -0.024543194, -0.060818963, -0.0042002043, -0.017416848, 0.038277864, 0.0045642224, -0.028001362, 0.023955166, 0.026503291, -0.06384311, 0.0086594215, -0.011536562, 0.034161665, -0.021939069, -0.01397268, -0.01253061, 0.022723107, -0.023689153, -0.014896725, 0.049198397, -0.03222957, -0.003822186, -0.023507144, 0.008127396, 0.0033811645, -0.0052467557, 0.025173226, -0.019586954, -0.02343714, 0.06955539, 0.03592575, 0.0026776304, -0.002689881, -0.016338795, -0.00780538, 0.006219803, -0.0059432895, 0.0020686006, -0.02690931, -0.049142394, 0.002299612, -0.020539, -0.023535145, -0.008260402, -0.01376267, -0.076443724, -0.031333525, 0.013132639, -0.007581369, -0.04090999, -0.012138591, -0.06182701, -0.022485094, 0.02734333, -0.015512755, 0.010864529, -0.028897407, -0.04090999, 0.0060517946, 0.00051846274, 0.021267036, -0.01992297, 0.03407766, -0.0040531973, -0.00025923137, 0.010934533, -0.0021386042, -0.026125273, -0.013181642, -0.05275457, -0.0020668507, -0.019586954, 0.013419653, 0.062499043, -0.0034441678, -0.0007542867, -0.01498073, 0.005932789, -0.008792428, -0.025971264, 0.003104651, 0.035869747, 0.0017010828, -0.026643297, -0.04497019, -0.041218005, -0.06843533, -0.0636751, 0.009828478, 0.011893579, -0.02548124, 0.06748328, 0.025131224, -0.02648929, -0.032509584, -0.034889698, 0.035785742, -0.028981412, 0.024837209, -0.015064733, -0.0328176, 0.05507868, 0.0076303715, -0.061603, -0.025551245, -0.039397918, 0.041246008, -0.0076723737, -0.0058277836, 0.006877835, -0.025957264, -0.027595343, 0.03306961, -0.019460948, -0.02525723, -0.017416848, -0.003533422, 0.0035176713, -0.033041608, -0.026657298, 0.01561076, 0.024725204, -0.01047951, 0.017514853, 0.03981794, -0.0024378686, -0.014644713, -0.030493485, -0.0492544, 0.007924385, -0.0698914, -0.01932094, -0.04004195, -0.034217667, -0.031361528, 0.009982486, -0.0575148, 0.022891115, 0.0018935922, -0.018942922, -0.012824625, 0.036709785, 0.013419653, -0.06137899, 0.062163025, 0.01869091, 0.0761077, 0.031053511, 0.01992297, -0.028491387, 0.07560368, 0.06748328, -0.0070913453, 0.020230984, -0.028645394, 0.035841744, 0.018242888, 0.014700715, 0.002772135, -0.004378713, 0.0369338, 0.017136835, -0.014252694, 0.039537925, -0.021701057, -0.034189664, -0.06406712, -0.0061112978, 0.018200886, 0.039929945, 0.0130276345, 0.01621279, 0.026167274, -0.028953409, 0.0097514745, -0.053594608, 0.02445919, 0.057570804, -0.018606907, -0.010801526, 0.04415815, 0.05488267, 0.017346844, -0.026167274, 0.019754961, -0.005978291, 0.006146299, 0.0102695, -0.018746912, -0.0069793398, -0.004588723, -0.027665347, -0.0031484033, 0.0027633845, -0.029849453, -0.009002439, -0.007308356, -0.007623371, -0.008162397, -0.017192837, -0.027217325, 0.008554417, -0.013405653, -0.010185496, 0.0039131907, -0.014546708, -0.002894641, 0.032705594, -0.004823235, -0.022779109, 0.0097514745, -0.033349622, 0.008113395, -0.020847015, -0.021827063, -0.02035699, 0.06322708, 0.008897433, 0.0033654138, -0.0082184, 0.019208936, -0.040349964, -0.0002612002, 0.005029745, -0.052922577, -0.0028106368, -0.008183398, -0.03326562, -0.034021657, 0.015064733, 0.0026006266, 0.066811256, -0.024921214, -0.047126293, 0.014315697, -0.027553342, 0.019754961, -0.0046762275, -0.013832673, 0.027847355, -0.035589732, 0.01703883, 0.044298157, -0.021827063, -0.009170447, 0.038949896, -0.0019565953, 0.02979345, 0.03466569, -0.008225401, -0.05530269, 0.06692326, 0.028519388, -0.015666762, -0.0027861355, 0.046930283, -0.022737106, -0.016394798, -0.015232742, 0.0246272, -0.03878189, -0.01726284, -0.012950631, -0.0056107733, 0.03962193, -0.014840723, 0.03673779, 0.008939435, 0.0044732178, -0.025327234, 0.015876774, -0.016618809, 0.048610367, -0.046538267, 0.017010828, 0.0554147, -0.018858919, 0.040797986, -0.05012244, -0.027063318, -0.0656352, 0.0032971606, -0.044270154, -0.0015715765, -0.029121418, 0.0063388087, -0.016940825, 0.027833356, 0.023297135, 0.004784733, 0.028155372, 0.067651294, -0.0065453188, 0.01171157, -0.0014219442, 0.026335282, -0.015974779, 0.031529535, 0.020062977, -0.032789595, -0.021197032, 0.027189324, -0.030213472, 0.02154705, 0.023465142, 0.022219082, 0.014868724, 0.020076977, -0.020875016, 9.5106196e-05, -0.009849479, 0.0016179538, -0.00636681, 0.035449725, -0.0738676, -0.021925068, -0.012964631, -0.006653824, -0.014672714, 0.0067308275, -0.0022838612, 0.020272987, 0.048974384, 0.033517633, -0.041414015, -0.020202983, 0.01968496, -0.020819014, -0.03878189, -0.021995071, 0.055162687, 0.0021893566, -0.007042343, -0.02423518, 0.011725571, 0.011263548, 0.034161665, 0.030773498, 0.02917742, 0.058466848, -0.04127401, -0.04457817, -0.041414015, 0.007028342, -0.0019215936, -0.001087678, -0.03015747, -0.02857539, 0.021729058, -0.014896725, -0.025621247, -0.018018877, -0.024585197, 0.017220838, 0.044046145, 0.026965313, -0.03799785, 0.01929294, 7.6784985e-05, -0.02339514, -0.028211374, 0.0015882023, -0.0066818255, 0.01110254, 0.045250203, 0.02034299, 0.0018673409, 0.014126688, 0.027889358, 0.0012766871, -0.018214887, 0.022667104, -0.0677633, 0.020525, 0.012586613, 0.019964973, -0.012838625, 0.007518366, 0.067035265, -0.07050743, -0.047378305, 0.0005823409, -0.008106395, 0.005852285, -0.032537583, 0.050654467, -0.013132639, 0.0067063267, -0.062499043, -0.03393765, -0.02174306, 0.054994676, 0.043038096, -0.017822867, -0.02753934, 0.026965313, 0.0133146485, -0.023115125, 0.014105687, 0.0040986994, -0.0164088, -0.04455017, -0.011844576, -0.02976545, -0.012887628, -0.016352797, -0.03656978, 0.037549827, -0.02319913, 0.0037661833, -0.06479515, 0.014840723, -0.01087153, -0.041862037, -0.041386016, 0.024361186, 0.0059817913, 0.055134684, 0.013811672, 0.022345088, -0.05891487, -0.036457773, 0.047910333, 0.057850815, -0.0018708411, -0.017626857, 0.021799061, 0.05255856, 0.006076296, 0.0126426155, 0.0067938305, -0.009415458, 0.020090979, 0.038109854, -0.04062998, 0.02959744, -0.04539021, 0.00021055713, -0.0065768203, -0.022023072, -0.034049656, -0.040125955, -0.036233764, -0.003920191, 0.13395852, -0.027049316, -0.038305864, -0.010171495, -0.013216644, -0.0045677223, 0.010045489, -5.4225296e-05, -0.016954826, 0.009807478, -0.045502216, -0.018018877, -0.034637686, 0.03595375, -0.021085026, -0.0030363977, 0.012054587, -0.024697203, 0.057570804, 0.024837209, 0.015526756, -0.028869405, 0.011522561, -0.021799061, 0.019348942, -0.031865552, 0.0025166224, -0.011823576, -0.01458871, -0.010150494, -0.010633518, -0.01089253, 0.05239055, -0.006328308, -0.011557562, -0.013636664, -0.024879212, -0.0048827375, -0.02220508, 0.01908293, -0.0003441105, -0.009807478, -0.064123124, -0.029429432, -8.919965e-05, -0.024809208, -0.02753934, 0.0184949, -0.005873286, -0.023717154, 0.022877114, -0.03144553, 0.023451142, -0.0032971606, 0.0013046885, 0.056982774, -0.015554757, 0.026587294, -0.030297475, -0.023325136, 0.020062977, 0.0013816922, 0.022135077, -0.0021491046, 0.012922629, 0.02937343, -0.020567002, 0.028463386, -0.011074539, -0.0048617367, 0.0073643588, 0.04256207, -0.037325818, -0.0067868303, -0.007784379, 0.011844576, 0.03757783, 0.07274754, 0.00924045, -0.044018142, 0.035617735, 0.03267759, 0.020496998, 0.0369898, 0.0055407695, -0.039957944, 0.0023538645, 0.002567375, 0.028841404, -0.003717181, 0.021925068, -0.02996146, 0.008057392, -0.01723484, -0.022849113, -0.015162738, 0.0054777665, 0.012901628, 0.005708778, -0.039005898, 0.07375559, -0.008526416, 0.010437508, -0.0042247055, 0.02690931, 0.030689495, 0.022415092, 0.031585537, -0.037017804, -0.0040986994, 0.0052957577, 0.02940143, 0.0082044, -0.018018877, 0.009954485, 0.015652763, -0.007966388, -0.0064613144, -0.0075323666, 0.041162003, 0.031669542, -0.024893211, 0.03144553, 0.03248158, -0.009632469, -0.0102625, -0.053734615, 0.010185496, -0.03393765, -0.008806429, -0.011984584, -0.004914239, 0.033545632, 0.0055757714, -0.012264597, 0.009548465, -0.043850135, -0.006328308, -0.00015116361, -0.02363315, 0.01892892, -0.0007984764, -0.017094832, 0.01825689, 0.028267376, -0.037353817, 0.0246272, 0.015638761, -0.043206103, -0.02937343, -5.8354402e-05, -0.030213472, 0.060482945, 0.0009887981, -0.019376943, 0.022695106, 0.0019635956, 0.02178506, 0.011361553, -0.021029023, -0.03455368, 0.025705252, -0.0009887981, -0.018732913, -0.006804331, -0.0003718931, -0.03777384, 0.00047296053, 0.013489657, -0.0050157444, -0.06266705, -0.016282793, 0.01622679, -0.03452568, 0.011851577, 0.0068288324, 0.005831284, -0.013195642, 0.019754961, 0.012376603, 0.06401112, -0.028169371, 0.04746231, 0.017108833, 0.002997896, -0.0050437455, 0.042590074, -0.0011436807, -0.0070108413, 0.024137175, -0.012012585, -0.035281718, -0.032649588, -0.018116882, -0.021393042, -0.015456753, 0.014063684, 0.0009152946, -0.02363315, 0.014840723, -0.018760914, 0.053902626, 0.025551245, 0.013671665, -0.02857539, -0.0349457, -0.017682862, 0.04964642, -0.011928581, -0.022275085, 0.02814137, 0.011844576, 0.037073806, -0.01724884, 0.053118587, 0.010164495, 0.021645054, -0.0024886213, -0.030493485, -0.009688471, 0.019628955, 0.009898482, 0.009429459, 0.033321623, 0.006261805, -0.02363315, -0.015246742, 0.0018585905, -0.0014893225, -0.014574709, -0.010094492, 0.0060972967, -0.021757059, -0.021197032, -0.03819386, -0.029289426, 0.0025848758, -0.021225033, 0.012887628, 0.010248499, -0.029513437, -0.010241498, 0.0070073414, -0.011935581, -0.01376267, -0.0046657273, 0.019068928, -0.020412995, -0.011564563, -0.017738864, -0.012243596, 0.019950971, -0.017906873, -0.035169713, 0.0013571911, 0.0054672663, 0.02237309, -0.009282452, -0.011368553, -0.01808888, 0.037297815, 0.016268792, -0.011340552, 0.03208956, -0.023115125, -0.023227131, 0.011473559, 0.0010273, 0.034217667, -0.037353817, -0.04329011, 0.029625442, -0.025901262, 0.0010920531, 0.030465484, 0.004497719, -0.0052922578, -0.017052831, 0.03225757, -0.014420702, -0.0063528093, -0.032929603, -0.0007958512, 0.014203692, 0.015358748, -0.0151487375, -0.018214887, 0.0184669, -0.023493145, 0.052726567, -0.01129155, -0.019978972, -0.005778781, -0.040965993, 0.03816586, -0.011445558, -0.0029208923, -0.005642275, -0.030045463, -0.015428751, -0.011690569, 0.005792782, 0.005498768, 0.015918775, -0.03346163, -0.070955455, -0.0031204019, 0.050430454, -0.042646077, -0.014105687, 0.005173252, -0.031949557, -0.03225757, -0.021127028, 0.044438165, -0.030633492, -0.00431921, 0.023493145, -0.012306599, -0.011662568, -0.03572974, 0.019838966, 0.025775256, -0.050374452, -0.03228557, -0.02280711, 0.045894235, -0.0058662854, 0.012796623, -0.0070178416, 0.01129155, 0.00019808777, -0.018676909, 0.02363315, 0.040405966, 0.019880967, -0.019236937, -0.0038431871, -0.006842833, 0.011564563, 0.0041162004, -0.004616725, 0.0130906375, -0.005176752, 0.017556855, 0.0035649235, -0.0026548794, 0.015764767, 0.00021558862, -0.0035036707, 0.013055636, -0.009429459, 0.00924045, -0.028701397, 0.0041197008, -0.0041022, 0.062219027, -0.0057402793, 0.014133688, 0.0011358053, -0.009156446, -0.022065075, 0.013230644, -0.0048897383, 0.0047007287, -0.021897066, 0.016352797, 0.021393042, 0.023045123, 0.00555127, 0.00025026218, 0.032901604, -0.020104978, 0.019236937, 0.006454314, -0.022149079, 0.0059047877, 0.019502949, -0.021057025, 0.011466558, -0.006590821, -0.010612517, 0.044018142, 0.00079366367, 0.061098974, 0.008533415, -0.03015747, 0.016590808, 0.01664681, 0.00032748468, 0.00028307628, 0.013503658, 0.03430167, 0.010724522, 0.0492544, 0.022779109, -0.002695131, 0.016758816, -0.031501535, -0.04357012, -0.025747254, 0.009597467, 0.043626126, -0.038109854, 0.005257256, 0.041218005, 0.005274757, 0.0008912309, 0.020553, -0.018816916, 0.044494167, -0.016072782, 0.022737106, -0.016520804, -0.019068928, -0.0051242495, -0.022989118, -0.019852966, -0.0009835479, 0.02549524, -0.013426654, -0.01334265, -0.0014149438, 0.03864188, -0.012502609, 0.01108854, -0.0014411951, 0.029345429, 0.022639101, 0.02095902, 0.03785784, -0.00882743, 0.012467607, 0.04827435, -0.019852966, 0.005257256, -0.04373813, 0.016744815, -0.009555466, -0.005855785, -0.016884822, -0.00028723275, -0.0010360505, 0.037745837, -0.006713327, -0.044690177, 0.00028373257, 0.021631053, -0.00057402794, -0.0056597753, -0.027049316, 0.010423508, 0.01458871, -0.0034354173, 0.00534476, -0.016506804, -0.02238709, -0.0077353767, -0.016072782, 0.0107455235, -0.0036296768, -0.0075463676, 0.0153727485, 0.006713327, -0.0011848077, 0.025831258, 0.017024828, 0.022695106, -0.021659054, -0.014574709, -0.013916678, 0.020230984, -0.0102695, 0.050486457, 0.0011139292, -0.0029698946, 0.046958286, -0.0036261766, -0.051074486, 0.014658714, -0.016520804, -0.02504722, -0.0102625, -0.008939435, 0.0021876064, 0.020174982, 0.0016870821, 0.010017488, -0.0071578487, 0.031529535, 0.021995071, -0.007084345, 0.021799061, 0.049366403, -0.0080153905, 0.006429813, 0.020693008, -0.0014543209, -0.00964647, 0.01128455, -0.017738864, 0.002485121, 0.015764767, 0.04886238, 0.013545659, 0.039481923, -0.01807488, -0.04824635, 0.012901628, 0.0014306947, -0.018354893, 0.0057647806, 0.033545632, -0.03018547, 0.020553, 0.014224692, -0.0068708346, -0.017948873, 0.010192497, 0.014896725, -0.015708765, 0.0013781921, -0.00087810523, -0.0004913364, 0.0021928567, 0.020595003, -0.03365764, -0.015232742, -0.06507517, -0.039397918, -0.015512755, 0.017640859, -0.048610367, 0.026419286, -0.0098144775, -0.010941532, -0.02014698, 0.041386016, 0.013706667, -0.014546708, -0.0037486826, -0.0044872183, -0.036233764, 0.017920872, 0.020847015, -0.017192837, 0.01829889, 0.010164495, -0.025523243, -0.015274744, 0.042338062, 0.0030959006, 0.010045489, 0.004616725, -0.028603392, -0.011592564, -0.0020003475, 0.0067833303, 0.03452568, 0.04533421, -0.023717154, -0.0017413348, 0.015204741, 0.020721009, 0.021701057, -0.015526756, 0.017724862, 0.0102765, 0.002997896, -0.011081539, 0.02790336, -0.01355266, 0.00054121384, 0.00698634, 0.010836528, -0.030381478, -0.007210351, 0.0047357306, -0.0044207154, -0.0031956555, -0.007427362, 0.008540416, 0.026461288, 0.0011253048, 0.024935214, 0.0082184, -0.020973021, 0.006814832, 0.0012390603, 0.014672714, -0.0004443029, 0.0018183385, -0.036485776, -0.02035699, 0.001313439, -0.024207179, -0.010549514, 0.0026548794, 0.008701424, 0.014077686, 0.014322697, 0.016926825, -0.00923345, 0.039537925, 0.0128666265, -0.012404604, -0.019502949, 0.030549487, 0.0054392647, -0.025957264, 0.024291182, -0.02198107, 0.0009301703, 0.004739231, -0.018914921, -0.022443093, 0.007987389, 0.005355261, 0.0225831, 0.016576808, 0.005642275, -0.0034756693, 0.0020213483, -0.016058782, -0.00097479747, 0.020595003, -0.0011751822, 0.011319551, -0.0061708004, -0.036065757, -0.017402848, 0.02835138, -0.021589052, -0.025327234, -0.0389779, 0.021631053, 0.0124816075, 0.043178104, 0.028239375, -0.020777011, 0.013125639, 0.032061562, 0.00016713314, -0.006468315, 0.01047251, -0.011151543, -0.025075221, 0.02707732, -0.026013266, -0.015862772, -0.03514171, -0.01972696, -0.008162397, 0.016254792, -0.0103605045, -0.005505768, -0.0063773105, 0.022709105, 0.009562465, 0.031109514, -0.0004079886, 0.029905455, 0.10959733, 0.004000695, 0.023451142, 0.00802239, 0.0004681478, -0.014868724, -0.016786817, 0.003181655, -0.020104978, -0.008841431, 0.00064403133, 0.044074144, 0.020763012, -0.022219082, -0.0053097587, 0.007861382, -0.008169398, 0.013636664, 0.0075953696, -0.0075253663, 0.032649588, 0.008575417, 0.0043612123, 0.022471095, -0.050794475, -0.011053538, -0.008162397, 0.013846674, -0.00031545287, 0.044858184, -0.01932094, 0.033713643, -0.019698959, 0.018760914, 0.01580677, 0.0128666265, -0.05295058, 0.029289426, 0.022149079, -0.0135736605, 0.015736766, -0.015092735, -0.009793477, -0.029009413, -0.016492803, -0.040125955, -0.0010999285, -0.019026926, -0.004588723, -0.008582418, -0.00016768005, 0.010822527, -0.023899164, 0.0032936605, 0.0430941, 0.024207179, 0.021029023, 0.043234106, 0.040601976, -0.03015747, 0.03301361, 0.006923337, -0.026125273, 0.008281403, 0.002486871, -0.03141753, -0.021561049, -0.023353137, 0.0052432553, 0.008155397, -0.012852626, 0.01932094, -0.013783671, -0.042618074, -0.0030486484, 0.008456412, 0.017682862, -0.015078735, -0.011851577, 0.019418946, -0.040993996, 0.0045292205, 0.009625468, 0.025201228, -0.03945392, 0.012341601, 0.026573293, -0.0054742666, -0.01458871, 0.03878189, -0.044494167, -0.014266695, 0.00078010047, -0.022219082, -0.0047637317, 0.0041022, -0.0058837864, -0.021393042, -0.0015978278, 0.016156787, 0.010500511, 0.018396895, -0.0030171468, 0.014329698, 0.031137517, 0.010647519, -0.027973361, 0.039201908, 0.009849479, 0.024347186, 0.031921554, -0.022849113, 0.02564925, -0.01892892, 0.00062740553, 0.01724884, 0.0069933403, 0.015134737, -0.008190399, -0.009667471, 0.017906873, 0.017122833, -0.0034441678, -0.034889698, 0.007742377, -0.022107076, -0.0003841437, 0.0061287982, -0.032537583, 0.0063213077, 0.039481923, 0.009408458, -0.0045852233, -0.0053727617, 0.01909693, 0.01703883, 0.010486511, -0.013258645, -0.012341601, 0.016128786, 0.006093797, 0.020987023, -0.011319551, -0.009478462, 0.018606907, -0.0072523532, 0.007623371, 0.016688813, -0.010339503, 0.00072059757, -0.016940825, -0.031669542, 0.015960777, -0.003881689, 0.040937994, -0.017640859, 0.011977583, 0.00013606912, -0.01274762, -0.010206497, -0.021393042, -0.0072803544, -0.041582026, -0.0048092343, 0.026349284, 0.0021053525, 0.013594662, 0.03620576, 0.018046878, 0.03427367, 0.036457773, -0.033797644, 0.0073503577, 0.00024151176, 0.008792428, 0.0029313928, -0.008043392, 0.009219449, -0.06865934, -0.018172884, 0.005299258, -0.008092394, -0.021939069, 0.025621247, 0.009128445, -0.03864188, 0.021393042, -0.03102551, 0.0067728297, 0.018998925, 0.006247804, -0.03612176, -0.032117564, 0.018396895, -0.012061588, -0.009555466, -0.018508902, -0.04236606, -0.003958693, 0.028365381, -0.0041127005, 0.008575417, 0.0021823563, 0.0018463399, 0.030101465, 0.079747885, -0.036681786, -0.001602203, -0.01829889, -0.0025411237, 0.025383236, -0.008848431, 0.014952728, 0.008330406, 0.042254057, -0.0246692, -0.00657332, -0.0013668166, 0.012754621, 0.011781573, -0.008120395, 0.020188984, 0.011116541, -0.04065798, -0.044074144, -0.008512414, 0.0151487375, 0.0037941847, -0.013930678, -0.011865578, -0.06020293, -0.029037414, -0.023129126, 0.0058172834, -0.00029729572, -0.020777011, -0.031865552, 0.013874675, 0.03060549, -0.0022033572, -0.0073853596, -0.026573293, 0.0006099047, 0.06401112, -0.0024781206, 0.012446606, 0.0059082876, 0.029653443, -0.022513095, -0.018564904, -0.005789282, 0.0095904665, -0.008988437, -0.034021657, -0.037213814, 0.007150848, 0.0011384304, -0.014294696, -0.0184809, -0.007434362, -0.03679379, 0.017416848, 0.03945392, -0.0027913859, 0.009394458, -0.020244986, 0.016534805, 0.0022086075, -0.012684617, 0.011991584, -0.009373456, -0.029037414, -0.014532708, -0.018970923, 0.009625468, 0.012026586, 0.015078735, -0.01622679, -0.009422459, -0.05264256, 0.010661519, 0.008967437, -0.0061427993, 0.020300988, -0.0080783935, -0.014462704, 0.0072523532, -0.0074833645, 0.011487559, -0.015932776, 0.012005584, -0.025313232, -0.04270208, -0.009359456, -0.02237309, 0.025607247, 0.016688813, -0.04048997, -0.027763352, -0.0097164735, -0.029261425, 0.023871163, -0.00045589719, -0.020735009, -0.00067290774, -0.004784733, -0.002957644, 0.023661152, -0.004350712, 0.048582364, -0.056450747, 0.0021911068, -0.00016253916, -0.02360515, 0.015666762, 0.015582759, -0.044326156, 0.009961485, 0.005155751, 0.026363283, -0.0064228126, 0.020721009, -0.019362943, 0.015470753, 0.016688813, -0.01727684, 0.03514171, -0.007742377, 0.018802915, 0.02360515, -0.0040776986, -0.011081539, 0.023717154, 0.00039901942, 0.02854739, 0.009499462, -0.02548124, 0.022009071, -0.00904444, -0.05569471, -0.021225033, 0.023591148, -0.015708765, -0.01806088, -0.014532708, 0.00013595975, -0.011627566, -0.045642223, 0.0010981784, -0.004063698, 0.0063213077, 0.008295404, 0.04295409, -0.016422799, -0.01684282, 0.03348963, 0.021799061, -0.008589419, 0.005687777, -0.05561071, -0.013503658, -0.0052957577, 0.041162003, -0.007966388, -0.016520804, -0.0073223566, -0.009009439, -0.015960777, -0.012019585, -0.025411237, -0.007336357, -0.007644372, -0.013650665, -0.0056947772, -0.012453606, -0.026461288, -0.03575774, -0.012817624, -0.032845598, -0.05608673, 0.041162003, 0.018522901, 0.015260743, 0.013188642, 0.032901604, 0.0043682125, 0.005869786, -0.016926825, -0.00677983 ] } ], "model": "text-embedding-3-small", "usage": { "prompt_tokens": 9, "total_tokens": 9 } } headers: CF-RAY: - 939c172698e2d766-NRT Connection: - keep-alive Content-Type: - application/json Date: - Sat, 03 May 2025 01:47:27 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-allow-origin: - '*' access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '33284' openai-model: - text-embedding-3-small openai-organization: test_openai_org_id openai-processing-ms: - '193' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload via: - envoy-router-7c84bdfdfc-tg6kf x-envoy-upstream-service-time: - '82' x-ratelimit-limit-requests: - '5000' x-ratelimit-limit-tokens: - '5000000' x-ratelimit-remaining-requests: - '4999' x-ratelimit-remaining-tokens: - '4999987' x-ratelimit-reset-requests: - 12ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_3c8398f41e61a12a22b4e7b12b9772e0 status: code: 200 message: OK version: 1 test_async_embeddings_with_batch_input.yaml000066400000000000000000004177411527070072300477570ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": [ "This is the first test string for async embeddings", "This is the second test string for async embeddings", "This is the third test string for async embeddings" ], "model": "text-embedding-3-small" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '209' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - Linux x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.9.2 method: POST uri: https://api.openai.com/v1/embeddings response: body: string: |- { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": [ 0.00102203, -0.03870587, -0.0064009707, -0.020090546, -0.018015232, 0.0006766651, 0.048107296, 0.029454468, 0.013952116, 0.0060102865, 0.018965377, -0.03618049, -0.00659475, -0.047907267, -0.028029252, 0.0143771805, -0.022240873, -0.003856835, -0.009220148, -0.0035942951, -0.00092435896, -0.03533036, -0.003381763, 0.05050766, 0.015202305, -0.036955606, -0.0030395235, 0.04015609, 0.016602518, -0.024378696, -0.020215565, -0.04628202, 0.022990987, -0.043356575, 0.008395024, 0.07651161, -0.008920102, 0.0092263995, -0.045156848, -0.01655251, 0.0004289713, -0.027729206, -0.017627673, 0.0023081626, -0.012633166, 0.041206248, -0.016827552, 0.016865058, 0.015839903, 0.013864603, -0.06641007, -0.01127046, -0.0022456532, 0.050882716, -0.032129873, -0.012789439, -0.009395175, 0.03718064, -0.02247841, -0.0031301621, 0.050007585, -0.017052587, -0.039255954, -0.028329296, 0.0042037624, 0.0073698675, -0.01852781, 0.034905292, 0.0062196935, 0.030904686, 0.05155782, -0.0003662665, -0.021940827, 0.00797621, -0.020403095, -0.004372538, 0.003672432, 0.008926353, -0.0040974966, -0.039355967, -0.031079713, 0.017452646, -0.03475527, -0.03765571, -0.022190865, 0.014727234, -0.0665601, -0.010182794, -0.0017721438, 0.012551904, -0.035380367, -0.0665601, -0.047957275, -0.032679956, 0.02827929, 0.008094978, -0.024416203, -0.048757393, -0.023166014, 0.0028238657, 0.007994963, 0.034730267, -0.034305204, 0.0053039296, -0.0059008948, -0.031179728, 0.0045944466, -0.020678136, -0.03700561, -0.022453405, -0.063259594, -0.011751782, -0.014552207, 0.0016627521, 0.048557363, -0.0031582916, -0.0171401, -0.0048163556, -0.011570505, -0.03338006, -0.013014474, -0.00392247, 0.046857107, 0.009413928, -0.04470678, -0.058008797, -0.03520534, -0.066660106, -0.0692105, 0.014764739, 0.020015536, -0.026353996, 0.039556, 0.046857107, -0.0639597, -0.0063665905, -0.07441129, 0.021715794, -0.0024910027, 0.0063540884, -0.0191154, -0.069060475, 0.01463972, -0.011776786, -0.020053042, -0.029004399, -0.025828917, 0.032955, -0.00014084167, 0.00074112805, -0.036780577, -0.034105174, -0.042556453, 0.059709057, -0.0046163253, -0.027654193, -0.030979699, -0.022528417, 0.012183098, 0.0036005462, -0.014164648, 0.0048163556, 0.027529174, -0.010845395, -0.009482688, 0.010551601, 0.013689576, -0.015102291, -0.024091154, -0.06646008, 0.0004395198, -0.039505992, -0.06716019, -0.006172811, -0.016039932, -0.004628827, 0.024041146, -0.059459016, -0.0006958087, 0.0035849188, 0.026454013, 0.045256864, 0.026429009, -0.00995151, -0.045681927, 0.049632527, 0.02135324, 0.049882565, 0.009926505, -0.0026394627, 0.010082779, 0.03290499, 0.059258986, -0.023866119, 0.026954088, -0.019465452, 0.041156244, -0.010095281, 0.034505233, -0.018102745, 0.017440146, 0.044081684, -0.012389379, 0.0027191625, 0.027604187, -0.02116571, -0.01754016, -0.04160631, -0.014789743, 0.02071564, 0.044931814, 0.00071807764, -0.017465148, -0.012064329, -0.03970602, 0.009551449, -0.019915521, 0.019440448, 0.036255497, -0.041881353, -0.016252466, 0.03543037, 0.04713215, 0.037930753, 0.0016705658, -0.0055164616, -0.012826946, 0.0049882564, 0.010214049, -0.022778455, 0.001443969, -0.03355509, -0.008457533, -0.03040461, 0.019002883, -0.001310355, -0.0008649749, 0.014864754, -0.023353541, 0.016014928, -0.035830434, -0.052207917, 0.015314823, -0.0008704445, 0.02182831, 0.01997803, -0.009263905, -0.033080015, 0.04648205, -0.012495645, -0.014002124, 0.040906202, -0.007907449, 0.013402033, -0.036930602, 0.004444424, -0.030604642, 0.039931055, 0.015877407, 0.012076831, 0.0070448183, 0.05155782, -0.017890213, 0.034780275, 0.0070448183, -0.04213139, 0.017677682, -0.0037880745, 0.013439538, -0.03600546, -0.017990228, -0.01134547, 0.052057896, -0.02815427, -0.052908026, 0.019552965, -0.030829675, -0.019140404, -0.013339523, 0.025528872, -0.01985301, -0.044356726, -0.0016861933, 0.014052131, -0.0092326505, 0.01207058, 0.031879835, -0.044931814, -0.002014368, -0.0056602336, -0.007969959, -0.039781034, 0.05465829, 0.019627977, 0.011970566, 0.0071510845, 0.044281717, -0.008251252, 0.019702988, -0.002211273, 0.0421814, -0.046257015, 0.03770572, 0.032529935, -0.018765345, 0.05400819, -0.027254133, 0.027579183, 0.034330208, 0.010070277, -0.004531937, 0.007669913, -0.029604489, 0.015139796, -0.05690863, 0.025628887, 0.03660555, -0.014452192, 0.030829675, -0.045306873, -0.02782922, -0.02287847, -0.0010861022, -0.034355212, 0.0019159155, -0.017465148, 0.012289364, -0.04193136, 0.012789439, 0.03210487, -0.020415597, 0.008463783, 0.041256256, -0.018552814, 0.008826339, -0.018940372, 0.006607252, -0.04390666, 0.032980002, 0.013239508, -0.05975906, -0.0244037, 0.04165632, -0.0125269, 0.0064509786, 0.04200637, 0.009776482, 0.009438932, 0.046006978, 0.017515156, 0.0054039448, 0.0129519645, -0.008557548, -0.010851646, 0.07051069, -0.08031218, -0.017202608, 0.0064447275, -0.021328235, -0.016239963, 0.03938097, -0.016102443, -0.01371458, 0.04998258, -0.0048101046, -0.0494575, 0.0020503108, 0.04405668, -0.013339523, -0.03205486, -0.012295615, 0.033880137, 0.023728598, 0.0026988469, -0.019990532, 0.024916278, -0.033480078, 0.024628736, 0.05445826, 0.045331877, 0.029029403, -0.04333157, -0.053208068, -0.050632678, -0.017027583, -0.0004465521, -0.03455524, -0.009620209, 0.0053508114, -0.0003824799, 0.00810748, -0.0120955845, -0.013239508, 0.0021065695, 0.016765043, 0.045506902, 0.03568041, -0.005425823, 0.020078044, -0.024053648, 0.022715945, -0.044406734, -0.019240418, 0.0060759215, 0.010532848, 0.01819026, 0.015539857, 0.0019893642, 0.033455074, 0.0136645725, 0.013827097, 0.017752692, 0.032079864, -0.031304747, -0.006844788, 0.03565541, 0.019152904, -0.047882263, 0.007076073, 0.074461296, -0.05460828, -0.05760874, -0.044481747, -0.0077261715, 0.014614716, 0.032479927, 0.043406583, -0.0047163405, -0.0046507055, -0.042581458, -0.040831193, -0.016827552, 0.06290954, 0.023953633, -0.033730116, -0.04443174, 0.004569443, 0.04095621, -0.040506143, -0.003102033, 0.0059040203, 0.02479126, -0.060109116, -0.010401578, 0.00031645424, -0.0019862386, -0.022978485, 0.0025081928, -0.009726475, -0.0029020025, -0.014002124, -0.065309905, 0.0052851764, -0.036055468, 0.00540707, -0.042406432, -0.006719769, 0.009488939, 0.057008646, 0.008788832, -0.0092263995, -0.05975906, -0.054958336, 0.02432869, 0.057658743, -0.007307358, -0.028179273, 0.016264968, 0.049557514, -0.017027583, -0.025216324, 0.038930904, -0.020428099, 0.03072966, 0.037155636, -0.010464087, 0.010539099, -0.01760267, 0.005622728, -0.017102594, -0.013889606, 0.0047257165, -0.041106235, -0.01345204, -0.028504323, 0.14012125, -0.031129722, -0.029154422, 0.0018784099, -0.020815657, -0.024378696, -0.00016272, -0.03638052, -0.013552056, 0.0349553, -0.020153057, 0.0071573355, -0.0751614, 0.015039781, -0.038805887, -0.0005387536, 0.021153208, -0.0027707326, 0.021103201, 0.02432869, -0.0049788803, -0.01345204, 0.013039477, -0.00036900127, -0.011420482, -0.00071612425, -0.007532392, 0.0029973297, -0.0171401, -0.003963101, 0.011132939, -0.0191154, 0.039405975, -0.001443969, -0.06485984, -0.0018252769, -0.017715186, -0.0012962903, -0.034180183, -0.0059571536, -0.011845547, -0.0064759823, -0.045156848, -0.025753906, -0.014389683, -0.008882597, -0.018365284, 0.035580397, -0.037155636, -0.036430527, -0.007826187, -0.008332513, 0.011564254, 0.03125474, -0.002067501, 0.027879229, 0.010589106, 0.04628202, -0.023291033, -0.029554483, 0.033680107, -0.0008266879, 0.037105627, -0.0029613867, 0.028604338, 0.03052963, -0.014764739, 0.009851494, -0.015664876, 0.0036693064, 0.007088575, 0.04378164, -0.026754057, 0.0011064179, -0.019090395, 0.031504776, 0.016490001, 0.029554483, 0.036405522, -0.050157607, 0.03673057, 0.009857745, 0.005135154, 0.03528035, -0.0077886814, -0.033155028, 0.00600091, 0.023953633, 0.030454619, 0.013227006, -0.001651813, -0.030604642, 0.04860737, -0.053358093, -0.028754361, 0.006150933, -0.006982309, 0.016152449, 0.015539857, -0.01695257, 0.05038264, 0.012864451, -0.043806642, -0.004878865, 0.011326718, 0.055808462, 0.007813685, 0.024553724, -0.022565922, 0.010039022, -0.011364223, 0.040731177, -0.024828766, -0.01985301, 0.012126839, 0.011989319, 0.01568988, 0.0066635106, 0.0076074037, 0.048207313, 0.021190714, -0.025103807, 0.013314519, 0.0468071, 0.00040943708, -0.017915217, -0.015002275, 0.0029660747, -0.018740343, -0.00048249506, -0.009026369, -0.005750872, 0.009126384, 0.008951358, -0.007888696, 0.015939917, -0.04978255, 0.00629783, 0.018215263, -0.02070314, 0.016277468, -0.0060384157, -0.013677075, 0.013277014, 0.020453101, -0.008213745, 0.010926657, -0.0025691397, -0.034980305, -0.02980452, -0.016777545, -0.010039022, 0.026754057, 0.013227006, -0.030154573, -0.0027144742, -0.01272693, 0.02082816, -0.0034255194, -0.027204126, -0.022328386, 0.011501744, -0.0026066452, -0.0061478075, 0.004641329, 0.018465301, -0.018202761, 0.026353996, -0.00890135, 0.01832778, -0.045456894, -0.01707759, -0.0042881505, -0.015039781, -0.020215565, 0.020890668, 0.0151648, -0.008463783, 0.009188893, -0.00016838491, 0.053058047, -0.038805887, 0.024166165, -0.00280355, -0.016677529, 0.018040236, 0.022865968, 0.04048114, -0.012914458, 0.01872784, -0.055558424, -0.036580548, -0.015127294, 0.023841117, -0.0028082384, 0.0024394325, 0.009238902, 0.0010509406, -0.041906357, 0.039606005, -0.0064759823, 0.062009405, 0.0028598087, -0.0050945226, -0.025728902, -0.04273148, -0.05260798, 0.017265119, 0.01819026, 0.0060977996, 0.031729814, 0.011220451, 0.017727688, -0.024566226, 0.04538188, 0.004919496, 0.016602518, 0.010039022, -0.025903929, -0.0045006825, -0.008001214, 0.020003034, 0.01542734, 0.030679652, 0.009838992, -0.010951661, -0.057458714, 0.00909513, 0.028229281, -0.0016908814, -0.015839903, 0.006632256, 0.0019768623, -0.0415313, -0.048257317, -0.034655254, 0.0041287513, 0.005860264, -0.013702078, -0.0087950835, -0.07721171, -0.0047444697, 0.0108016385, -0.0052914275, 0.012289364, 0.0025925806, 0.031729814, -0.030354604, 0.0069260504, -0.041756332, -0.009588954, 0.0044006673, -0.024778757, -0.03145477, 0.002156577, 0.015027279, 0.014302169, 0.009863996, 0.015477347, -0.040856197, 0.042556453, 0.019865513, -0.0014095887, 0.019803002, -0.01859032, -0.033830132, 0.008882597, -0.015314823, 0.014539705, -0.045506902, -0.012508147, 0.022265878, -0.0145272035, -0.0024566227, 0.005963404, 0.0025691397, -0.021215718, -0.012295615, 0.042931512, 0.009182642, 0.0009915567, -0.005222667, -0.008482536, 0.035305355, 0.022178363, -0.003137976, -0.03638052, 0.0038693368, 0.010120285, 0.03553039, -0.0066572595, -0.020990683, 0.03355509, -0.034530237, 0.015339826, -0.022040842, -0.006138431, 0.0027551053, -0.02030308, -0.010832893, -0.031729814, -0.019652981, 0.022315884, -0.00044108252, -0.0448568, -0.053308085, 0.009876498, 0.019227916, -0.013114489, -0.0050788955, -0.008601304, -0.052407946, -0.071510844, -0.032604944, 0.004181884, -0.023991138, -0.008113731, -0.0019393567, -0.00086966314, 0.03673057, 0.0002406615, 0.01628997, 0.05215791, -0.029554483, -0.023553573, -0.012208101, 0.0316548, -0.036880594, 0.031929843, -0.035555393, -0.0020878166, -0.021028189, -0.022315884, 0.02551637, 0.039881047, 0.02189082, -0.027404156, -0.009326414, 0.0065509933, 0.0066510085, -0.00968897, -0.02269094, -0.019690486, -0.01774019, 0.019615475, -0.0055289636, -0.0047507207, -0.004000607, 0.011889303, -0.015377332, 0.023616081, -0.031879835, 0.0008477848, -0.034105174, 0.006157184, -0.013126991, 0.05043265, 0.019502958, 0.018702837, 0.034505233, 0.0019237292, -0.04848235, 0.017227612, 0.019928021, 0.016640024, -0.018615324, -0.011970566, 0.01740264, -0.012414383, 0.011864299, 0.023153512, 0.028104262, -0.021803306, 0.026729053, -0.0030223334, 0.023753602, 0.015289819, 0.008782582, -0.02030308, 0.045356877, 0.0106641175, -0.030804671, 0.020178061, -0.018815354, 0.032279894, 0.010101532, -0.011770535, 0.011876801, 0.007351115, 0.05975906, 0.010420331, 0.02321602, 0.03528035, 0.020890668, 0.045156848, 0.026379, -0.010007768, 0.0053445604, -0.031354755, -0.04410669, -0.038930904, 0.026278986, 0.030804671, -0.018902866, -3.8555652e-05, -0.00042154832, 0.0096514635, -0.013677075, 0.0031504778, -0.017452646, 0.028754361, -0.016277468, 0.014839751, -0.0051132753, -0.027879229, 0.010757881, -0.019602973, -0.01443969, 0.013464542, 0.053208068, 0.00962646, -0.029179426, -0.009201395, 0.025753906, 0.00745113, -0.0303296, -0.032529935, 0.019127902, 0.015864907, 0.0382308, -0.0023550447, 0.0090888785, -0.029454468, 0.028654346, -0.0013087923, -0.013389531, -0.041056227, -0.009588954, -0.011076679, -0.01595242, -0.016502503, 0.012476892, 0.008257503, 0.022303382, 0.018540312, -0.061359305, 0.0048694885, -0.006185313, -0.0057414956, 0.002697284, -0.04080619, 0.011676771, 0.012189348, -0.03790575, -0.0049351235, -0.022103352, -0.029504474, 0.0092326505, -0.013026976, 0.030354604, -0.008138734, 0.0030410863, 0.017877711, 0.0178027, -0.0356054, 0.030354604, 0.021403246, -0.03790575, -0.016915066, -0.012370626, -0.025091305, 0.0005832916, -0.007219845, 0.004181884, 0.0015244499, 0.007719921, 0.035830434, 0.0047632223, -0.038455833, 0.021940827, -0.020290578, -0.016252466, -0.014327173, 0.016377484, 0.017827705, 0.02380361, 0.020628128, 0.015464845, 0.036430527, -0.0068322862, 0.026929084, 0.017440146, 0.040931206, 0.015139796, -0.0250538, -0.004331907, -0.001250971, 0.018740343, -0.021278227, -0.010032771, -0.028379304, 0.007088575, 0.0051445304, 0.041556302, -0.01463972, 0.008788832, -0.010832893, -0.033055015, 0.028904384, -0.014477195, -0.009451433, -0.0069197994, 0.048507355, -0.0342802, 0.0089388555, -0.0017252617, 0.004997633, -0.019752996, 0.013589561, 0.008957609, -0.010832893, -0.018977879, -0.018165255, -0.021390744, 0.029779516, 0.014589713, -0.012826946, 0.004510059, -0.039480988, -0.017977726, -0.019940523, -0.012170596, -0.03923095, 0.035755422, -0.018690335, -0.009051372, -0.027004095, 0.030479623, 0.0002170251, 0.00095092546, -0.00095092546, 0.0040349867, -0.02855433, 0.0049413745, 0.032154877, -0.0032754967, 0.017427644, -0.007826187, -0.015552359, -0.011051676, 0.0421814, 0.014302169, -0.0029504474, 0.00850754, -0.023303535, -0.02202834, -0.0054945834, -0.019702988, 0.021978334, 0.016464997, -0.010870399, 0.015852405, -0.00890135, 0.005103899, 0.027479168, -0.02340355, 0.01463972, 0.00823875, 0.008182491, 0.008663814, 0.017090091, 0.01113919, 0.027629191, 0.024953784, 0.008970111, -0.026053952, -0.002001866, -0.002222212, 0.0034067666, 0.00046061672, -0.0056164768, 0.005478956, 0.020353086, 0.0014697541, 0.047882263, 0.0062665753, -0.0090826275, -0.006969807, 0.01062036, -0.009782733, 0.022440903, -0.0037880745, -0.028604338, -0.03152978, -0.0050413897, -0.010389076, -0.013702078, -0.013101987, -0.02254092, 0.021965832, -0.0031145348, -0.0058196327, 0.0058915187, 0.020215565, -0.009326414, -0.03975603, -0.040006068, 0.017302625, 0.0038599605, -0.0053508114, 0.026954088, -0.015102291, -0.028029252, 0.01892787, -0.0061884387, -0.04338158, 0.011414231, -0.0035067818, 0.029579487, -0.0033661355, 0.032604944, -0.0171401, -0.002953573, 0.004003732, 0.018477803, -0.008126233, 0.0073761186, 0.007313609, -0.012964467, -0.017440146, -0.03137976, 0.017327627, -0.040781185, -0.03858085, -0.03152978, 0.0342802, 0.026328994, 0.019090395, 0.010720376, -0.017090091, -0.0051976633, 0.048632376, 0.024916278, -0.0049726292, 0.01595242, 0.0068322862, -0.0038162037, 0.005810256, -0.0283543, -0.005432074, -0.040256105, 0.013101987, 0.0024503716, 0.0045350627, -0.025466362, 0.027054103, -0.0019643605, 0.02169079, 0.010101532, 0.006100925, 0.011858049, 0.033205036, 0.06250948, 0.00771367, -0.007232347, 0.0027379151, 0.01814025, -0.028579334, -0.024416203, 0.013589561, -0.023065997, -0.02004054, 0.0025910179, 0.029779516, 0.017702686, -0.0002750417, -0.00032075177, 0.0039787283, 0.00045553784, 0.015114793, 0.006225944, -0.0061540585, -0.0047569713, 0.04095621, -0.0019924897, 0.014414686, -0.060159124, -0.005769625, -0.019552965, 0.011101684, 0.03895591, 0.024191169, -0.012939462, 0.050607674, -0.029779516, -0.0032661203, 0.0015080412, 0.014139645, -0.062359456, 0.005841511, 0.009920254, -0.00073643983, -0.013039477, -0.0069197994, -0.0037380669, -0.010345319, -0.0051257773, -0.033655103, 0.008395024, -0.00863881, 0.008144985, -0.01852781, 0.00063447125, 0.010001517, -0.019690486, 0.015002275, 0.03040461, -0.015739888, 0.015564861, 0.022065846, 0.044681776, -0.030354604, 0.017165104, 0.009970262, -0.021253223, 0.0080762245, 0.009407677, -0.029704506, -0.02097818, -0.005432074, -0.0033505082, 0.049282473, -0.020065542, 0.025128812, -0.030104566, -0.026253982, 0.009976513, 0.0074698827, 0.001363488, -0.010095281, -0.026253982, 0.029004399, -0.023828615, 0.0006942459, 0.018215263, 0.021190714, 0.00035024842, 0.007932453, -0.010757881, -0.0071885902, 0.0133770285, 0.029179426, -0.028529327, -0.02880437, 0.0042568957, -0.02221587, -0.000244373, -0.0047913515, 0.0025285084, -0.023591077, 0.004016234, -0.0067947805, 0.01964048, 0.03290499, -0.011220451, 0.0224284, 0.013126991, 0.018777847, -0.05790878, 0.034930296, -0.0027222878, 0.05215791, 0.015677378, -0.021990836, 0.026078954, -0.020553118, -0.010526597, 0.0013978683, 0.0036693064, -0.0011947125, -0.0046444545, -0.026629038, 0.0061353054, 0.0015603929, 0.017152602, -0.020428099, -0.008313761, -0.055358395, -0.006500986, 0.010239053, -0.028379304, -0.0076386584, 0.028179273, -0.005907146, -0.011095433, 0.0063665905, 0.06410972, -0.016114945, 0.017002579, -0.021378241, 0.006469731, 0.015964922, 0.017827705, 0.012314368, -0.0051914123, 0.0037411924, 0.041381277, 0.002798862, 0.015214807, 0.010507843, -0.047307175, 0.012739432, 0.0073011075, -0.0349553, 0.01173928, -0.015127294, 0.033505082, 0.0044944314, 0.0044475496, -6.95418e-05, -0.0066572595, -0.019290425, -0.010239053, -0.0032098617, -0.05550842, -0.00520704, 0.0244037, -0.00093842356, 0.00336301, 0.0037005611, -0.0021518888, 0.03488029, 0.031554785, -0.016302472, -0.012695676, 0.0077449246, -0.0023316036, -0.011889303, 0.012151843, 0.0126644205, -0.05480831, -0.024953784, -0.001238469, -0.025303837, -0.007132332, 0.0283543, -0.0054226974, -0.0044850553, 0.009307662, -0.015727386, 0.004306903, 0.008301259, -0.01595242, -0.026529023, -0.038880896, -0.007951206, -0.023378545, -0.024828766, -0.046106994, -0.056158517, 0.0080762245, 0.014289667, -0.00033071422, -0.019665483, 0.040306114, -0.0105203455, 0.01365207, 0.057658743, -0.02670405, -0.0029863904, -0.004272523, -0.008757578, 0.03040461, -0.00040513958, 0.010432832, -0.004000607, 0.037430678, -0.018465301, -0.015814899, -0.0042287665, -0.012289364, 0.018465301, 0.01048909, -0.0028738733, 0.025403854, -0.025328841, -0.030754665, 0.002686345, 0.012764436, 0.005650857, -0.012670672, -0.006072796, -0.055308387, -0.030654648, -0.014927264, -0.0059571536, 0.0032879987, -0.0073761186, -0.008207494, 0.027204126, 0.029229432, -0.014427188, -0.011951813, -0.00043834772, -0.0006270482, 0.06435976, -0.014402185, 0.017490152, -0.0015650811, 0.02519132, -0.005728994, 0.010026521, 0.01200182, 0.056958638, -0.0015978985, -0.00982024, -0.025578879, 0.0055227126, -0.0068822936, 0.0055445908, -0.009526445, -0.018340282, -0.025416354, 0.0087950835, 0.038005766, -0.0010235928, 0.0036286754, -0.03217988, 0.031479776, 0.008913852, -0.023841117, 0.022365892, 0.017827705, -0.013352025, -0.025728902, -0.0043819146, -0.018227765, 0.011076679, 0.013477044, -0.014414686, -0.009370171, -0.033080015, -0.0030379607, -0.0053195567, 0.0054570776, 0.007176088, 0.017965226, -0.0038068274, -0.001280663, -0.011120437, -0.009370171, -0.033180032, -0.0069260504, -0.019440448, -0.0421814, 0.0029879531, -0.007994963, 0.014977272, 0.012733181, 0.0077386736, -0.032204885, -0.010157791, -0.024366196, 0.014402185, 0.0046069487, -0.020453101, -0.010645364, 0.014064633, -0.016852556, 0.018227765, 0.0050413897, 0.029604489, -0.042956516, -0.0021581398, -0.0053695645, -0.0150897885, 0.008445031, 0.002294098, -0.026353996, 0.02735415, -0.009788984, 0.009582703, -0.025441358, 0.032529935, 0.0015846153, 0.033055015, -0.0035442875, -0.009751479, 0.036030464, -0.0106578665, 0.015939917, 0.043731634, -0.015714884, -0.01826527, 0.011039174, -0.008601304, 0.044406734, -0.00026957213, -0.0071885902, 0.0032286146, 0.027904231, -0.048507355, -0.013339523, 0.014864754, 0.00078683806, 0.00062900165, -0.0012204977, 0.0010360946, 0.008695069, -0.017102594, -0.0035630404, -0.02354107, 0.005141405, 0.01774019, 0.04495682, -0.0031285994, -0.01937794, 0.022490911, 0.033205036, 0.006982309, 0.018877864, -0.047682233, -0.023516066, -0.011151691, 0.041381277, -0.005810256, -0.043606613, -0.016715035, -0.011570505, -0.0013814595, 0.0031661051, -0.033605095, -0.03738067, 0.02202834, 1.6335485e-05, -0.007807434, -0.009601456, -0.031104717, -0.048057288, -0.026103958, -0.029104413, -0.05550842, 0.01971549, 0.0021159458, -0.013752086, 0.009326414, 0.014914762, 0.0031504778, 0.01002027, -0.020353086, -0.017977726 ] }, { "object": "embedding", "index": 1, "embedding": [ 0.0016487965, -0.034404144, -0.0073315105, -0.038644362, -0.01656731, 0.013063419, 0.03709554, 0.028488155, 0.014510678, 0.00030825668, 0.0036752766, -0.039761543, -0.016021414, -0.028945185, -0.039837714, 0.016706958, -0.014840755, -0.0061318087, -0.013533143, -0.0022502341, -0.020947173, -0.037933428, 0.002612049, 0.064238, 0.006811005, -0.03422641, -0.0093690995, 0.039786935, 0.01678313, -0.022077052, -0.01852238, -0.04405254, 0.027091678, -0.035242032, 0.0027977172, 0.071550466, 0.002629505, 0.015729424, -0.035191253, -0.002869128, 0.01273969, -0.036435388, -0.02625379, 0.0005324073, 0.0027881956, 0.033921726, -0.009934038, 0.012155708, 0.018763589, 0.0025485728, -0.060023174, -0.014510678, -0.0038879223, 0.04600761, -0.025403209, -0.0026326787, -0.011895455, 0.038390454, -0.018458903, -0.02922448, 0.051111102, -0.023054587, -0.03704476, -0.026076056, -0.0031674665, 0.009165975, -0.019779209, 0.045322064, -0.004179913, 0.019474523, 0.051593523, 0.00618259, -0.03120494, 0.009077108, -0.007636197, 0.006811005, 0.014396421, 0.0050558858, 0.019525304, -0.029529167, -0.043290824, 0.018738199, -0.040853336, -0.025390513, -0.024501845, 0.010968702, -0.067487985, -0.0113749495, -0.008131312, 0.0009513508, -0.036460776, -0.051796645, -0.041361146, -0.03597836, 0.023702044, 0.01937296, -0.018382732, -0.05489429, -0.028081907, 0.012676214, 0.01859855, 0.020883696, -0.03275376, 0.019246008, 0.00484324, -0.021696193, 0.0144091165, -0.015932547, -0.03915217, -0.034531098, -0.06830048, -0.0065761427, -0.0060397685, 0.01750676, 0.06901141, -0.009711871, 0.000213439, 0.0027739136, -0.012657171, -0.028970575, -0.014193296, -0.022394432, 0.052710705, 0.016605396, -0.038644362, -0.053320076, -0.04349395, -0.073886395, -0.06489815, 0.022953024, 0.020236239, -0.019779209, 0.03620687, 0.050857197, -0.061241917, -0.0036943196, -0.08013246, 0.016288014, -0.0029326042, -0.004271954, -0.015742118, -0.069062196, 0.009737262, -0.012314399, -0.019322181, -0.020642487, -0.020299716, 0.036740072, -0.0005189186, 0.007223601, -0.04996853, -0.01862394, -0.059109114, 0.056265377, -0.03965998, -0.025999885, -0.03717171, -0.005138405, 0.01214936, 0.0070585627, -0.021683497, -0.0019487218, 0.029630728, -0.014523374, 0.0053510508, 0.011311473, 0.024870008, -0.00805514, -0.037933428, -0.07469889, 0.0036974933, -0.031001816, -0.07058562, -0.0048591094, -0.005668432, -0.0061921114, 0.013241152, -0.057839587, -0.013012638, 0.016211843, 0.016516529, 0.019322181, 0.035242032, -0.005509741, -0.03508969, 0.04506816, 0.013926696, 0.038949046, 0.021683497, 0.01089253, 0.02269912, 0.032296732, 0.07789809, -0.018001873, 0.016351491, -0.025733285, 0.029630728, -0.01565325, 0.030620959, -0.020528229, 0.017709883, 0.021175688, 0.010391068, -0.0040561343, 0.03656234, -0.009032675, -0.01249848, -0.04364629, -0.0005070168, 0.03153502, 0.04593144, 0.020071201, -0.017760664, -0.00901998, -0.03755257, 0.01734172, -0.027447145, 0.0104101105, 0.030747911, -0.044814255, -0.011565379, 0.036283042, 0.04009162, 0.03018932, -0.007877407, 0.000717282, -0.015881766, 0.01787492, 0.01153364, -0.033286963, 0.011013135, -0.024082901, -0.018154217, -0.044331837, 0.028056517, -0.011463816, -0.0058810776, 0.02231826, -0.031433456, -0.0019201576, -0.022394432, -0.040269352, 0.021645412, -0.0022454734, 0.010822706, 0.011209912, -0.0039831367, -0.039076, 0.062613, -0.03417563, -0.018560465, 0.033490088, 0.009216757, 0.004065656, -0.0420213, -0.0113686025, -0.018852456, 0.044738084, 0.010397415, 0.007185515, 0.006211154, 0.040294744, -0.018268473, 0.03529281, -0.0064428425, -0.047404088, 0.024438368, -0.0059826397, 0.019411048, -0.057991933, -0.032245953, -0.013875916, 0.044255663, -0.009248494, -0.060530983, 0.013939392, -0.040777165, -0.006950653, -0.009788043, 0.028869014, -0.014015563, -0.050171655, -0.007325163, 0.012733342, 0.008848594, 0.0074330727, 0.021086821, -0.035394374, 0.025327036, -0.0068363957, 0.0033071144, -0.045702923, 0.04070099, 0.020782135, -0.011927194, 0.0026263313, 0.046286907, -0.01432025, 0.014142516, -0.013609315, 0.036359213, -0.04260528, 0.025225475, 0.030747911, -0.023994034, 0.03272837, -0.02823425, 0.035927575, 0.027878784, 0.013545839, -0.0022073877, -0.0010394241, -0.038390454, 0.024044815, -0.052050553, 0.015564385, 0.026939334, -0.02349892, 0.027675658, -0.057839587, 0.00055422727, -0.0049606715, 0.0016051565, -0.030443225, 0.009337361, -0.004538554, 0.0041767396, -0.035851404, 0.019385656, 0.038238112, -0.021150297, 0.00856295, 0.040853336, -0.016694263, 0.0065634474, -0.014663021, 0.0026755254, -0.03448032, 0.020782135, 0.0053986576, -0.06682783, -0.021683497, 0.0380096, -0.032449074, 0.0038657056, 0.01758293, 0.0051479265, 0.021873927, 0.039177563, 0.014536069, 0.0098896045, -0.0043925587, -0.008766075, -0.01753215, 0.05738256, -0.07490201, -0.0038847486, -0.0013821961, -0.0029786245, -0.0117431125, 0.032372903, -0.00074624305, -0.015640557, 0.05494507, -0.00426878, -0.0355975, -0.025568247, 0.047810335, -0.010568801, -0.034988128, -0.024654187, 0.042960748, 0.015589775, 0.026025277, -0.0256952, 0.037349444, -0.028310422, 0.022127831, 0.049308375, 0.035318203, 0.037730303, -0.044230275, -0.053218514, -0.039888497, -0.01161616, 0.021099517, -0.034429535, -0.0057128654, 0.0026675907, 0.019525304, 0.00056493893, -0.023295796, 0.0015392998, -0.0029119744, 0.016453052, 0.041183412, 0.053624764, 0.0023105368, 0.026812382, -0.009273885, 0.024666883, -0.054741945, -0.018700114, 0.0026993288, 0.0063761924, 0.014840755, 0.0069570006, -0.007845668, 0.049562283, 0.02125186, -0.00065459916, 0.027447145, 0.038415845, -0.039177563, -0.0057128654, 0.04004084, 0.026533086, -0.03712093, 0.01509466, 0.06880829, -0.043240044, -0.04806424, -0.03846663, -0.017494064, 0.026101448, 0.0043290826, 0.03912678, 0.0038561842, -0.0127016045, -0.030290881, -0.044788864, -0.01809074, 0.051314227, 0.0131522855, -0.035775233, -0.047784947, 0.016935471, 0.031941265, -0.036486167, -0.024870008, 0.0067919623, 0.017913006, -0.051593523, -0.011114697, -0.009235799, -0.0006077854, -0.026101448, 0.008766075, -0.010124467, -0.0041005677, -0.029859243, -0.06535518, 0.00059985084, -0.026660038, 0.0021597806, -0.03417563, -0.018484294, 0.0026945681, 0.058296617, -0.0017979657, 0.002323232, -0.070077814, -0.05012087, 0.0056843013, 0.07292155, -0.0034277192, -0.022864157, 0.031433456, 0.042681452, -0.013063419, 0.0009743609, 0.044433396, -0.029579947, 0.02074405, 0.032702982, -0.0020375887, 0.023752825, -0.021162992, 0.010340286, -0.008467736, -0.017494064, 0.015615165, -0.045575973, -0.016745044, -0.035343595, 0.11943697, -0.0291737, -0.017671797, 5.63352e-05, -0.02721863, -0.018776285, -0.0033610691, -0.031357285, -0.014091735, 0.025796762, -0.017176682, 0.00730612, -0.084753536, 0.008683556, -0.032626808, -0.0061381566, 0.009826128, 0.0057446035, 0.035267424, 0.04344317, -0.0038307935, -0.022978414, 0.01584368, -0.000121695935, -0.0071410816, 0.013901306, -0.011755807, 0.008791465, -0.023321187, -0.020629792, 0.010968702, -0.006109592, 0.035800625, 0.022394432, -0.058245838, 0.004408428, -0.017290939, -0.006620576, -0.03564828, -0.005858861, -0.01273969, 0.001544854, -0.025200084, -0.021404203, -0.027015505, -0.01910636, -0.019322181, 0.033033058, -0.03912678, -0.027421754, -0.01565325, -0.0032230082, 0.015475518, 0.031839702, -0.00853756, 0.030087758, 0.008061488, 0.03415024, -0.021823145, -0.028792841, 0.04265606, 0.006487276, 0.039863106, -0.0031119247, 0.031966656, 0.033769384, -0.03356626, 0.028869014, -0.010219681, -0.0068617864, -0.009477009, 0.04539824, -0.022216698, -0.008290002, -0.021201078, 0.031484235, 0.017163986, 0.030824082, 0.033286963, -0.034581877, 0.03516586, 0.017392501, 0.0032912453, 0.041868955, 0.0066142287, -0.025263561, -0.0021153472, 0.020058505, 0.029097527, 0.013126895, -0.0007851223, -0.03574984, 0.033388525, -0.043798637, -0.029326042, 0.0035134123, -0.024628798, 0.010936963, 0.02050284, -0.006046116, 0.04156427, 0.023384662, -0.05357398, -0.013660096, 0.011121045, 0.041056458, 0.0021454983, 0.018141521, -0.010365677, 0.013304628, -0.017138597, 0.05357398, -0.022559471, -0.022673728, 0.010537063, 0.019246008, 0.024171768, 0.0037482744, -0.00071172783, 0.044001758, 0.018573161, -0.019601475, 0.016821215, 0.044661913, 0.014002868, -0.016630786, -0.026964724, 0.02047745, -0.00033939976, 0.005773168, -0.00915328, -0.010194291, -0.0019328528, 0.015031184, -0.017722579, 0.016605396, -0.034048676, 0.0101879435, 0.0107021015, -0.023346577, 0.017468672, 0.00081567024, -0.017367112, 0.0019709386, 0.006899872, -0.0130761145, 0.014396421, 0.017595626, -0.03765413, -0.03755257, -0.009546833, -0.016072195, 0.04742948, 0.006014378, -0.0256952, -0.013012638, -0.018116131, 0.026380744, -0.018370036, -0.043925587, -0.030849474, 0.026583867, 0.0069823912, -0.01407904, 0.004233868, -0.003926008, -0.033286963, 0.02250869, -0.004284649, 0.013914001, -0.05844896, -0.02178506, -0.007375944, -0.009711871, -0.030341662, 0.007871059, 0.0074711586, -0.019626867, -0.0068871765, 0.007915492, 0.05697631, -0.035572108, 0.027828002, -0.0018233563, -0.0076425443, 0.0192714, 0.026837772, 0.040320136, -0.0066967476, 0.0047035925, -0.04417949, -0.040447086, -0.025453988, 0.015564385, -0.011489207, 0.004713114, 0.009489704, -0.0057604727, -0.04115802, 0.047835726, -0.011984322, 0.05794115, -0.004303692, -0.0117431125, -0.028132688, -0.04359551, -0.027269412, 0.016745044, 0.0036022791, 0.013253848, 0.028361203, 0.0113749495, 0.019042885, -0.019855382, 0.051238056, 0.0009513508, 0.020134676, 0.0008331262, -0.03112877, -0.011705027, -0.0013631532, 0.021620022, 0.008645469, 0.041970517, 0.0016678394, -0.0062778043, -0.055960692, -0.0017836835, 0.022864157, -0.006963348, -0.015450127, 0.012803166, -0.0026072883, -0.030849474, -0.04115802, -0.031941265, 0.0027358278, 0.006588838, -0.0030309926, -0.010765578, -0.041056458, -0.0060397685, 0.019842686, 0.005839818, -0.00015998007, 0.00060937234, 0.03470883, -0.03765413, 0.0051288838, -0.030062368, -0.0022470604, 0.0038307935, -0.01905558, -0.042783014, 0.011489207, 0.032017436, 0.011286083, -0.00495115, 0.006525362, -0.024209853, 0.037070148, 0.017430587, 0.0036340172, 0.034429535, -0.014421811, -0.033286963, 0.009204061, -0.018433513, 0.018179607, -0.041894346, -0.01362201, 0.026964724, -0.010314896, 0.011939889, -0.0026342657, 0.0142567735, -0.0129301185, -0.003229356, 0.035826012, 0.0066967476, -0.008518517, -0.017913006, -0.016046803, 0.034581877, 0.022292871, -0.0014075866, -0.033997897, 0.0082582645, -0.0023137105, 0.04265606, -0.0038307935, -0.022369042, 0.023371967, -0.026076056, 0.01798918, -0.0034880217, -0.0014702694, -0.0022740378, -0.026279181, -0.012866642, -0.0380096, -0.020413972, 0.029351434, -0.005395484, -0.036435388, -0.049689233, 0.0060207252, 0.026279181, -0.012193793, 0.005535132, -0.010683059, -0.038415845, -0.08389025, -0.031103378, 0.007344206, -0.013317324, -0.0037641434, 0.014955012, -0.0015734184, 0.037730303, 0.00064706133, 0.017151292, 0.06088645, -0.040370915, -0.042325985, -0.016351491, 0.030062368, -0.016770434, 0.028869014, -0.024870008, -9.4520154e-05, -0.028107299, -0.007731411, 0.03260142, 0.049816187, 0.017430587, -0.052101333, 0.0033452, 0.0048591094, 0.00016434406, -0.0045702923, -0.0238163, -0.018941322, -0.01806535, 0.02403212, -0.008708946, -0.0069760433, -0.007191863, 0.01934757, -0.012860294, 0.029376823, -0.023752825, 0.013406191, -0.044839647, -0.0063888878, -0.015564385, 0.0500447, 0.02101065, 0.0061667208, 0.010105425, 0.0055383057, -0.050730243, 0.017722579, 0.021912012, 0.015043879, -0.023625873, -0.0067030955, 0.0133808, 0.009546833, 0.00624924, 0.035876796, 0.027142458, -0.012073189, 0.026482305, -0.012022408, 0.01859855, 0.01753215, 0.009743609, -0.020134676, 0.029986195, 0.0020407625, -0.022013575, 0.019868076, -0.024704969, 0.044712692, 0.0056557367, -0.011317821, 0.0053193127, 0.016224537, 0.046109173, 0.016656177, 0.028437374, 0.03861897, 0.031890485, 0.021074126, 0.031027207, -0.013469667, 0.00019499121, -0.03448032, -0.037730303, -0.030976426, 0.021467678, 0.041437317, -0.021543851, 0.001450433, 0.009254842, 0.014459897, -0.009781695, 0.0022899068, -0.02770105, 0.031890485, -0.012162056, 0.035800625, -0.0024327284, -0.03618148, 0.004367168, -0.021239163, -0.017722579, 0.0038276198, 0.05045095, 0.01014351, -0.034074068, -0.01581829, 0.03257603, -0.0029405388, -0.017887617, -0.022927633, 0.023219625, 0.0171259, 0.034277193, -0.0076933256, 0.007839321, -0.016935471, 0.03171275, 0.003449936, -0.006595186, -0.03618148, 0.011997017, -0.015589775, -0.0028643673, -0.014447202, 0.025568247, -0.0059064683, 0.025085827, 0.032423686, -0.050806418, 0.0053351815, -0.0045861616, 0.00040446295, -0.0028056516, -0.027548706, 0.02047745, 0.010733839, -0.04062482, 0.0094452705, -0.018166913, -0.0131142, 0.0046083783, -0.013101505, 0.01335541, -0.019512609, -0.00056057493, 0.019423742, 0.016021414, -0.027497925, 0.03227134, 0.016643481, -0.021607326, -0.013926696, -0.0059191636, -0.018738199, 6.0649603e-05, -0.027878784, 0.01164155, 0.0048622834, 0.007763149, 0.023828996, 0.01828117, -0.037476398, 0.018166913, -0.0061857635, -0.017062426, -0.032982275, 0.015208918, 0.008791465, 0.027980346, 0.024844617, 0.007820278, 0.033337742, 0.001459161, 0.03521664, 0.01640227, 0.03567367, 0.017836835, -0.019703038, 0.012136665, 0.0056620846, 0.017354416, -0.0046528117, -0.013038028, -0.012911076, 0.015361261, 0.003005602, 0.03552133, -0.027751831, 0.004932107, -0.0069252625, -0.025619028, 0.018649332, -0.024958875, -0.00064666464, -0.0033452, 0.04654081, -0.03915217, 0.0052336194, -0.0024882702, 0.00068554387, -0.027320191, 0.020883696, -0.006747529, -0.015957937, -0.013164981, -0.0044465135, -0.0013330019, 0.026304571, 0.0067538763, -0.01683391, -0.0019058754, -0.028488155, -0.0125556085, -0.018116131, -0.009077108, -0.041894346, 0.05095876, -0.024552627, -0.014764584, -0.034581877, 0.012104927, -0.0022296044, -0.001861442, 0.0009926104, 0.0028786494, -0.02132803, 0.017138597, 0.023575092, -0.0065634474, 0.02815808, -0.0124096135, -0.010810011, -0.025631722, 0.043290824, 0.008664513, -0.0052748793, 0.026736211, -0.025428599, -0.028107299, -0.0071791676, -0.021188384, 0.017633712, 0.029478386, -0.019931553, 0.023537006, -0.018763589, -0.0047353306, 0.028970575, -0.014866145, 0.01432025, -0.003738753, -0.000119414755, 0.011330516, 0.021645412, 0.01760832, 0.025098521, 0.03671468, 0.01611028, -0.015513604, 0.0020915435, 0.001271906, -0.005154274, -0.014002868, 0.0017186203, 0.0016789477, 0.019664953, -0.00519236, 0.038847484, 0.01437103, -0.013748962, -0.009895952, 0.012612738, 0.00014540036, 0.017024338, -0.008962851, -0.025149303, -0.028792841, -0.007261687, -0.017722579, -0.015589775, -0.022749899, -0.023270406, 0.02598719, -0.00033344884, -0.0092231035, 0.005344703, 0.022191308, -0.0065634474, -0.034505706, -0.042783014, 0.020997955, 0.0065507526, 0.013304628, 0.015437432, -0.03422641, -0.026634648, 0.017138597, -0.0052748793, -0.04207208, 0.009102499, 0.004316387, 0.03521664, 0.0016583179, 0.02975768, -0.016478444, -0.0033452, 0.00011078595, 0.007052215, -0.012028756, 0.01260639, 0.015234307, -0.013545839, -0.01825578, -0.026609257, 0.017722579, -0.036841635, -0.0465662, -0.02453993, 0.035115078, 0.026431523, 0.013748962, 0.0023422749, -0.027370973, -0.014193296, 0.054589603, 0.03470883, -0.003070665, 0.024095597, 0.0076806303, -0.00941988, 0.008569298, -0.0122636175, -0.0066840528, -0.047404088, 0.014675717, 0.001014827, -0.00041973693, -0.02125186, 0.012784123, 0.0054240483, 0.019233314, 0.0089247655, 0.009718219, 0.0020185458, 0.019576086, 0.067589544, -0.00072839035, -0.010638625, 0.008442346, 0.022495994, -0.018585855, -0.033972505, 0.015742118, -0.030747911, -0.02127725, -0.00034971465, 0.032474466, 0.013736268, -0.010886182, -0.008664513, 0.018052654, 0.002646961, 0.028310422, 0.011095654, -0.0019122231, -0.012003365, 0.027548706, -0.002253408, 0.021556545, -0.05479273, 0.0072489916, -0.012790471, 0.015780203, 0.045118943, 0.030392444, -0.021213774, 0.052253675, -0.026329963, 0.008328089, 0.0053542242, 0.022826072, -0.053624764, 0.0052653574, 0.0016115041, 0.0013250674, -0.013533143, -0.002627918, -0.014840755, -0.021543851, -0.011006787, -0.030595569, 0.01102583, -0.0116669405, 0.008175746, -0.016275318, -0.0031642928, 0.031484235, -0.0072362963, 0.016554615, 0.034911957, -0.012600042, -0.0009450032, 0.021556545, 0.034734223, -0.029300652, 0.009934038, 0.012250923, -0.028005736, -0.00057208, 0.008651817, -0.030849474, -0.010340286, -0.0050812764, -0.010994093, 0.030773303, -0.006398409, 0.017189378, -0.018217694, -0.042427547, 0.009165975, 0.01557708, 0.0047226353, -0.017405197, -0.03910139, 0.03163658, -0.018776285, 0.0056969966, 0.026279181, 0.015208918, 0.005071755, 0.0053669196, 0.007102996, -0.01753215, 0.012523871, 0.027853392, -0.03305845, -0.035368986, 0.006830048, -0.029554557, 0.011279736, -0.002013785, 0.0070839534, -0.02084561, -0.0015527885, -0.0039545726, 0.0235497, 0.02456532, -0.021835841, 0.021048736, 0.00968648, 0.020655183, -0.06428878, 0.037349444, 0.0038212722, 0.055656005, 0.013330019, -0.027624879, 0.04108185, -0.024222549, -0.012733342, 0.01241596, 0.001553582, 0.010670363, -0.011209912, -0.022851462, 0.0006696748, -0.0029849722, 0.011559031, -0.015488213, -0.0024771618, -0.052761488, -0.017202072, 0.015158136, -0.021556545, 0.00087676616, 0.023003805, 0.0035261074, -0.021581937, 0.009165975, 0.05890599, -0.00587473, 0.018928627, -0.025466684, 0.007096648, 0.014815364, 0.0087787695, 0.010080034, 0.00880416, 0.015450127, 0.035394374, 0.010962354, 0.0015916678, 0.01731633, -0.031484235, 0.0198046, 0.0026707647, -0.031357285, 0.0062524136, -0.016973559, 0.016745044, -0.0032166606, -0.00070974417, 0.002884997, -0.0110004395, -0.015780203, -0.0012354072, 0.0015821464, -0.04204669, -0.0038530102, 0.0190048, 0.0024549451, 0.0040180488, -0.006506319, -0.019423742, 0.021823145, 0.027929565, -0.017887617, -0.0036276695, 0.023168843, 0.010359329, -0.0023105368, 0.012143013, 0.03615609, -0.054183356, -0.0039513987, -0.0009822955, -0.02130264, -0.025872933, 0.03521664, 0.0018027264, -0.0015900809, 0.0111527825, -0.01881437, -0.00084502803, 0.0045353803, -0.009603961, -0.021772364, -0.03701937, -0.0020772615, -0.023384662, -0.0112226065, -0.045169722, -0.03816194, -0.0061540254, 0.024793835, -0.0025739633, -0.027142458, 0.029046748, -0.0042084777, 0.009661091, 0.060073953, -0.016706958, -0.005065407, -0.0034721526, -0.013279239, 0.026634648, -0.0050463644, 0.0057604727, -0.009788043, 0.0366639, -0.036917806, -0.0053669196, -0.0095976135, -0.008245569, 0.029808462, 0.02325771, -0.019614171, 0.024400283, -0.017252853, -0.030417835, 0.016605396, 0.013583925, 0.004913064, -0.014231383, 0.0044655567, -0.0543357, -0.026660038, -0.014244078, -0.0004978921, 0.0051193624, -0.011425731, -0.013291934, 0.021746974, 0.025580943, -0.0033515478, -0.016948167, 0.0013814026, 0.0024295547, 0.072566085, -0.0012076363, 0.023486225, -0.0060937232, 0.025047742, -0.015183527, 0.0031754011, 0.0041005677, 0.060226295, -0.0033578954, -0.014358335, -0.02919909, 0.009743609, -0.005005105, 0.0016297535, 0.004065656, -0.015615165, -0.032702982, 0.01150825, 0.043468557, -0.0120414505, 0.004300518, -0.028564326, 0.03521664, -0.0017503585, -0.02772644, 0.021213774, 0.016414966, -0.013164981, -0.003770491, -0.022280175, -0.017786054, 0.007375944, 0.0028976924, -0.0013829895, -0.0138124395, -0.02721863, 0.00389427, -0.012003365, -0.006963348, 0.0029437125, 0.0056843013, -0.010441848, -0.011082959, 0.0105561055, -0.010080034, -0.0267616, -0.013723573, -0.020375887, -0.046286907, -0.0066967476, -0.000614133, 0.047454868, 0.0036657553, 0.0021089995, -0.029986195, 0.0071664723, -0.015615165, 0.02473036, 0.0071410816, -0.019626867, -0.0056589106, 0.008772423, -0.023968644, 0.02823425, -0.0045861616, 0.036308434, -0.053777106, -0.015120051, -0.008829551, -0.017151292, 0.0073505533, 0.012289008, -0.0235497, 0.0286405, 0.0036308435, 0.017329026, -0.037781082, 0.022737205, -0.00071688526, 0.023955949, 0.0065634474, -0.004497295, 0.035445157, -0.0096674375, 0.0106322775, 0.03950764, -0.015907157, -0.015259698, 0.001443292, 0.0016805346, 0.037222493, 0.005579565, -0.0031627058, 0.012117622, 0.034658052, -0.053269297, -0.020147372, 0.015145441, 0.0023771867, 0.0093754465, 0.0016535572, -0.0011727243, -0.004716288, -0.024743054, 0.0003374161, -0.035394374, 0.0049955836, 0.0133681055, 0.044204883, -0.009997514, -0.013482362, 0.014764584, 0.041056458, 0.0050178003, 0.0238163, -0.03712093, -0.023778215, 0.0004241009, 0.051771257, -0.0050971457, -0.03866975, -0.00730612, -0.017836835, 0.00049551175, 0.0013020573, -0.033388525, -0.03958381, 0.015234307, 0.0039006176, -0.008131312, -0.005754125, -0.033312354, -0.04996853, -0.034861173, -0.021581937, -0.049308375, 0.010790968, 0.00396092, -0.011552683, 0.011628855, 0.014916927, 0.0031801618, 0.006519014, -0.02379091, -0.010365677 ] }, { "object": "embedding", "index": 2, "embedding": [ -0.0009395032, -0.034558482, 0.00066574593, -0.039433744, -0.0077826413, -0.0013172089, 0.03646288, 0.026077563, 0.015679548, -0.015133619, 0.0016695226, -0.034355346, -0.015819203, -0.032984182, -0.04349646, 0.009794956, -0.01129943, -0.011673963, -0.010740807, -4.6568493e-05, -0.01611121, -0.02367802, 0.007268454, 0.049971413, 0.008309525, -0.023830373, -0.0013846563, 0.038595807, 0.012162758, -0.027931176, -0.029276952, -0.0462134, 0.012657901, -0.028819896, -0.006262297, 0.073179685, -0.0032438254, 0.015298667, -0.040779516, -0.010283751, 0.010252011, -0.03425378, -0.023970028, -0.0027883567, 0.01228337, 0.041871373, -0.011280387, 0.010639239, 0.013013389, 0.011185166, -0.06408936, 0.0004681646, -0.0012870559, 0.057385873, -0.026382266, -0.0038087969, -0.011762834, 0.022560773, -0.0074525457, -0.02694089, 0.054643538, -0.032374773, -0.03877355, -0.028058136, -0.008493617, 0.021265782, -0.011978666, 0.045096155, -0.002393194, 0.0221545, 0.043699596, 0.016758706, -0.02950548, 0.008468225, -0.01693645, 0.015044748, 0.01016314, -0.0027042457, 0.007382718, -0.039129037, -0.031587623, 0.014574996, -0.047152903, -0.01738081, -0.024706395, 0.0038691028, -0.058503117, -0.015869986, -0.006062335, 0.0045102504, -0.048092406, -0.06551131, -0.052155126, -0.029023033, 0.0108995065, 0.009007804, -0.013051477, -0.04837172, -0.029099207, 0.0069637503, 0.010093312, 0.030444983, -0.024947619, 0.016568266, -0.004900652, -0.022459205, 0.014549604, -0.021989454, -0.030191062, -0.031841543, -0.061651725, -0.00019262197, -0.00702723, 0.012524594, 0.063479945, 0.0052593136, 0.0005542593, -0.0011815205, -0.009090329, -0.04105883, -0.018383794, -0.010594803, 0.063327596, 0.01468926, -0.023639932, -0.057538223, -0.03557416, -0.080238655, -0.07150381, 0.021837102, 0.030775078, -0.023360621, 0.04004315, 0.03646288, -0.05235826, -0.016644442, -0.078105725, 0.028921464, -0.009693388, 0.0010156791, -0.013902108, -0.06388622, 0.013419661, -0.026661579, -0.012321457, -0.017304633, -0.024934923, 0.029023033, 0.007293846, 0.010467843, -0.05378021, -0.022611557, -0.06033134, 0.05916331, -0.023970028, -0.01854884, -0.032933395, 0.0011513676, 0.019577216, 0.0054370575, -0.021786317, -0.008563445, 0.020491326, -0.011724746, 0.0011077251, 0.019488344, 0.034863185, -0.010975683, -0.03681837, -0.07536339, 0.012480157, -0.0370469, -0.08237158, 0.0068812263, -0.009509296, 0.0010291686, 0.028921464, -0.045019977, -0.0003215656, 0.016504787, 0.0332381, 0.029099207, 0.014257596, -0.0020408803, -0.045223113, 0.04887956, 0.014092548, 0.031155959, 0.01655557, 0.02019932, 0.018536145, 0.03204468, 0.06292132, -0.022522686, 0.025087275, -0.03143527, 0.031993892, -0.022141805, 0.039941583, -0.010632891, 0.019488344, 0.04065256, 0.0033326973, -0.0074906335, 0.039713055, -0.009820348, -0.006135337, -0.051393364, -0.0045673824, 0.026331482, 0.049590535, 0.008214305, -0.0103345355, -0.0017583945, -0.040550992, 0.019869223, -0.033949077, 0.005164094, 0.029835574, -0.045477033, -0.010512279, 0.026077563, 0.046898983, 0.027956568, 0.005865547, -0.0039960626, -0.014790827, 0.01924712, 0.021595879, -0.039179824, 0.015666852, -0.025201539, -0.015971554, -0.037808657, 0.02276391, -0.005732239, -0.008620577, 0.013508533, -0.029860968, -0.0027090067, -0.018002912, -0.032374773, 0.01585729, 0.0031311484, 0.012270674, 0.0124738095, -0.00045983287, -0.040754125, 0.040525597, -0.022256069, -0.015349451, 0.037554737, -0.004849868, 0.007147842, -0.04207451, 0.023487581, -0.02539198, 0.03783405, 0.015311363, 0.005916331, -0.0010259947, 0.042150684, -0.018129872, 0.033441234, -0.0024995229, -0.040550992, 0.026864713, -0.0054624495, 0.015501803, -0.052256692, -0.016390523, -0.025772858, 0.043953516, -0.008544401, -0.04598487, 0.012854689, -0.034634657, -0.009852088, -0.0103916675, 0.02272582, -0.013990981, -0.042506173, -0.0009990156, 0.023779588, 0.003434265, 0.003431091, 0.034202997, -0.028337449, 0.015387539, -0.0064241705, -0.008176217, -0.05555765, 0.04270931, 0.02297974, 0.006887574, 0.0075350697, 0.039840017, -0.016898362, 0.010150444, -0.014422644, 0.034609266, -0.04288705, 0.020415151, 0.03821493, -0.017520465, 0.03991619, -0.03643749, 0.022027541, 0.032400165, 0.0018377445, -0.008963369, 0.014282988, -0.03874816, 0.014346468, -0.057436656, 0.0068812263, 0.036234353, -0.020961078, 0.022383029, -0.055506866, -0.002523328, -0.0094966, 0.0053259674, -0.03194311, -0.0065130424, -0.018599624, 0.0055195815, -0.039179824, -0.0032104983, 0.032908004, -0.028413624, 0.006205165, 0.048092406, -0.02317018, 0.009071284, 0.0012005644, -0.0011307365, -0.03313653, 0.015374843, 0.010766199, -0.074957125, -0.011280387, 0.03950992, -0.036234353, -0.004269026, 0.027296377, -0.005894113, 0.018218745, 0.06276897, 0.013064173, 0.009299812, -0.0008784038, 0.0003806813, -0.03067351, 0.062311918, -0.082168445, -0.021405438, 0.0030343414, -0.01630165, -0.015374843, 0.03389829, -0.0024852399, -0.017545857, 0.04143971, -0.0075985496, -0.034380738, -0.021900581, 0.031359095, -0.006919314, -0.034609266, -0.023195572, 0.029454695, 0.026915498, 0.010747155, -0.029480087, 0.03207007, -0.028794505, 0.029099207, 0.049184263, 0.04067795, 0.032374773, -0.057944495, -0.044969194, -0.032552518, -0.02539198, 0.010010787, -0.049006518, -0.0077762934, 0.0043452024, 0.020529415, 0.0016869796, -0.015768418, 0.0026709188, -0.006976446, 0.033009574, 0.029124599, 0.045350075, -0.006176599, 0.020986471, -0.0042658523, 0.019805744, -0.024465172, -0.02143083, 0.0018536145, 0.012207194, 0.015514499, 0.019158248, -0.002331301, 0.043166365, 0.015412931, 0.008937976, 0.027626473, 0.034837794, -0.041795198, 0.0039865407, 0.045273896, 0.020567503, -0.057284303, 0.030343415, 0.07394144, -0.047152903, -0.045019977, -0.041922156, -0.016987234, 0.022040237, 0.01449882, 0.044740666, -0.0013719604, -0.014892396, -0.035624947, -0.039408352, -0.010550367, 0.041109614, 0.021354655, -0.029099207, -0.042836268, 0.012962605, 0.02583634, -0.037275426, -0.011667614, 0.003564399, 0.02104995, -0.06505425, -0.013140349, -0.006055987, -0.0069320104, -0.03605661, 0.0121944975, -0.019488344, 0.007884209, -0.031130565, -0.07561731, -0.005992507, -0.015768418, 0.009839391, -0.03336506, -0.004608644, -0.0009720367, 0.05611627, -0.00090696977, 0.008823712, -0.06505425, -0.04603566, 0.008519009, 0.05520216, -0.003646923, -0.029251559, 0.035472594, 0.045400858, -0.013394269, -0.0013148284, 0.038341887, -0.03029263, 0.02482066, 0.035980433, -0.002491588, 0.011312126, -0.027728042, 0.00956008, -0.013406965, -0.012257977, 0.02668697, -0.05276453, -0.011673963, -0.036919937, 0.12665519, -0.033034965, -0.03194311, -0.005992507, -0.015082835, -0.0069383583, 0.0012862624, -0.04270931, -0.014143332, 0.028845288, -0.011604134, -0.009744172, -0.08871958, 0.0076302895, -0.03311114, 0.004757822, 0.010518627, 0.003519963, 0.031663798, 0.033771332, -0.0019155074, -0.030571943, 0.019335993, -0.00010632891, -0.0045419903, 0.01839649, -0.020618286, 0.014143332, -0.02452865, -0.022306854, 0.0066653946, -0.016580962, 0.04387734, 0.009223636, -0.05977272, 0.00015800555, -0.018459968, 0.007147842, -0.035447203, -0.011566047, -0.017241154, 0.007896906, -0.033441234, -0.017469682, -0.031384487, -0.025163451, -0.02656001, 0.038519632, -0.040500205, -0.026610794, -0.014143332, -0.0008371418, 0.009363292, 0.040322464, -0.013229221, 0.028591368, 0.019094769, 0.034736227, -0.014029068, -0.032146245, 0.034685444, 0.0086459685, 0.03821493, -0.004767344, 0.034202997, 0.032984182, -0.03631053, 0.017914042, -0.017330026, -0.004789562, -0.016987234, 0.04314097, -0.014282988, -0.0017933085, -0.008944324, 0.03250173, 0.011121687, 0.021151518, 0.04171902, -0.031003606, 0.03653906, 0.013457749, 0.008119085, 0.041414317, 0.00087919727, -0.015260579, 0.0074017616, 0.023309836, 0.026763145, 0.008220653, 0.0031597144, -0.021925973, 0.03646288, -0.047025945, -0.028591368, 0.007255758, -0.025696682, 0.023957333, 0.021202302, -0.0019742264, 0.04314097, 0.027651865, -0.050860133, -0.008068301, 0.0093188565, 0.03783405, -0.0013172089, 0.01738081, -0.023398709, 0.003405699, -0.022179894, 0.03811336, -0.016987234, -0.0061321626, 0.012619814, 0.011832662, 0.022497293, 0.006208339, 0.0011164536, 0.046289578, 0.030495767, -0.02206563, -0.0020773814, 0.035218675, 0.01411794, -0.01494318, -0.023563756, 0.010550367, -0.006027421, 0.008468225, -0.017291937, -0.0103345355, 0.003678663, 0.020821422, -0.0062369048, 0.016568266, -0.025379283, 0.01187075, 0.01725385, -0.022306854, 0.025493547, -0.0052243997, -0.018015608, 0.0041484144, 0.011680311, -0.024604827, 0.017393505, 0.00076572684, -0.044943802, -0.028896071, -0.008957021, -0.013851324, 0.043318715, 0.008906237, -0.018599624, -0.014536908, -0.00498635, 0.02077064, -0.0039865407, -0.032806437, -0.026280697, 0.00956008, 0.009807652, -0.009452164, -0.0025011098, 0.01449882, -0.03697072, 0.01744429, 0.0018615494, 0.014740043, -0.057690576, -0.024681004, 0.0044880323, -0.0015028877, -0.039408352, 0.006398779, 0.00898876, -0.006138511, -0.009249028, 0.0061575547, 0.061600942, -0.04121118, 0.03041959, -0.00037949104, -0.005941723, 0.02149431, 0.026788538, 0.038798943, -0.004411856, 0.018180657, -0.042176075, -0.027880393, -0.018764673, 0.007274802, -0.015565283, 0.001282295, 0.020986471, -0.002320192, -0.031790756, 0.045400858, -0.012841993, 0.049590535, -0.011115339, -0.013381572, -0.025785554, -0.039586093, -0.021151518, 0.010791591, 0.010252011, 0.005046656, 0.026991673, 0.003983367, 0.012911821, -0.012511898, 0.049590535, 0.006059161, 0.024160467, -0.0040151067, -0.04263313, -0.009801304, -0.0057195434, 0.023284445, 0.011147079, 0.04669585, 0.005687803, -0.016492091, -0.04771153, -0.003631053, 0.027499514, 0.00039913016, -0.020097751, 0.008398397, -0.0030184714, -0.0272202, -0.043648813, -0.027981961, 0.00020948383, 0.01228337, -0.002388433, -0.016314346, -0.043801162, -0.015501803, 0.011559699, -0.004907, -0.008030213, 0.0010307557, 0.0243763, -0.03257791, -0.0019059854, -0.036869153, -0.0027661386, -0.0053926213, -0.025366588, -0.037427776, 0.013153045, 0.040500205, 0.0067161783, -0.015285972, 0.010550367, -0.025899818, 0.03181615, 0.02386846, -0.0003729447, 0.035193283, -0.016885666, -0.037529346, 0.009185548, -0.022433814, 0.005859199, -0.046518106, -0.01570494, 0.02874372, -0.014714652, 0.019170944, -0.015603371, 0.018599624, -0.016796794, -0.0006637622, 0.04232843, -0.0037262728, -0.006033769, -0.020123143, -0.01782517, 0.040779516, 0.021722838, 0.004868912, -0.0322986, 0.008722145, 0.002397955, 0.045908697, -0.0018901154, -0.022014845, 0.024338212, -0.028845288, 0.013394269, -0.007078014, -0.002347171, -0.003643749, -0.043978907, -0.010486887, -0.023398709, -0.02539198, 0.019234424, -0.0021567312, -0.04156667, -0.043801162, 0.016415915, 0.029581655, -0.0027201157, 0.001172792, -0.014663868, -0.037148464, -0.0759728, -0.02541737, 0.015260579, -0.017609337, -0.009636256, 0.018218745, 0.0031390833, 0.044816844, -0.007522374, 0.023386013, 0.07170695, -0.032425556, -0.026102955, -0.008912585, 0.043166365, -0.021468919, 0.020453239, -0.032146245, -0.010436104, -0.022103718, -0.005043482, 0.033339668, 0.049260437, 0.012880081, -0.05388178, 0.0027359857, -0.0012926104, 0.0039897147, -0.0025931557, -0.017418897, -0.02583634, -0.0193106, 0.020085055, 0.0016211191, -0.004960958, -0.016669834, 0.02003427, -0.0054338835, 0.027169418, -0.034076035, 0.010271056, -0.050403077, 0.010474191, -0.021913277, 0.03783405, 0.020250103, 0.008246046, 0.02051672, 0.0027566168, -0.051901206, 0.027956568, 0.024884138, 0.00921094, -0.02579825, -0.0034564831, 0.014016372, 0.0032025634, 0.014930484, 0.03504093, 0.027499514, -0.02085951, 0.02077064, -0.015095531, 0.011889794, 0.018980505, 0.005944897, -0.017406201, 0.030241847, 0.008169869, -0.030114887, 0.015387539, -0.022675037, 0.04862564, 0.009014153, -0.006982794, 0.002553481, 0.019970791, 0.053830996, 0.021253087, 0.017799778, 0.041998334, 0.02605217, 0.03590426, 0.025480852, -0.011578742, 0.003491397, -0.02586173, -0.047660746, -0.031333704, 0.016593657, 0.038087968, -0.027626473, 0.0090966765, 0.003678663, 0.014168724, -0.0042880704, 0.011470826, -0.028515192, 0.02719481, -0.008258741, 0.030343415, 0.0051831375, -0.04570556, 0.004814954, -0.019399472, -0.009648952, -0.006925662, 0.04296323, 0.011369258, -0.02988636, -0.02064368, 0.018917024, 0.00089427375, -0.025049187, -0.0243763, 0.025823642, 0.026585402, 0.043369498, -0.0040690647, -0.0020170752, -0.022078326, 0.026356874, -0.006982794, 0.0019948573, -0.03532024, 0.017622033, -0.02810892, -0.0039357566, -0.010093312, 0.020630984, -0.0041801543, 0.0129245175, 0.024807964, -0.057284303, -0.009699736, -0.00962356, -0.0028216837, 0.0012648379, -0.020719854, 0.026483834, 0.008791973, -0.032019284, 0.01129943, -0.0148797, -0.01842188, 0.0062781665, -0.012086581, 0.005929027, -0.021380046, -0.0002719719, 0.026153738, 0.023906548, -0.02630609, 0.030597335, 0.015171708, -0.018498056, -0.016276259, -0.014384556, -0.01839649, 0.0064241705, -0.020872207, 0.0055005373, 0.005481493, 0.0077953376, 0.028540583, 0.013330789, -0.03953531, 0.01725385, -0.008112737, -0.010080616, -0.022052934, -0.0052370955, 0.008119085, 0.018625017, 0.029810183, 0.004643558, 0.028286664, 0.0071097543, 0.031790756, 0.021887885, 0.036386706, 0.020846814, -0.026661579, 0.011128034, 0.00018667072, 0.021786317, -0.017622033, -0.0043325066, -0.013305397, 0.01766012, 0.0041928506, 0.03339045, -0.015476411, 0.010429755, -0.0062718187, -0.03656445, 0.02668697, -0.029987928, 0.0022122762, 0.0061607286, 0.04410587, -0.031155959, 0.0007411284, -0.0013632319, -0.0020567502, -0.02899764, 0.022040237, -0.0028486627, -0.020212015, 0.0066146106, -0.0033549152, -0.0069637503, 0.0373516, 0.011877098, -0.020313583, 0.002477305, -0.034939364, -0.029124599, -0.016568266, 0.002424934, -0.044435963, 0.052662965, -0.019589912, -0.019615304, -0.027956568, 0.0039706705, -0.012626162, -0.0069954903, 0.006002029, -0.0033200013, -0.030368807, 0.012499201, 0.021405438, -0.011972318, 0.03554877, -0.014828916, -0.02104995, -0.02136735, 0.051291797, 0.0128673855, 0.005979811, 0.02656001, -0.023792284, -0.026102955, -0.008525357, -0.023652628, 0.014676563, 0.019424863, -0.021253087, 0.019031288, -0.030191062, -0.0038595807, 0.03542181, -0.011039163, 0.016923754, -0.0028835766, 0.0039611487, 0.016263563, 0.012384938, 0.017368114, 0.018650409, 0.04321715, 0.0056687593, -0.01206119, 0.002350345, 0.0053196196, -0.0069700982, -0.0015949337, 0.0029787966, 0.009014153, 0.023322532, 0.00047649635, 0.04311558, 0.016606353, -0.022167198, -0.0052720094, 0.010962986, -0.0055830614, 0.025290411, -0.0009807652, -0.027321769, -0.02760108, -0.0041611106, -0.011058207, -0.012740426, -0.034482308, -0.021468919, 0.027245592, 0.01269599, -0.019653391, 0.0046022963, 0.019869223, -0.0123087615, -0.050225332, -0.041998334, 0.023843069, 0.0041833287, 0.009033197, 0.019361384, -0.029530872, -0.028058136, 0.023055917, -0.0010521801, -0.041160397, 0.018498056, -0.0042563304, 0.038875118, 0.0061861207, 0.033517413, -0.0077255093, -0.0004225384, 0.0034088732, 0.009706084, 0.0002924045, 0.0064939987, 0.010175835, -0.007465242, -0.020846814, -0.037681695, 0.02539198, -0.027956568, -0.043064795, -0.03796101, 0.033822116, 0.014778132, 0.018079089, 0.007046274, -0.023398709, -0.016631747, 0.05172346, 0.020402456, -0.018383794, 0.02821049, 0.00052847055, -0.018053697, 0.008017518, -0.0087031005, -0.004964132, -0.041084222, 0.01199771, 0.005862373, -0.0015862052, -0.02694089, 0.017076105, 0.013851324, 0.017164977, 0.0044531184, 0.016593657, 0.004814954, 0.023246357, 0.06657777, 0.0055068852, -0.018053697, 0.009426772, 0.024135076, -0.021100733, -0.031333704, 0.011724746, -0.030698903, -0.02579825, 0.0012846754, 0.030495767, 0.019653391, -0.020910295, 0.0048593897, 0.021037254, -0.0075350697, 0.02630609, 0.009680692, -0.00724941, -0.013990981, 0.029327735, 0.008176217, 0.018942416, -0.059112526, -0.0031279745, -0.012035798, 0.020808727, 0.036615234, 0.03364437, -0.02393194, 0.053323157, -0.022217982, 0.0017869605, -0.00065066945, 0.021735534, -0.05758901, 0.0041706325, 0.012162758, 0.0012973715, -0.021760926, -0.007700118, -0.011096295, -0.009693388, -0.007992125, -0.034304563, 0.015273275, -0.010975683, 0.0029835575, -0.012657901, 0.0020583372, 0.0284898, -0.022878174, 0.01852345, 0.026737753, -0.004840346, 3.5285924e-05, 0.030140279, 0.03422839, -0.029530872, 0.003478701, 0.012937213, -0.030698903, 0.0055481475, -0.00054196006, -0.02386846, -0.008036561, 0.0025026968, -0.009629908, 0.04171902, -0.002323366, 0.024135076, -0.012994345, -0.0319685, 0.016035035, 0.00705897, -0.0032565212, -0.016225474, -0.048219368, 0.039357565, -0.0017409376, 0.0064939987, 0.02143083, 0.015247883, -0.0015592262, 0.006046465, 0.0012386525, -0.011908838, 0.024515955, 0.032400165, -0.03219703, -0.023627236, 0.0064844764, -0.025912514, 0.012607117, -0.0011926295, -0.0016552396, -0.020757943, -0.00077128137, -0.0051736156, 0.03247634, 0.015197099, -0.02187519, 0.01797752, 0.011978666, 0.012169106, -0.062413484, 0.035599556, -0.002555068, 0.04634036, 0.008963369, -0.024592131, 0.021443525, -0.024262035, -0.016657138, 0.008442833, 0.0044340743, 0.0028264446, -0.0076937694, -0.031866934, 0.006401953, -0.0028788156, 0.005963941, -0.019742263, -0.0050910916, -0.049996804, -0.013686277, 0.02003427, -0.019196335, -0.006747918, 0.019094769, -0.009541036, -0.018828152, 0.014346468, 0.062210348, -0.013153045, 0.017583946, -0.027093241, 0.017291937, 0.022103718, 0.006951054, 0.019031288, 0.012035798, 0.0018552014, 0.040855695, 0.015019355, 0.006316255, 0.022916261, -0.03425378, 0.01699993, -0.0005455308, -0.040601775, -0.0014386143, -0.020059664, 0.020783335, 0.0036850108, 0.004856216, 0.00028982564, -0.0152351875, -0.021773621, -0.0028486627, -0.008100041, -0.048651032, -0.0076747257, 0.00927442, 0.0032977832, -0.0011228016, 0.0017504595, 0.0016393696, 0.018510753, 0.034431525, -0.02162127, -0.008906237, 0.02212911, 0.0076620295, -0.016606353, 0.011680311, 0.021113431, -0.050656997, -0.01136291, -0.0055798874, -0.03168919, -0.012378589, 0.027296377, 0.0022154502, -0.0029184907, 0.011699354, -0.0148797, 0.0031200394, 0.0061861207, -0.0077382056, -0.02289087, -0.04093187, -0.0015457367, -0.011978666, -0.011413694, -0.04545164, -0.044816844, -0.003923061, 0.034583874, -0.0042341123, -0.017812474, 0.024985706, 0.001237859, 0.013978284, 0.04890495, -0.014359164, -0.0028216837, -0.0017456985, -0.021684749, 0.04578174, -0.007198626, 0.012467462, -0.009172852, 0.029302344, -0.035244066, -0.008957021, 0.000565765, -0.017863257, 0.02181171, 0.020656375, -0.010378972, 0.021062646, -0.004411856, -0.029200776, 0.013990981, 0.015743027, 0.0038151448, -0.011477174, -0.003545355, -0.051393364, -0.028134312, -0.014143332, -0.0014179833, 0.01602234, -0.0027026588, -0.01757125, 0.026509225, 0.030851254, -0.011591438, -0.016695226, 0.0030105365, 0.0038310147, 0.061245453, -0.0016282606, 0.024363603, 0.0010529737, 0.029226167, -0.00801117, 0.013241917, 0.007287498, 0.055252947, 0.0041579367, -0.015400236, -0.026102955, 0.017710906, -0.009445816, 0.0037961008, 0.0026042648, -0.020935686, -0.028921464, 0.0053323153, 0.047889274, -0.011972318, 0.005875069, -0.020694463, 0.02656001, 0.0037802309, -0.037503954, 0.025810946, 0.010467843, -0.015285972, -0.010702719, -0.015882682, -0.017723601, 0.0013894173, 0.007135146, -0.0025011098, -0.008791973, -0.025277715, 0.0010926486, -0.008512661, -0.013076869, 8.616907e-06, -0.0007288291, -0.0077699455, -0.010410711, 0.005592583, -0.010245663, -0.025912514, 4.8701022e-05, -0.016911058, -0.034431525, -0.00476417, -0.0052656615, 0.043801162, 0.006982794, 0.0010450386, -0.02003427, -0.0006847899, -0.017799778, 0.015171708, 0.012581726, -0.020300888, -0.019196335, 0.013483141, -0.025455458, 0.013051477, 0.00068240945, 0.031638406, -0.043928124, -0.013864021, -0.013635493, -0.010524975, 0.007065318, 0.0026978978, -0.022205286, 0.025430067, 0.00444677, 0.015311363, -0.02418586, 0.028667543, 0.0020805553, 0.019869223, 0.009268072, -0.016504787, 0.039205216, -0.00968704, 0.013076869, 0.03709768, -0.023754196, -0.014143332, 0.0020678593, -0.00465308, 0.029048424, -0.0016520657, -0.0030517986, 0.008944324, 0.029353127, -0.050250728, -0.017774384, 0.011915186, 0.0042214165, 0.0074525457, 0.010302795, 0.0052656615, -0.010144096, -0.020148534, -0.00449438, -0.026102955, 0.0034279171, 0.02380498, 0.04915887, -0.0064083007, -0.013572013, 0.010347231, 0.042023726, 0.017291937, 0.011381955, -0.04529929, -0.0074588936, 0.0009871132, 0.049488965, 0.0014703543, -0.04402969, -0.013368877, -0.0086459685, 0.0040214546, 0.0021122952, -0.014536908, -0.036894545, 0.020783335, 0.0018948764, -0.0057258913, -0.008296829, -0.03539642, -0.03938296, -0.024655612, -0.021151518, -0.04915887, 0.0097187795, -0.0132673085, -0.008950672, 0.011394651, 0.015946163, 0.0054719714, 0.017241154, -0.015197099, -0.0041928506 ] } ], "model": "text-embedding-3-small", "usage": { "prompt_tokens": 27, "total_tokens": 27 } } headers: CF-RAY: - 939c1719ce3bfd43-NRT Connection: - keep-alive Content-Type: - application/json Date: - Sat, 03 May 2025 01:47:25 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-allow-origin: - '*' access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '99724' openai-model: - text-embedding-3-small openai-organization: test_openai_org_id openai-processing-ms: - '193' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload via: - envoy-router-698696864c-sgwrg x-envoy-upstream-service-time: - '79' x-ratelimit-limit-requests: - '5000' x-ratelimit-limit-tokens: - '5000000' x-ratelimit-remaining-requests: - '4999' x-ratelimit-remaining-tokens: - '4999962' x-ratelimit-reset-requests: - 12ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_cee192745cdb948fb51aeab7b7299d8c status: code: 200 message: OK version: 1 test_async_embeddings_with_content.yaml000066400000000000000000001356141527070072300471250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "This is a test for async embeddings", "model": "text-embedding-3-small" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '83' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - Linux x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.9.2 method: POST uri: https://api.openai.com/v1/embeddings response: body: string: |- { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": [ -0.0012588563, -0.029314144, -0.004568109, -0.021721158, -0.025343766, -0.0004159207, 0.04500728, 0.03663181, 0.009042029, -0.0049231243, 0.026416058, -0.030574812, 7.522572e-05, -0.031241372, -0.03683468, 0.0045789764, -0.017837722, -0.0065569207, 0.017272595, -0.0049231243, -0.008542109, -0.047383714, 0.015185973, 0.042804737, 0.0033654028, -0.019634536, 0.012171963, 0.041471615, 0.033994555, -0.034690093, 0.006172924, -0.038370665, 0.009599911, -0.028618604, -0.017069729, 0.06578656, -0.010759146, 0.036747735, -0.039819706, -0.0077886074, 0.0027314464, -0.03750124, -0.04993403, -0.0045898445, 0.013425386, 0.05135409, -0.03257449, -0.01773629, 0.018866543, 0.034516208, -0.048398044, 0.012041549, -0.0050209346, 0.047876388, -0.032081816, -0.007237971, -0.029792327, 0.030429907, -0.0102447355, 0.0064736004, 0.061787203, -0.031212391, -0.029256182, -0.018881032, 0.006412016, -0.00872324, -0.022416698, 0.025285805, -0.011730005, -0.0064736004, 0.05193371, 0.023720838, -0.009969417, 0.0034740812, -0.0029632933, 0.021025617, 0.012063284, 0.016519092, -0.016345207, -0.034516208, -0.030111117, 0.027531821, -0.029256182, -0.019040428, -0.01775078, -0.013179048, -0.07807445, -0.009136218, -0.0018765109, 0.0011746306, -0.020721318, -0.028256342, -0.07679929, -0.029328635, 0.029343124, 0.011845929, -0.001069575, -0.04593467, -0.03428436, 0.021721158, 0.01576559, 0.02543071, -0.017576894, 0.008230565, 0.0015187784, -0.016475622, 0.0012162906, -0.00537595, -0.04758658, -0.003651589, -0.05671555, -0.014193378, 0.0036751358, 0.003187895, 0.057150263, -0.022576094, -0.00452826, -0.013323952, -0.008563845, -0.021156032, -0.018576734, -0.007031482, 0.03628404, -0.007491553, -0.024590263, -0.046166517, -0.03553054, -0.047064923, -0.061787203, 0.0005922964, 0.02082275, -0.028270833, 0.0557302, 0.039153147, -0.06288847, -0.011164878, -0.077205025, 0.027010165, -0.0033291767, 0.022981824, -0.017910173, -0.04660123, 0.03654487, -0.022720998, -0.045673843, -0.010708429, -0.021996476, 0.047905367, 0.005970058, -0.008643542, -0.011338763, -0.02548867, -0.04494932, 0.042456966, -0.018953485, -0.031734046, -0.038370665, 3.5575144e-05, 0.010679448, -0.0052346685, -0.036515888, 0.019938834, 0.034139458, -0.0284737, 0.0017578705, 0.03083564, 0.01005636, -0.005129613, -0.022735488, -0.062830515, 0.004568109, -0.03750124, -0.054889757, 0.000167206, -0.012012568, -0.014867184, 0.028096948, -0.06943815, 0.012164718, -0.011186614, -0.014722279, 0.024039626, 0.022416698, 0.00039848688, -0.06074389, 0.06584452, 0.02246017, 0.06404771, 0.0010713863, 0.0025285804, -0.007006124, 0.058599308, 0.056918416, -0.008607317, 0.024213511, -0.031212391, 0.042891677, 0.003068349, 0.031618122, -0.01223717, 0.0075712507, 0.03990665, -0.0050173122, 0.0057671918, 0.05199167, -0.0251409, -0.023633895, -0.042312063, -0.009969417, 0.0045572408, 0.042456966, -0.019388199, 0.0028763507, 0.010643222, -0.015953965, 0.007810343, -0.034371305, 0.013505083, 0.037066527, -0.018055078, -0.030980544, 0.043326393, 0.054426063, 0.0402834, -0.026256664, -0.0015142502, -0.016026419, -0.0013657232, 0.017388519, 0.00015962117, -0.004473921, -0.023648385, -0.0016011928, -0.01677992, 0.03190793, -0.015968457, -0.001709871, -0.0135558, -0.02482211, 0.0091144815, -0.015838042, -0.034690093, 0.01391806, -0.021953005, -0.006654731, 0.023083258, -0.001220819, -0.028401246, 0.038167797, -0.019069409, -0.019344727, 0.02218485, -0.014570129, 0.0020576413, -0.04219614, -0.023271633, -0.030024175, 0.03929805, 0.0073176683, 0.005618665, -0.012910975, 0.042341042, -0.02146033, 0.007448082, 0.013968777, -0.05158594, 0.033009205, -0.008317508, -0.006031642, -0.047180846, -0.00988972, -0.0060026613, 0.057440072, -0.036747735, -0.057816826, 0.006129453, -0.036370985, -0.007364762, -0.007926267, 0.0067597865, 0.004408714, -0.046977982, 0.010983747, 0.0342264, -0.011309782, 0.007940757, 0.0013113841, -0.019518612, -0.0016138719, 0.011940116, -0.0052093104, -0.05868625, 0.051209185, 0.029009845, 0.0051513487, 0.004386978, 0.033443917, -0.010563525, 0.016533583, -0.010208509, 0.05222352, -0.04350028, 0.0322557, 0.02822736, -0.018634696, 0.033617802, -0.04865887, 0.040718116, 0.040399324, 0.001111235, -0.028430227, 0.01275158, -0.035646465, 0.023039786, -0.046717152, 0.008795693, 0.02447434, -0.015635176, 0.034371305, -0.047702502, -0.008998558, -0.036399964, 0.011621326, -0.045499958, -0.0063830353, -0.03118341, -0.00072678574, -0.035095826, 0.015301896, 0.029140258, -0.0068902005, 0.028444719, 0.03663181, -0.002318469, 0.023691857, -0.026633414, -0.00048769361, -0.023648385, 0.0496732, -0.0018620205, -0.04121079, -0.0017297954, 0.04100792, -0.025532141, 0.005415799, 0.052803133, 0.027039146, 0.030951563, 0.031734046, -0.017388519, 0.013447121, -0.0066112597, -0.0068865777, 0.003044802, 0.057845805, -0.05767192, -0.020243134, -0.0077596265, -0.010222999, -0.019634536, 0.025749497, -0.0053723278, -0.009962172, 0.066655986, 0.013505083, -0.031994876, -0.007803098, 0.021851571, -0.002628202, -0.015562724, -0.016982786, 0.049876068, 0.004289168, 0.009353574, -0.02586542, 0.02182259, -0.026705867, 0.03990665, 0.04590569, 0.054976698, 0.03153118, -0.032284684, -0.039182127, -0.07488655, -0.02143135, 0.005129613, -0.014657072, -0.01912737, -0.026213191, -0.011476423, 0.0125849405, -0.031994876, -0.025169881, -0.00569474, 0.024300454, 0.027604273, 0.058077652, -0.025720516, 0.009382554, -0.0035429106, -0.0051404806, -0.045876708, -0.010273716, 0.0016645883, 0.0022170362, 0.023097748, 0.02819838, -0.0016274566, 0.025271313, -0.0093898, -0.005988171, -0.0011338763, 0.044021934, -0.05630982, -0.0009980286, 0.019011447, 0.012758825, -0.043703143, 0.010976503, 0.06729357, -0.036747735, -0.0584544, -0.024445359, -0.013512328, 0.024039626, -0.010447602, 0.075929865, 0.0060859816, 0.018315906, -0.06132351, -0.043674164, -0.018996956, 0.043413334, 0.038428627, -0.021054598, -0.022938354, 0.036689773, 0.019272275, -0.029212711, -0.018489791, -0.0052165557, -0.007237971, -0.0584544, -0.010527299, -0.009462252, -0.011222839, -0.026024817, 0.0037566444, -0.006346809, -0.01291822, -0.008143622, -0.067931145, 0.009599911, -0.029256182, -0.009027539, -0.027415898, 0.010143302, 0.0031208768, 0.06735153, 0.007861059, 0.019388199, -0.050484665, -0.06010631, 0.035878308, 0.06004835, 0.017374028, -0.0028093325, 0.010947522, 0.045528937, 0.0018131153, -0.008237811, 0.04636938, -0.0013901758, 0.027357936, 0.037530217, -0.035443597, 0.024010645, -0.021532781, -0.00030226135, -0.022039948, -0.012106756, -0.021040108, -0.039124165, -0.036313023, -0.00093463284, 0.14397693, -0.043065563, -0.019359218, 0.008773956, -0.012505243, -0.026503, 0.017794251, -0.012222679, -0.018533263, 0.034371305, -0.047238808, -0.01676543, -0.05094836, 0.03950092, -0.027560802, -0.012824032, 0.0063721677, -0.020170681, 0.021547273, 0.01776527, -0.0010795372, -0.041761424, 0.009650628, -0.014033983, 0.0171132, -0.014041228, 0.01191838, -0.020243134, -0.010831598, -0.012592185, -0.004285545, -0.036168117, 0.054918736, 0.005593307, -0.04184837, -0.009230405, -0.01914186, -0.0036443437, -0.041529577, 0.012512488, 0.013193538, -0.01375142, -0.057208225, -0.034139458, -0.0052708946, -0.018084059, -0.015910495, 0.028430227, -0.026430547, -0.027937554, -0.00047275034, -0.020735808, 0.017243614, 0.018402848, -0.014772995, 0.03883436, 0.0044884114, 0.034719076, -0.02112705, -0.022836922, 0.043413334, 0.023488991, 0.015026578, -0.020243134, 0.0152439345, 0.022720998, -0.018996956, 0.028430227, -0.017388519, 0.01225166, 0.013722439, 0.04500728, -0.016895844, -0.018156512, -0.022228323, 0.021445839, 0.03686366, 0.06706172, 0.035008885, -0.033067167, 0.03689264, 0.017808741, -0.0019073031, 0.044050913, -0.0050245575, -0.05801969, 0.010317188, 0.021590743, 0.02143135, 0.014533903, 0.01982291, -0.023517972, 0.018852051, -0.034342322, -0.037385315, -0.017069729, -0.010817108, 0.02279345, 0.0118241925, -0.00887539, 0.054049313, 0.004546373, -0.019170841, -0.010679448, 0.032516528, 0.02882147, 0.0019471518, 0.019764949, -0.038109835, 0.025995836, -0.010715675, 0.048484985, 0.0021337161, -0.03263245, 0.010578016, 0.011266311, 0.0018746996, -0.008607317, -0.005437535, 0.02987927, 0.006506204, -0.035066847, 0.016519092, 0.04019646, -0.014041228, -0.009143462, -0.030806659, 0.017939154, -0.0146136, -0.0031353673, -0.007006124, 0.008795693, 0.0080277, 0.028096948, -0.020909693, 0.014780241, -0.03184997, -0.00056014577, 0.019735968, -0.024068607, 0.0015857967, 0.0045318827, 0.00562591, 0.0029614822, 0.025720516, -0.015229444, 0.0052346685, 0.014910654, -0.037153468, -0.037877988, -0.0160554, -0.02754631, 0.039442956, 0.011382234, -0.024126569, -0.0106939385, 0.0001393572, 0.02315571, 0.0080277, -0.010998238, -0.010933031, 0.014316547, 0.0033961951, -0.004571731, -0.013425386, 0.0059773033, -0.032313664, 0.003635287, -0.010462092, 0.02586542, -0.07088719, -0.019837402, -0.011650307, -0.013381914, -0.016649507, 0.0073973658, 0.00247243, -0.013476102, 0.0129544465, 0.008216075, 0.043094546, -0.043703143, 0.04636938, -0.0017823231, -0.008636298, 0.010027379, 0.012983427, 0.04384805, -0.02047498, 0.023329595, -0.039182127, -0.029560482, -0.016635016, 0.01743199, -0.017475462, -0.0052093104, 0.020706827, -0.008360979, -0.026314626, 0.036805697, -0.0044232043, 0.03889232, 0.0016365132, 0.0021463952, -0.01844632, -0.046804097, -0.028125929, 0.052542306, -0.0029940854, -0.0065170717, 0.035762385, 0.010911295, 0.02987927, -0.018866543, 0.036428947, 0.007650948, 0.017837722, 0.008201584, -0.025923382, -0.004933992, -0.02987927, 0.015881514, 0.029096788, 0.030024175, -0.005339724, -0.016707469, -0.035095826, 0.011701024, 0.026387077, -0.007455327, -0.01409919, 0.0060461327, -0.012548714, -0.029966213, -0.038022894, -0.036660794, 0.0026734846, -0.0030647265, -0.0086145615, -0.0077234004, -0.042312063, -0.010063605, 0.0073901205, -0.013628251, 0.003169782, -0.010483827, 0.00019460198, -0.016649507, -0.011259066, -0.018736128, 0.00089478417, 0.001361195, -0.010672203, -0.040428307, -0.011925626, 0.01712769, 0.009643382, 0.011483667, -0.0012733467, -0.03019806, 0.048484985, 0.02751733, 0.0006565977, 0.0149541255, -0.011498158, -0.022923864, 0.012700864, -0.012027059, 0.022880392, -0.041819386, -0.036370985, 0.024054118, -0.022445679, -0.0124545265, 0.013635497, 0.02147482, -0.007089444, -0.012440036, 0.017953645, 0.015316387, -0.0112445755, -0.018562244, -0.0052817627, 0.045470975, 0.024995996, 0.0036207966, -0.023720838, 0.016519092, 0.002394544, 0.051701862, -0.006810503, -0.02214138, 0.017200142, -0.034081496, 0.018576734, -0.0155047625, 0.021909533, -0.00032105364, -0.026126249, -0.010606997, -0.015490272, -0.01744648, 0.01673645, -0.0028147665, -0.043529257, -0.057874784, -0.0042638094, 0.037820026, -0.028575132, 0.00652794, -0.0021645082, -0.044833396, -0.0630044, -0.018721638, 0.029140258, -0.023648385, -0.0044232043, 0.018518772, -0.0029687274, 0.01543231, 0.00313899, 0.020214153, 0.038428627, -0.041326713, -0.035878308, -0.029705387, 0.03683468, -0.009882474, 0.024039626, -0.01843183, 0.0031317447, -0.0113749895, -0.02714058, 0.008998558, 0.03156016, 0.012374829, -0.036950603, -0.0088246735, 0.0077234004, 0.008759466, -0.010143302, -0.007948002, -0.018808581, -0.0038218515, 0.012679128, 0.01257045, -0.0077523813, 0.0023818647, 0.002454317, -0.00063622056, 0.025981344, -0.017939154, -0.003948643, -0.028415738, -0.010672203, -0.024923543, 0.054628927, 0.010447602, 0.017156672, 0.013693458, -0.003807361, -0.030893601, 0.012613921, 0.0013177237, 0.0074082334, -0.023228163, -0.00011943286, 0.026358096, 0.005850512, 0.027024657, 0.0057092304, 0.023691857, -0.026894242, 0.014512167, 0.008527619, -0.00301401, 0.0292272, 0.005165839, -0.0052056876, 0.021025617, -5.7905127e-05, -0.02009823, 0.025937874, -0.008578336, 0.061613318, 0.017895684, -0.017968135, 0.007549515, 0.013816627, 0.035675444, 0.011295292, 0.019156352, 0.050745495, 0.015707629, 0.021300934, 0.017649347, -0.023387557, -0.010049115, -0.03127035, -0.052252498, -0.031357296, 0.027749177, 0.043703143, -0.03663181, -0.0052455366, 0.014809222, 0.004223961, -0.00049176905, 0.014983106, -0.03083564, 0.045036264, -0.013765911, 0.02822736, -0.007585741, -0.02583644, -0.011215595, -0.030140098, -0.014577375, 0.008962332, 0.036805697, 0.0023365822, -0.015359858, 0.00755676, 0.024923543, -0.020953165, -0.037414297, -0.011505403, 0.011845929, 0.017692817, 0.037964933, 0.022213832, -0.0044050915, -0.0018837561, 0.04836906, -0.015577215, -0.00789004, -0.04663021, 0.0025901648, 0.0047999555, -0.019562082, -0.0153743485, 0.011309782, -0.016504603, 0.03118341, 0.0110562, -0.0428627, 0.008071171, 6.022586e-05, -0.008339244, 0.0013747797, -0.035675444, 0.0030574813, 0.022373227, -0.041036904, -0.014475942, -0.021866063, -0.028908413, 0.02547418, -0.02282243, 0.020417018, -0.014910654, 0.0027785404, 0.028082456, 0.0051151225, -0.021286445, 0.022039948, 0.04428276, -0.0054592704, -0.019054918, -0.017475462, -0.021909533, 0.0218081, 0.004785465, 0.034979902, -0.0042203385, 0.0055462127, 0.030053155, 0.0023076013, -0.040776078, 0.01912737, -0.026560962, -0.023083258, -0.030748697, 0.023271633, 0.008549355, 0.017316066, 0.022054438, 0.016330717, 0.0055751936, 0.027604273, 0.021286445, 0.008933351, 0.027705707, 0.046977982, -0.015330877, 0.007984228, 0.015591705, 0.012657393, -0.013200783, 0.0046985224, -0.008085661, 0.008752221, 0.0062055276, 0.036428947, -0.0012008946, 0.016577054, -0.015519253, -0.036168117, 0.0041189054, -0.002626391, -0.020706827, -0.0050318027, 0.0502818, -0.02882147, 0.010346169, -0.0019471518, -0.020257624, -0.0055860616, 0.004187735, 0.0048035784, -0.022692017, -0.0020413396, -0.01612785, -0.008491393, 0.029299654, 0.014287566, -0.02985029, 0.008092906, -0.060512044, -0.028386757, -0.02282243, 0.0048289364, -0.023054278, 0.02415555, -0.020561922, -0.01205604, -0.02543071, 0.026705867, 0.010947522, -0.010317188, -0.017808741, -0.005165839, -0.032806337, 0.01193287, 0.028676566, -0.010353413, 0.014005003, -0.0075929863, -0.025952363, -0.026169721, 0.045326073, -0.0023039787, 0.015881514, 0.009628892, -0.030893601, -0.023851251, -0.0017651158, -0.0004333545, 0.030342964, 0.04494932, -0.01777976, 0.015403329, 0.004571731, 0.010085341, 0.02511192, -0.018924505, 0.022010967, -0.0013892702, 0.0022061684, 0.0018122096, 0.023604915, 0.0018457188, 0.010338923, 0.011664798, 0.0064265067, -0.03863149, -0.00028822376, 0.010215755, -0.013063124, -0.0027332578, -0.009462252, 0.010266471, 0.025416218, 0.008252301, 0.029415578, 0.0032023855, -0.013294972, 0.014142661, -0.0006253527, 0.027952043, 0.013968777, -0.012258906, -0.025242332, -0.0147440145, 0.003050236, -0.01343263, 0.006248999, -0.0076002316, -0.004633316, 0.021011127, 0.007520534, 0.008049435, -0.0061040944, 0.025575612, -0.0024995995, -0.017649347, -0.017808741, 0.028806979, 0.0019036805, -0.016533583, 0.016794411, -0.017997116, -0.0153743485, 0.018562244, -0.007216235, -0.02854615, 0.01123733, -0.007962492, 0.020953165, 0.0063612997, 0.025952363, -0.013947041, -0.0009142557, 0.0015830797, 0.032226723, -0.012773315, 0.015707629, 0.030371945, -0.009925946, -0.02586542, -0.025198862, 0.01875062, -0.04500728, -0.023706347, -0.037617162, 0.032081816, 0.005821531, 0.025648065, 0.027386917, -0.0136644775, 0.008715995, 0.028111437, 0.005158594, -0.020547433, 0.013287726, -0.006136698, -0.037240412, 0.0160554, -0.026343606, 0.004075434, -0.04364518, 0.0040066047, -0.01473677, 0.014925145, -0.011541629, 0.015953965, -0.011816948, 0.0036316644, 0.0055208546, 0.032980222, -0.0036968715, 0.025633574, 0.10792474, -0.0030701603, 0.002318469, 0.00044671286, 0.016692977, -0.021924024, -0.027126089, -0.0036425323, -0.01914186, -0.025937874, -0.0026535604, 0.032023855, 0.010259226, -0.016229283, 0.0039341524, 0.008433431, 0.0010949332, 0.002121037, -0.001087688, -0.00042044895, 0.010201264, 0.025387237, -0.0027332578, 0.016504603, -0.053440712, -0.009788287, -0.025242332, 0.010882314, 0.014830957, 0.043790087, -0.013773155, 0.03663181, -0.03118341, 0.0101143215, 0.00050942926, 0.023373067, -0.053759504, 0.008411696, 0.023561442, -0.013215274, 0.005379573, -0.009541949, -0.010128812, -0.017287085, -0.009715835, -0.04257289, -0.0140847, -0.020764789, -0.0054665157, -0.006690957, 0.0024271475, 0.027604273, -0.028560642, 0.024532301, 0.042833716, -0.030603793, 0.008752221, 0.02954599, 0.05129613, -0.023271633, 0.017533423, 0.025691535, -0.0402834, 0.023416538, 0.005202065, -0.022634055, -0.02543071, -0.0071365377, -0.00060678687, 0.03176303, -0.015953965, 0.021735648, -0.014635337, -0.050687533, -0.000110319736, 0.02080826, 0.01914186, -0.012302377, -0.025343766, 0.027357936, -0.018518772, 0.0054013086, 0.033791687, 0.033646785, -0.016287245, 0.0044340724, 0.002318469, -0.001212668, -0.0008449733, 0.04523913, -0.035675444, -0.021677686, 0.011838683, -0.02882147, 0.002878162, -0.00016143247, 0.0021898665, -0.03376271, 0.012838523, 0.010447602, 0.013447121, 0.03054583, -0.007839324, 0.016577054, 0.016171321, 0.015185973, -0.054078292, 0.04126875, 0.014758505, 0.036457926, 0.011208349, -0.028357776, 0.013606516, -0.012353093, -0.016939316, 0.012077775, 3.0763866e-05, 0.001561344, 0.0073973658, -0.028270833, 0.0075277793, 0.0025847307, 0.0046079573, -0.016359698, 0.0045173923, -0.043877028, 0.0052093104, -0.00045237318, -0.014193378, -0.021373387, 0.037762064, -0.0061765467, 0.005745456, -0.0018837561, 0.039703783, 0.008194339, 0.014367264, -0.027923062, -0.01373693, 0.02314122, 0.018996956, 0.024662714, 0.012222679, -0.0046152025, 0.014381754, -0.016997278, 0.010396885, 0.0035229863, -0.03292226, 0.008744976, -0.0056150425, -0.041181806, 0.0016229284, -0.029024335, 0.026198702, 0.0039595105, 0.020286605, 0.008252301, 0.004473921, -0.039442956, -0.011295292, -0.002979595, -0.03990665, 0.016533583, 0.024532301, 0.0045173923, 0.013577535, 0.016620526, 0.014055719, 0.02277896, 0.028415738, -0.025735008, -0.0045789764, -0.0008984068, -0.00048135404, -0.024083098, 0.0006316923, 0.020764789, -0.05674453, -0.0098245125, 0.015403329, -0.029705387, -0.017634856, 0.036023214, -0.002251451, -0.00789004, 0.0067344285, -0.016533583, -0.004506524, -0.00176421, -0.009578176, -0.00029682743, -0.034835, -0.0073720072, -0.017707309, -0.017953645, -0.026285645, -0.034168437, 0.0020141702, 0.036718756, -0.0012244415, -0.014120926, 0.011382234, -0.002466996, 0.032052837, 0.05129613, -0.020243134, 0.0147440145, -0.013476102, -0.019562082, 0.039471935, -0.0047999555, 0.006321451, 0.0026734846, 0.055237528, -0.008629052, 0.0076944195, -0.004137018, -0.0033454786, 0.015852533, 0.0052056876, 0.012193698, 0.012729845, -0.033733726, -0.047412694, 0.020895204, 0.024083098, 0.009512968, -0.021648705, 0.0016174945, -0.060859814, -0.047035944, -0.0221124, -0.0013412706, -0.0034106853, -0.02046049, -0.017301576, 0.019170841, 0.026372585, 9.8433055e-05, -0.031936914, -0.011215595, 0.015041068, 0.06004835, -0.0091144815, 0.005531722, -0.0069517847, 0.02175014, -0.02918373, -0.0032512906, -0.00060905097, 0.046079572, -0.009433271, -0.020692337, -0.027444879, -0.0026426925, -0.0058613797, -0.019866383, -0.0096651185, -0.012766071, -0.020214153, 0.002615523, 0.049702182, 0.008237811, 0.007679929, -0.038080856, 0.013265991, 0.011193858, -0.0069083134, 0.008049435, 0.005165839, -0.0035084959, -0.030111117, -0.0058831153, -0.0035247975, 0.012526979, -0.010578016, -0.014041228, -0.0122444155, -0.03721143, -0.0057418337, -0.0076002316, 0.0023836761, 0.0056440234, 0.0015993814, -0.006495336, -0.0028799733, -0.0044123367, -0.01576559, -0.021315426, 0.01911288, -0.022068929, -0.037617162, -0.001587608, -0.0063794125, 0.017605875, 0.0054918737, -0.019866383, -0.041471615, -0.007274197, -0.032516528, 0.026416058, 0.020040268, -0.011882154, -0.0008101057, 0.009911455, -0.017272595, 0.028053477, -0.002573863, 0.05468689, -0.03689264, 0.004408714, -0.0013965154, -0.021286445, 0.017852213, 0.019214313, -0.04121079, 0.029908251, 0.003461402, 0.038515568, -0.009353574, 0.029125769, -0.0027459369, 0.02985029, -0.0030339342, -0.018678168, 0.040689133, -0.006488091, 0.029676406, 0.03625506, -0.009788287, -0.019083899, 0.012744335, -0.01090405, 0.044137858, -0.005995416, -0.016837882, 0.022025457, 0.012389319, -0.046253458, -0.018388359, 0.017316066, -0.012171963, 0.00987523, 0.0040500755, 0.001626551, -0.009440516, -0.028879432, -0.002937935, -0.017316066, -0.005951945, 0.011983587, 0.04094996, -0.01975046, -0.016649507, 0.013715194, 0.029343124, -0.00313899, 0.013381914, -0.041181806, -0.0321108, -0.011317028, 0.033414938, -0.011230085, -0.022605075, -0.008049435, -0.013483347, -0.0008010492, 0.0010125189, -0.0443697, -0.034690093, 0.011990832, 0.0013385536, 0.0040283403, -0.008629052, -0.044137858, -0.04828212, -0.022213832, -0.024677206, -0.069206305, 0.018214474, -0.0010704807, 0.0005184858, 0.009773796, 0.013519573, 0.018721638, 0.003521175, -0.0237788, -0.012831277 ] } ], "model": "text-embedding-3-small", "usage": { "prompt_tokens": 7, "total_tokens": 7 } } headers: CF-RAY: - 939c1708fdf87961-NRT Connection: - keep-alive Content-Type: - application/json Date: - Sat, 03 May 2025 01:47:23 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-allow-origin: - '*' access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '33338' openai-model: - text-embedding-3-small openai-organization: test_openai_org_id openai-processing-ms: - '89' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload via: - envoy-router-6546dd5b5b-cgzc4 x-envoy-upstream-service-time: - '27' x-ratelimit-limit-requests: - '5000' x-ratelimit-limit-tokens: - '5000000' x-ratelimit-remaining-requests: - '4999' x-ratelimit-remaining-tokens: - '4999992' x-ratelimit-reset-requests: - 12ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_3db89a0fc1f707874dd19823da0aab4b status: code: 200 message: OK version: 1 test_async_embeddings_with_dimensions.yaml000066400000000000000000000424221527070072300476150ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "This is a test for async embeddings with dimensions", "model": "text-embedding-3-small", "dimensions": 512 } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '118' content-type: - application/json host: - api.openai.com user-agent: - AsyncOpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - async:asyncio x-stainless-lang: - python x-stainless-os: - Linux x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.9.2 method: POST uri: https://api.openai.com/v1/embeddings response: body: string: |- { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": [ -0.0068461937, -0.074672244, 0.063718334, -0.030411024, -0.033028822, -0.014435025, 0.054880943, 0.032230485, -0.00655378, 0.019104362, 0.017656216, -0.054323968, -0.025435349, -0.047454566, -0.036816277, -0.0014435025, -0.009347955, -0.018111084, 0.041030746, 0.011631567, -0.022483364, -0.07307558, 0.04210757, 0.050536513, 0.0070411363, -0.0164123, 0.028907182, 0.048717048, 0.105603114, -0.01207715, -0.0030169666, -0.05068504, -0.016180225, -0.074003875, -0.026177987, 0.048642784, -0.038468644, 0.09891938, -0.06360694, 0.009473275, 0.0039754338, -0.03995392, -0.07225867, 0.0047621657, -0.006470233, 0.06654036, -0.06921386, -0.010619722, 0.018556666, 0.06479516, -0.06746866, 0.03124649, -0.026103724, 0.071404636, -0.06861974, 0.015066267, -0.017981121, 0.01111172, -0.016560826, 0.015539699, 0.067691445, -0.06245585, -0.056440484, -0.036370695, 0.020422544, -0.0018670382, -0.04285021, 0.04437262, -0.01969847, -0.01919719, 0.05476955, 0.0318963, -0.017052824, 0.0070364946, 0.03417991, 0.034718323, 0.038431514, 0.014657817, -0.044781066, -0.04207044, -0.028647259, 0.08146738, -0.04110501, -0.019457115, -0.018835155, -0.018825872, -0.06590912, -0.0058436324, -0.017637651, -0.010127725, -0.034569796, -0.063644074, -0.102781095, -0.030930871, -0.011919339, 0.028795786, 0.03089374, -0.07370681, -0.059559565, 0.023615887, 0.01095391, 0.050833568, -0.019902697, 0.021313708, -0.039025623, -0.043407187, 0.009436144, -0.0020933107, -0.05265303, 0.02874009, -0.07084766, -0.031766336, 0.0073660403, 0.026289383, 0.07040208, -0.03492255, -0.03211909, -0.013386049, 0.00016433766, -0.028108846, -0.04151346, -0.018593797, 0.05952243, -0.002634044, -0.033103086, -0.0647209, -0.04678619, -0.09015625, -0.09706278, 0.03033676, 0.03692767, -0.0003791515, 0.07489504, 0.05670041, -0.10627149, -0.031747773, -0.094760604, 0.051427677, -0.0074635115, 0.019958396, -0.024618449, -0.018148215, 0.05673754, -0.02552818, -0.03950834, 0.038320117, -0.057405915, 0.097953945, 0.020589637, 4.9823462e-05, -0.037967365, -0.05967096, -0.027032021, 0.03317735, -0.044558275, -0.052170318, -0.02630795, 0.022706155, 0.035720885, -0.016709354, -0.050647907, 0.005876123, 0.03507108, -0.031135095, 0.015938867, 0.036147904, -0.023244567, -0.027459038, -0.061861742, -0.1181537, 0.004971033, -0.05009093, -0.059151113, -0.02484124, -0.006182461, -0.007291776, 0.024729844, -0.07723435, 0.004653091, -0.05814855, -0.049051236, 0.03653779, 0.025101162, -0.0076445295, -0.09505766, 0.10448916, 0.027737528, 0.047268905, 0.039248414, 0.014648533, -0.023894375, 0.07612039, 0.06939951, 0.0065212897, 0.029909743, -0.035126776, 0.06360694, 0.0072175125, 0.032898862, -0.022947513, -0.000543054, 0.05265303, -0.0022418383, 0.011232399, 0.043332923, -0.060599256, -0.016087394, -0.056774672, -0.0318963, -0.007858038, 0.041030746, -0.020329714, 0.034885418, -0.008818826, -0.049199764, 0.024599882, -0.04069656, -0.007403172, 0.033325877, -0.026976323, -0.039842527, 0.056997463, 0.061081972, 0.04028811, -0.0637926, -0.01544687, -0.018519534, -0.02222344, 0.032898862, 0.033530105, -0.008247923, -0.02859156, -0.031060832, 0.011937905, 0.041996177, -0.021146616, 0.028907182, -0.017637651, -0.05361846, -0.007853396, -0.00586684, -0.05502947, 0.02428426, 0.017730482, 0.0032026262, 0.028257374, 0.017396294, -0.052021787, 0.030819476, -0.039322678, -0.04582076, 0.026734967, -0.03497825, -0.014147253, -0.0569232, -0.0018972079, -0.06011654, 0.05792576, 0.0045904308, 0.018408138, -0.003485757, 0.043184396, -0.048457127, 0.0422561, 0.023764415, -0.060933445, 0.03692767, -0.00048387505, -0.0021478483, -0.07006789, 0.005138126, -0.013432464, 0.030113969, -0.056997463, -0.075377755, 0.022873249, -0.056626145, -0.01794399, 0.018064668, -0.008247923, 0.026456477, -0.037503216, 0.020311147, 0.07556341, -0.02796032, 0.011919339, 0.021109484, -0.028535863, -0.006563063, -0.0066280435, -0.010062744, -0.073558286, 0.059373904, 0.04730604, -0.02003266, -0.01198432, 0.024488486, -0.020923825, 0.037187595, -0.04715751, 0.059559565, -0.03395712, 0.043889903, 0.035126776, -0.036816277, 0.074857906, -0.06639183, 0.086146004, 0.07418953, -0.027069153, -0.019828433, 0.0077420007, -0.060265068, 0.061081972, -0.09030478, -0.0061406875, 0.0440013, -0.029724084, 0.032081958, -0.06739439, -0.015121966, -0.068471216, 0.014379327, -0.09386944, -0.0003707388, -0.05361846, -0.0019088116, -0.021889254, -0.007964792, -0.0065862704, -0.024247129, 0.049236897, 0.047417432, 0.0064470256, 0.026734967, -0.040362373, -0.0121699795, -0.036593486, 0.057591572, 0.012151414, -0.09134447, 0.008011207, 0.033808593, -0.01579034, -0.005165975, 0.026883494, 0.018073952, 0.03542383, 0.08540336, -0.04325866, 0.006669817, -0.023448793, -0.022242006, 0.011631567, 0.07199875, -0.08815113, -0.038282987, -0.017080672, -0.042404626, -0.04288734, 0.011492322, -0.03061525, -0.019085795, 0.08065048, -0.0023694793, -0.047046114, -0.011185984, 0.045226652, 0.015233361, -0.0073567573, -0.013822349, 0.037726007, -0.0284616, 0.013376766, -0.048048675, 0.015567548, -0.039062757, 0.069770835, 0.049756743, 0.0699565, 0.022873249, -0.018649496, -0.055140868, -0.072518595, -0.03167351, -0.019327153, 0.02337453, 0.0073242667, -0.022149177, -0.025026899, 0.00052419794, -0.028071715, -0.009501124, 0.0048039393, 0.022056347, 0.0063495543, 0.082952656, -0.05339567, 0.011770812, -0.00090683054, -0.041624855, -0.07797699, 0.013970876, -0.035535228, 0.0015409738, 0.029594122, 0.07626892, 0.02437709, 0.03973113, 0.001357635, -0.03670488, -0.006029292, 0.04207044, -0.07790272, -0.024766976, 0.03198913, 0.010916778, -0.06531501, 0.02809028, 0.14340338, -0.083398245, -0.022464799, -0.042293232, -0.012095716, 0.027013455, -0.01997696, 0.0955775, 0.022984644, -0.001954066, -0.050759304, -0.06368121, -0.009635727, 0.09654293, 0.043927036, -0.053841252, -0.054101173, 0.044966727, 0.016143093, -0.04370424, -0.028610127, -0.009533615, 0.013311786, -0.08488352, -0.030095402, -0.009190145, -0.029278502, -0.01844527, 0.024135733, 0.0002032101, -0.02396864, 0.023114607, -0.09386944, -0.008535695, -0.05499234, -0.024191432, -0.04266455, 0.0033697197, 0.005917896, 0.11703974, -0.015409738, 0.056106295, -0.07292704, -0.0690282, 0.0440013, 0.096394405, 0.023281699, 0.011566586, 0.003112117, 0.033845723, 0.011278814, -0.023671584, 0.08176444, 0.045523707, 0.03417991, 0.053915516, -0.041921914, 0.008535695, -0.03492255, -0.006985438, -0.032731768, 0.00080703857, -0.027718961, -0.04619208, -0.041699123, 0.021814989, 0.20793863, -0.05228171, -0.06100771, 0.060524993, -0.03564662, -0.05796289, 0.01672792, -0.048382863, -0.02053394, 0.031265058, -0.038208723, -0.0012369563, -0.052950084, 0.067617185, -0.025175426, 0.030299628, 0.035999376, -0.0032815314, 0.029445594, 0.033344444, -0.023578756, -0.06356981, -0.0011104759, -0.028944314, 0.040659428, -0.021517934, 0.04756596 ] } ], "model": "text-embedding-3-small", "usage": { "prompt_tokens": 9, "total_tokens": 9 } } headers: CF-RAY: - 939c17136f99e37d-NRT Connection: - keep-alive Content-Type: - application/json Date: - Sat, 03 May 2025 01:47:24 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-allow-origin: - '*' access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '11151' openai-model: - text-embedding-3-small openai-organization: test_openai_org_id openai-processing-ms: - '100' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload via: - envoy-router-964f95c58-6whsj x-envoy-upstream-service-time: - '53' x-ratelimit-limit-requests: - '5000' x-ratelimit-limit-tokens: - '5000000' x-ratelimit-remaining-requests: - '4999' x-ratelimit-remaining-tokens: - '4999987' x-ratelimit-reset-requests: - 12ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_25528908028913f5e0c09c82f660f73d status: code: 200 message: OK version: 1 test_async_embeddings_with_encoding_format.yaml000066400000000000000000000251271527070072300506060ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "This is a test for embeddings with encoding format", "model": "text-embedding-3-small", "encoding_format": "base64" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '127' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - Linux x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.9.2 method: POST uri: https://api.openai.com/v1/embeddings response: body: string: |- { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": "DK6fO8qEirxr7OE8SQSPvXpWIry659e8mJ5BPUKZiTxZdqM76WvmPDaVLT0xwge86TEXvU+lObzFhka8XkUfvNgvd7yb1d08OC0NuzMvyTshVB07MPgsvc7umDzYWhU9SjeBPBQdz7wB2Yu8gYuCPbZ9ST1078a8c4rZvDwyrrxnsRs8BRj8vNv2nrz3bA88HFZZvMXnCT2xSeC8Pp/vOuVimzyN/1K8kc5OvXAk9bvH6AA8UgsePWdMrrxvgQ69+Z8BvEE0HD2w4Ei9FbEEPUY9Zzyd0qq7ZbROvA6Azjx0Ho+8vbKpPITGyLsC3bW7mAMvPTXHKL3QIYu6mNRmvNYnozx078a6l295vQyuHz1frra6bYhrvP/bPj1O17Q8KCSQvJKcUzzXjBA9m9XduwBFVrwJSDs7RTk9vSS6gb2m2Tm8UXfoPL+6/bxyggW9mnDwOxBS/Tx6u4+94JMfvIIqv7xvgQ69/d7xvH1XGb08lxu9H7y9vBvtwTtOdnG8G1IvvJU4Xb06ZCm7+NUmPJg5VLzP8kI8y4i0u35fbbta2xC6arKSvGUVkjwU56m7Y0eNvT7ON7xc3Ae9C39Xu5drT7w4yJ88kmKEPVtEKLwLf1c86ZYEvdP3Y7ujDug7jf/SvHuJFLxWED89LyqovB2FIb1i6fy8IbkKulupFb0O5bu8UXdovOqaLrzeXIO8XNwHPRAYLj36o6u9gYsCvQjfo72zRi083S07vbmCaj1KbaY6oWsBvfaeCjz9Q1+8PgTdvKtHcjwVUMG9qqQLPQhEkTwW7308dLkhvAJ88jtoUFi9v+UbPGIUG73BHDi9H/LivEI4xrypEFY9MWHEu2N9MjmyQgM9NQF4PDsuhDrC6jy7kwFBvBfoID0tLds6o9QYvRVMF7x3VSu8PcqNvPM4przCu/S5kmIEvfVvwjqrqLW892yPvZJiBDvwAQq9mDnUvBAYLj3/2z68HBwKPCMmzLyLMU49mNRmPCGKwjycaRM9wuo8uwJCo7ypENY8r9yePQbiVjvgyUQ8cOolvXIhQj0B2Qu9CRIWPbkd/bzvCGe8tBAIPYTGyLxgsuA8Bd6sPFrbEL1BNBy9RzYKvdPBvrxkSze8e172PKPUmLzAGA48OMgfO3ZRgTv3ByI9fiUevYWQI7xS3FU8izHOvIbDlbyxSeA8nAhQPZg51DyuDpq9KvIUvAt/17zHuTi93sx3PUc2iryjOQa77GzdvNHvjztJo0s8HByKPE3TCrzGVEs7xFf+uyW+q7zT9+M8c+scvVDUAbqHYlI9nAhQu2AXTrx+X+08aRqzO/k+Pr31CtU8dO9GOuEy3DyS/ZY8W6kVvP48Ar1MP9W8c1AKve5pqrymqnE9YnmIPO02uDc6ms66l9C8PAKnkLya0bO6ptWPPFUMlbxMBYY8z1ewO0ptJrqD+EO9c8B+O0nZ8LzellI8L4+VPFWnJ70zL0k9oaGmvCpXgrz0axi7KvKUPK6pLL0xwoe9DuW7O43JLT3C5pK89j3HN052cbxbDgO8saojvfY5HTuoRnu7UXdovQJCIz3uaao8JLoBvbkdfTwTtDc80/fjO7GqIz3rZIm8XHcaPUQ1kzvLUg89FB1PPSj1R71rh/Q8ljGAvSW+Kz1XeVY9GVXiujsDZryx5HI8n2oKu51x5zw4Z1y9MPgsvDD4rDyrR3K8njtCPNBb2rpu7Vi8AnzyOeH8Nj1SQUO9kv2WPH28Bj0Ae/u8XqqMPPrdertGA5g8cyXsvPXULz20Slc9RTm9u6Q9sLwyKx+8RDUTvQwTjTyvd7E7qj+evOkG+bwQs8C8ogo+PWvs4TzIhz29TnJHPeaVjTvb9h49rq3WPBVMl7x7iZS7OfsRvGrot7xPQMw8/QmQPW+BDr0PFIQ8sniovEmjy7yqpAu9OAJvPKaq8bu24ja8Q6HdPIstpDx2wfW8DK4fvf1DXzw3/sQ8tXkfvI4yxbzChc872L+CvJ0MejsNfKS8eIgdOkOdMzmOkwg9IbkKPbewOz1WEL88M5Q2vYbDFb371h29BagHPA4b4Twbt5w8AQ8xPKcIgrxWdSw5h5h3PIpjSbq85KS86ALPvEai1DwvYE09SaPLPIJZB72xD5G6ua2IvGJOarxREnu9jpOIPN3IzbyhBhS7Qc8uPWVPYT2V/g086WvmPBGBRbxmfqm8Hx2BvO1lAD3S87m862SJPAp7LTveMWW56WvmvJloHDyuSOk8V3lWvFupFb3q/5u9WrDyO7lIm7x6VqK8CBlzPQsaarzP8kI7gYuCvRkbE71+igu8aOtqu1cU6TsVTJe8EeYyvfOdEz1iTuo8OMgfvDyXm7vTIgI8wksAPcK79LyhPDm9nXFnvJPLG72SnNO7xecJPQB7+zr3QfG8o3NVvKmraL0A4Oi8fI0+vYKPrDzwAQq9ecJsvLmCajy3FSk9KvKUvC33tTuhBpS91CYsvY4yxTzFvGs9e4mUvOJhJL02lS28EFL9PIHBJ7xhSsC8V3lWPSSPY7uP/B88b4GOPDP5o7yyQoM8oDgPvJdrz7zT9+O87mmqu+T5A73tZQC9gfv2uuZq77xzUAo+VnWsvE48Ir0yKx89HriTu6M5Br3vMwW8H1dQvWCyYLwVTBc8x1j1vJPLm7yBi4K8ZuMWPZ0M+ryMmmW8+zuLPFfewzxOoY8842n4PFQ+kDxAZhe9MV2avAcVybyY1OY7jJrlPIdi0rwB2Qu9iv5bPD83zzt/xNo7yoQKvSosZD1g4Si9DhvhvMa1jjy5fsC8zb9QvFZ1rLxFOb07ExmlPNVZnrzA6cW9CH7gvJeal7wYUbi8jf9SvGgWCT3YlGQ7aIZ9u6zbJ7zZ+dG7wYGlPDJlbjyOMkW8ukzFPO8IZ7wCfPI8MC5SvCAhq7t1vcs84saRPB3qjrnWwrW8CnutPPY5nTpjfbK8elaiPPgLTL3QvJ08FB3PPK2lAj1GPWe8O8kWvePKOztiTmo8c1CKPGKzVz3dkig9wOlFvTpkKT2mD986TAkwvIrINj0W7/28eyQnvYT87brnmTc8mnBwO4GLAr371h08KixkvKVwojyMmuW8A+HfO9sw7rtBNBw8xFd+PDb6GjxREvu86WvmOwbi1jw+BF28tn1JvHBPkzlOcsc80/djPNSLmbtmGTy9brezOw+vFjw+BN08CneDvNhaFb3mlQ09NGK7PDf+xDyKme68HFbZPLUUMjw3YzI9mDnUvC33NT2oRvs8O574uiGO7Lsjhw+8723UvPaeirsCeMg801zRuxyM/jvdkig7EkugPBbv/bugct47D6+WvGJO6rsGER88C39XO/pC6Dybnzg7VUK6O/M4pjwlIxk8CH5gvCGKwjsKdwM9SQQPvf1D37yH/eS7toHzvJ4FHbysrN88KcNMvSMmzLv3QfG42fnRPLQQiDxInyG7+AvMvBKwjTtyIUK8pabHu+DJRLyCj6w88qTwvOT5AzyF9ZC8bekuPHZRAb16u4+8wksAvKBy3rjHgxO9TAWGvKOp+jmuSGk6Ov+7vO02uDu5SJs8l5qXvZxpEzzqNcG8CnstPKM5hrwMSTK7zb9QPKHXS71GotQ8jJY7vexs3bxDZ447wksAvJX+Dby5HX28JPAmPEZohbwX6KC8RZqAPKaq8Tv/QCw9nZyFvC/FOruLLaS70e8Pvd3IzbwI36M8+t36uzZfiDzEHS89RAbLPPMCATymD187ak0lOyYnQzt3HwY9gSYVu64OGr3ezPe5PQAzvdkoGrzezPc8YrPXPH6KC7yWMYA7850TvP+lmTySYoQ8ZRWSPOtos7zn/qQ8Rj1nu7mtCLxm4xY8fI2+vFGiBrzO7pi776N5OmgWibyV02+8BRh8unwoUTv3ojS9st2VvFILHjydnIU8lzWqu1faGby651c8Oy4EPNJYJ7wO5Tu76wPGvC8qKLylcKK6o9hCO/nZUDwRHFg83vcVvaejFD0GER89JIs5vHmMRzwXg7O8gWBkvIBYEDzkM1O8tErXu4JZh7y3sLu8PzMlPePKO7y85KQ6H431PBNT9DuEJww9KFq1OfM8UDxfrja7c4rZO0TQpbwFfem7H/JiPKYP3zxxU707qEb7u1d51rt8KFE8FlRrPaBy3rzvo/m8gFiQPLewO73k+QM9K1ssPFytPzxQ1AE8wrv0vNLzObyRzk48NstSvBdNjjyUNDO8vB50vc2FAb01x6g79wciPe0Ak7vVWR48CnstvCos5LwJrai99weivIuSkTxcTPy80YoiPMlRmDy0gPy7Ca0oPNVZnrxasPI89NCFO1JBw7w7A+a4ntbUvHhdfzzRiqK8d1UrPbdLzrxUE/K8rq1WPMGBJbyNLhs85ceIPSfxnTye1lS9axuquVES+7uuc4e8QTQcvGNHDTuzq5q8F02OvCG5irwD4V88hl6oPOH8trycaRO8kv2WO4L0GT1/xFq6QwKhPGJO6rwJ4828GfB0vFN0NT01LJa7c8D+O4zFA7wCp5A83CkRvW2IazrkM1M5o6n6uI1kwLy5gmo83S07Pdn5UTy5Hf27WRE2vGsXAD3kmMC88qTwPA9KqTtUPpC7MmVuPA6287sl9FC8/UPfPBO0t7x+igu9mnBwPKULNbvuaSo9q0fyO2fnQL2ZzYm8p6MUPWnkDT3Zw6w6Z+dAPbBFNj0ovyI9rHa6uhYanDz3pl68x4OTO5X+jbxJo0u9v0oJvdkomrtAO3k8MWHEvIQnjLwuXKM8H411uBmAgLxOdnE8njtCveGXybo1kYO8yh8dPSOHjzz93nG8OGfcvKgMLL1n50C8RmgFPC2SyDzellK6dFS0OhJPyjsZVWI8MWHEO5pwcLxmfim9/QkQvEo3AbzEghw9G+1BPHcfBrtDPHC8BNqCPdbCtTxREvu88aBGvXAkdTqYA687v+UbOuGXyby8SZI8URJ7vAEPMT3szSC89p6KvQPh3zyWZ6U8uHqWu9z6SDy05Wm9IvPZu/UKVTzdyM28pnTMvHMl7LvH6IC8zfV1PLOrGr2/H+s8sniou5X+Db25fkC8dYemPGIUm7wTtDe8LfOLPAkSlrzeXIO7v0oJPOtoszwtksg80r0UOgp7LTyabMa87NFKvAsa6jxBoGa8IIaYvB8dAT1aFeC8tID8uxhROL2pq2g8wuq8O1gNDLtOdvE8jPuoPBSCvDuSYoQ8elaiPF9N8zrAsyA9qavoPEbYebqcCFA825XbORu3HD2eoC87xYZGvYb5Oru7Gso8J5DavG5SxjyBJpU78m5LPIn6sTuBxdG8bIAXu/emXjya0TM7oaGmvPemXj3S87k7GbrPO95cg7wtksi8tRQyu+zNoDzTIoI8Ov+7vInEjDxnTC482cMsPD6f7zwghpg7v0oJvf3ecTyPNm+9QaBmvEBmlzwB2Ys8WA2MuhtSrzyxSeC8KPXHu1Z1LDykoh08LyooPTJlbrzxzw47nQx6O/vWHb1I1UY7MsaxPEo3Ab1LOys8PcoNPGJO6rzcxCO9VD4QPQtFiLxX3kM87NFKOwh+YDySN+a8A+FfuiqRUbz52VC7KFo1PfRrGDz1b8I8+m0GPGuH9LvgLrK7LyoovSfGfzzGtQ48IIaYvGYZPLy6sTI9rkhpO0w/1TplsKQ8kpzTPAbiVr0Ae3s7aBYJPEo3gbzZ+dG8VnGCvE48IjxeSck88zxQPXOKWTuD+EO71vhau0KZCTuuSGm8a+xhPDqaTj0ZVWI7txWpvNb4Wrx5J9o8WrDyulfewzxjRw297ZulvH+ONTypq2g8QGYXO/pthryvQYy7fMNjvJrRs7xPpTm6WtsQPci94ruE/O28xeuzPKYP3ztiTuo8Qc8uvCS6gTwLGmq8cIU4vBVMlzzoY5I8Pp/vvOH4DL2AWBC8PzOluTWRAzzvo/k8ybYFvKM5Bj3eMeU76v8bOyS6gbxxUz27786Xu6mr6LwE2gK8SzsrvD2bxTzZw6w6nGmTPFLcVT0TU3S8EbfqPIRh2zxmGTw88aBGvJ9qCrsyKx+8j2ENvbDgSDqbOku8IiIivMKFT722gXO8bSP+u/ShvTypdcO8Sm0mO4X1kLyv3B48l295O/TQBT0vKig8YxjFOzbLUj0TfpK8HSA0vKgMLDzlxwg8c4pZu70Xl7zraLM8ptk5vbQQiLyxD5G5hZAjPd4xZbvellK8kpzTPHdVqzw2X4i8rECVvFtEKDxwJHU6zSCUO3G4KrvrZIm6bYhrPD83T7xVQrq7eL7CvEABqjxfE6Q8EFL9PNz6yLzblds8ntZUOQtFCDxUPpA83jFlPP7XFL34OhQ9OfsRu4qZbrytpYK8RZqAvFeve7sST0q8ST7evGexG7yacHC7F4Mzvd0tuzxkSzc8+qfVOgu1/DzI7Cq9CH7gO1gNDD3qmi69723Uuo4yRTyxqiM9qRBWvNP34zwZgAA9mWicvB/y4jyHYlK81pPturJCg7veMWU8wBiOPDPKWz36p9U8UXfoPKbVj7ydcee8iv7bN80glDyeBZ08lgI4vMAYDr37Ows9BhGfvGqDSjpqTaU72JRkPZnNCbxi6fw7SzsrPNEltbxmfqk86pouPaQHC7uY/4S8X01zO8jsqjrvbdS7+HA5vB0gtDuAXLq8ciFCPTRekTtg4ag8LyooPGGrA72VOF09imPJO6DTIbwhVJ29hGFbO4HF0Tv1CtU8CxrqO4X1ELxs5YQ8cLSAunwo0bw0XhE9+DqUvGCy4Drb9p485JjAvDpkqbsvYE08TW4dPWKvrbwHerY88QW0vBm2Jby1FDI7/jwCO9BbWrxV3Uw9XbFpvI3/Uru35uA7cbgqPRvtQTr2PUc8CRIWvRxW2Tsqx/Y8nQz6OwYRnzxk5sk8ISl/POEyXD0PSqm63yqIu/RrmLjTXFG9h2JSO3UiObyhawG9hfWQvJLS+DxJPt48v0qJPFF3aDzqNUE8jcmtPFtEqLzWXci7Y+Ifu9wpkbzun888MPisux5TJjwCfPI8gcGnvJbMEjuhBpQ8P5iSPEBml7sZ8PQ8e7+5PIz7qLwBdB69ee0KPOlr5jzOJD69oHLeuydWizxaek28VHjfvJc1qjuuqay79aXnPGtRzzzVvou8t7C7OrnjLTlLcVA8blLGu5EzvLwARda7blLGvKIKvrt9W0O9THV6vH76f7zyCV48yL3iu2kas7ttiOs8aLVFu1FzPj3raLM8coKFvJGYqTuM+6i8dx+GOgh+YD296M68ST5eOgWoB7sSSyA9mAMvu52cBTwIRJG8oaEmvMjsqryvEsQ738WaPGpNpTxygoW8dLmhvHiIHTzQkX88iV+fvP3e8Tq7ew08nZyFvCcrbb3YvwK97TY4PIb5ujvQW1q83sz3u+/OFz1dsWk89aVnvIGLAr3sMg68fMPjPIFgZDzwAQq7WaxIuoGLgrkBdB48xlChvOH4jLyBJhU8pqrxPIfHv7v63fq8KSg6vdtbjLsahKq8C7X8OjD4rLx3Vau8axcAvUGgZruMxQM976N5uzP5Izx6uw+9qRBWu3QeDzyXNSq81PAGPW1OHDx/xNq8HeoOvW1OnLzrZIm8kpzTPCpXgjwhVJ28NZGDO3bwPb2s2ye7Hu44vBZU67yz4T+7hyiDPGLp/LuYOdQ7paZHPF17RLyMmmW8Hx0BO3nCbLzmMCC9w0+qvIBcurxpf6A8e+6BvHRUtDwG4ta8fVtDvPY9x7zn/iQ9U3ALPYD3TDvTknY6F02OuiIiorxbDgM9HPFrPP1D3zwn8R296jVBPKaq8TuurVY7KPXHPLOrmjx1vUu8eYxHPH/EWjwwLtI8kC8SvEx1ejw0XhE8yh+dujto0zwR4gi94JMfPT7ON7ymqvE8XbHpPE528Ty546282JRkPByMfjw0/U09xFd+vJrRM7tFOb080CGLPKl1Q70kKva86GOSPOc0SjulcCK8RgMYPDvJFrrkzuW8Ps63vCgkkDwWGhy9mJ5BO1qwcjwvZPc8DRe3vIz7qLyKme48/g26PBjsSjq5rQg82PWnuofHP7ww9AK6EFJ9PPg6lLyaNiG8uR39vBlV4rwEdZW7ogq+PCqR0bw2lS2962gzPZ2cBT2abMa8brezNy3zi7xfeJG9J/EdvBeDMzuAXDq9/d5xPN9grTshVB29ylnsui0tW7o1kYM81fQwPMDpxby54y08" } ], "model": "text-embedding-3-small", "usage": { "prompt_tokens": 9, "total_tokens": 9 } } headers: CF-RAY: - 939c1836de294a26-NRT Connection: - keep-alive Content-Type: - application/json Date: - Sat, 03 May 2025 01:48:10 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-allow-origin: - '*' access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '8414' openai-model: - text-embedding-3-small openai-organization: test_openai_org_id openai-processing-ms: - '82' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload via: - envoy-router-698696864c-zvnz4 x-envoy-upstream-service-time: - '52' x-ratelimit-limit-requests: - '5000' x-ratelimit-limit-tokens: - '5000000' x-ratelimit-remaining-requests: - '4999' x-ratelimit-remaining-tokens: - '4999987' x-ratelimit-reset-requests: - 12ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_f9b3fad2c1ee8d52c777edfa84b4cfff status: code: 200 message: OK version: 1 test_chat_completion_404.yaml000066400000000000000000000040231527070072300445660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "this-model-does-not-exist" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '103' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.54.3 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.54.3 x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.6 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "error": { "message": "The model `this-model-does-not-exist` does not exist or you do not have access to it.", "type": "invalid_request_error", "param": null, "code": "model_not_found" } } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1225a16c283d93-SIN Connection: - keep-alive Content-Type: - application/json; charset=utf-8 Date: - Mon, 11 Nov 2024 23:43:52 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff alt-svc: - h3=":443"; ma=86400 content-length: - '231' openai-organization: test_openai_org_id strict-transport-security: - max-age=31536000; includeSubDomains; preload vary: - Origin x-request-id: - req_c3e0f92d7b5426d1a4a17bb3d39953ea status: code: 404 message: Not Found version: 1 test_chat_completion_extra_params.yaml000066400000000000000000000065511527070072300467550ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4o-mini", "max_tokens": 50, "response_format": { "type": "text" }, "seed": 42, "stream": false, "temperature": 0.5, "service_tier": "default" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '220' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.7 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-AbMH70fQA9lMPIClvBPyBSjqJBm9F", "object": "chat.completion", "created": 1733467125, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "This is a test. How can I assist you further?", "refusal": null }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 12, "total_tokens": 24, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "service_tier": "default", "system_fingerprint": "fp_0705bf87c0" } headers: CF-RAY: - 8eda465e8fe9e58c-KUL Connection: - keep-alive Content-Type: - application/json Date: - Fri, 06 Dec 2024 06:38:46 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '825' openai-organization: test_openai_org_id openai-processing-ms: - '558' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9998' x-ratelimit-remaining-tokens: - '199943' x-ratelimit-reset-requests: - 12.967s x-ratelimit-reset-tokens: - 16ms x-request-id: - req_22ff608d47a299f0780f52360631eabb status: code: 200 message: OK version: 1 test_chat_completion_handle_stop_sequences_as_string.yaml000066400000000000000000000067131527070072300527130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4o-mini", "stop": "stop" } headers: Accept: - application/json Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '105' Content-Type: - application/json Host: - api.openai.com User-Agent: - OpenAI/Python 1.109.1 X-Stainless-Arch: - x64 X-Stainless-Async: - 'false' X-Stainless-Lang: - python X-Stainless-OS: - Linux X-Stainless-Package-Version: - 1.109.1 X-Stainless-Runtime: - CPython X-Stainless-Runtime-Version: - 3.12.12 authorization: - Bearer test_openai_api_key x-stainless-read-timeout: - '600' x-stainless-retry-count: - '0' method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-Clubs1bbZwGUeDKpnPUWDMEhSbquh", "object": "chat.completion", "created": 1765535060, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "This is a test. How can I assist you further?", "refusal": null, "annotations": [] }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 12, "total_tokens": 24, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "service_tier": "default", "system_fingerprint": "fp_11f3029f6b" } headers: CF-RAY: - 9acc82e96fde4bf3-MXP Connection: - keep-alive Content-Type: - application/json Date: - Fri, 12 Dec 2025 10:24:20 GMT Server: - cloudflare Set-Cookie: test_set_cookie Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '852' openai-organization: test_openai_org_id openai-processing-ms: - '500' openai-project: - proj_Pf1eM5R55Z35wBy4rt8PxAGq openai-version: - '2020-10-01' x-envoy-upstream-service-time: - '826' x-openai-proxy-wasm: - v0.1 x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '10000000' x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - '9999993' x-ratelimit-reset-requests: - 6ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_993163a7581641b7b9aee6e03ef4ca3a status: code: 200 message: OK version: 1 test_chat_completion_handles_not_given.yaml000066400000000000000000000067121527070072300477540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4o-mini", "stream": false } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '106' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.109.1 x-stainless-arch: - x64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - Linux x-stainless-package-version: - 1.109.1 x-stainless-read-timeout: - '600' x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.10.12 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-CZDvsSHdMnAgYuQ8J81NMgOK2wfam", "object": "chat.completion", "created": 1762511072, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "This is a test. How can I assist you today?", "refusal": null, "annotations": [] }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 12, "total_tokens": 24, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "service_tier": "default", "system_fingerprint": "fp_560af6e559" } headers: CF-RAY: - 99ac1f128834ed5e-MXP Connection: - keep-alive Content-Type: - application/json Date: - Fri, 07 Nov 2025 10:24:32 GMT Server: - cloudflare Set-Cookie: test_set_cookie Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '850' openai-organization: test_openai_org_id openai-processing-ms: - '512' openai-project: - proj_Pf1eM5R55Z35wBy4rt8PxAGq openai-version: - '2020-10-01' x-envoy-upstream-service-time: - '797' x-openai-proxy-wasm: - v0.1 x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '10000000' x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - '9999993' x-ratelimit-reset-requests: - 6ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_9eac1833161f4ac89019c12f24002ef4 status: code: 200 message: OK version: 1 test_chat_completion_metrics.yaml000066400000000000000000000063041527070072300457310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4o-mini", "stream": false } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '106' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.54.3 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.54.3 x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.6 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASYMQRl3A3DXL9FWCK9tnGRcKIO7q", "object": "chat.completion", "created": 1731368630, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "This is a test.", "refusal": null }, "logprobs": null, "finish_reason": "stop" } ], "service_tier": "default", "usage": { "prompt_tokens": 12, "completion_tokens": 5, "total_tokens": 17, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_0ba0d124f1" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e122593ff368bc8-SIN Connection: - keep-alive Content-Type: - application/json Date: - Mon, 11 Nov 2024 23:43:50 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '765' openai-organization: test_openai_org_id openai-processing-ms: - '287' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - '199977' x-ratelimit-reset-requests: - 8.64s x-ratelimit-reset-tokens: - 6ms x-request-id: - req_58cff97afd0e7c0bba910ccf0b044a6f status: code: 200 message: OK version: 1 test_chat_completion_multiple_choices.yaml000066400000000000000000000070141527070072300476120ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4o-mini", "n": 2, "stream": false } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '114' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.54.3 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.54.3 x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.6 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASYMUBq69UHDarAz2fsd0O50rv0r1", "object": "chat.completion", "created": 1731368634, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "This is a test. How can I assist you further?", "refusal": null }, "logprobs": null, "finish_reason": "stop" }, { "index": 1, "message": { "role": "assistant", "content": "This is a test. How can I assist you further?", "refusal": null }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 24, "total_tokens": 36, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_0ba0d124f1" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1225a91a253e53-SIN Connection: - keep-alive Content-Type: - application/json Date: - Mon, 11 Nov 2024 23:43:54 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '1030' openai-organization: test_openai_org_id openai-processing-ms: - '399' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9997' x-ratelimit-remaining-tokens: - '199962' x-ratelimit-reset-requests: - 22.564s x-ratelimit-reset-tokens: - 11ms x-request-id: - req_01290a92a1a3d787c7a00bb3836da597 status: code: 200 message: OK version: 1 test_chat_completion_multiple_choices_streaming.yaml000066400000000000000000001025741527070072300516720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "system", "content": "You're a helpful assistant." }, { "role": "user", "content": "What's the weather in Seattle and San Francisco today?" } ], "model": "gpt-4o-mini", "n": 2, "stream": true, "stream_options": { "include_usage": true } } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '254' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.54.3 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.54.3 x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.6 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |+ data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":"I'm"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":"I'm"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" unable"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" unable"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" provide"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" provide"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" real"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" real"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":"-time"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":"-time"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" weather"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" weather"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" updates"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" updates"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" as"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" To"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" my"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" get"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" capabilities"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" do"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" latest"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" not"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" weather"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" include"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" information"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" accessing"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" for"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" live"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" Seattle"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" data"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" San"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" However"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" Francisco"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" I"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" can"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" recommend"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" easily"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" checking"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" check"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" reliable"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" current"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" weather"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" weather"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" website"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" Seattle"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" using"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" San"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" weather"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" Francisco"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" app"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" using"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" You"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" weather"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" can"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" also"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" website"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" ask"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" app"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" voice"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" assistant"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" service"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" search"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" Would"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" online"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" for"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" like"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" some"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" current"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" tips"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" weather"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" on"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":" conditions"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" where"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" find"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" this"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":" information"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{"content":"?"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[{"index":1,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} data: {"id":"chatcmpl-ASYMaNc7XmbGRUNREnmvhyyISBHsv","object":"chat.completion.chunk","created":1731368640,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_0ba0d124f1","choices":[],"usage":{"prompt_tokens":26,"completion_tokens":104,"total_tokens":130,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} data: [DONE] headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1225d0fa1481e4-SIN Connection: - keep-alive Content-Type: - text/event-stream; charset=utf-8 Date: - Mon, 11 Nov 2024 23:44:00 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 openai-organization: test_openai_org_id openai-processing-ms: - '176' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9993' x-ratelimit-remaining-tokens: - '199945' x-ratelimit-reset-requests: - 59.369s x-ratelimit-reset-tokens: - 16ms x-request-id: - req_892a6021b1acc00254d8ff80adcd82fb status: code: 200 message: OK version: 1 test_chat_completion_multiple_tools_streaming_no_content.yaml000066400000000000000000000221051527070072300536320ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "system", "content": "You're a helpful assistant." }, { "role": "user", "content": "What's the weather in Seattle and San Francisco today?" } ], "model": "gpt-4o-mini", "stream": true, "stream_options": { "include_usage": true }, "tool_choice": "auto", "tools": [ { "type": "function", "function": { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. Boston, MA" } }, "required": [ "location" ], "additionalProperties": false } } } ] } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '602' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.54.3 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.54.3 x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.6 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |+ data: {"id":"chatcmpl-ASYMdbuQwnGMCODejEnbp2ufs6WgR","object":"chat.completion.chunk","created":1731368643,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"role":"assistant","content":null},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMdbuQwnGMCODejEnbp2ufs6WgR","object":"chat.completion.chunk","created":1731368643,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_j7L00WV19wwQCvKOIVZewXZm","type":"function","function":{"name":"get_current_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMdbuQwnGMCODejEnbp2ufs6WgR","object":"chat.completion.chunk","created":1731368643,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMdbuQwnGMCODejEnbp2ufs6WgR","object":"chat.completion.chunk","created":1731368643,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMdbuQwnGMCODejEnbp2ufs6WgR","object":"chat.completion.chunk","created":1731368643,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n\": \"S"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMdbuQwnGMCODejEnbp2ufs6WgR","object":"chat.completion.chunk","created":1731368643,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"eatt"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMdbuQwnGMCODejEnbp2ufs6WgR","object":"chat.completion.chunk","created":1731368643,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"le, W"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMdbuQwnGMCODejEnbp2ufs6WgR","object":"chat.completion.chunk","created":1731368643,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"A\"}"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMdbuQwnGMCODejEnbp2ufs6WgR","object":"chat.completion.chunk","created":1731368643,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"id":"call_sAIOI3dvcd1YMsEqD8DI3l8B","type":"function","function":{"name":"get_current_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMdbuQwnGMCODejEnbp2ufs6WgR","object":"chat.completion.chunk","created":1731368643,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMdbuQwnGMCODejEnbp2ufs6WgR","object":"chat.completion.chunk","created":1731368643,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMdbuQwnGMCODejEnbp2ufs6WgR","object":"chat.completion.chunk","created":1731368643,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"n\": \"S"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMdbuQwnGMCODejEnbp2ufs6WgR","object":"chat.completion.chunk","created":1731368643,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"an F"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMdbuQwnGMCODejEnbp2ufs6WgR","object":"chat.completion.chunk","created":1731368643,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"ranci"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMdbuQwnGMCODejEnbp2ufs6WgR","object":"chat.completion.chunk","created":1731368643,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"sco, C"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMdbuQwnGMCODejEnbp2ufs6WgR","object":"chat.completion.chunk","created":1731368643,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"A\"}"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMdbuQwnGMCODejEnbp2ufs6WgR","object":"chat.completion.chunk","created":1731368643,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null} data: {"id":"chatcmpl-ASYMdbuQwnGMCODejEnbp2ufs6WgR","object":"chat.completion.chunk","created":1731368643,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","choices":[],"usage":{"prompt_tokens":75,"completion_tokens":51,"total_tokens":126,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} data: [DONE] headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1225e40bed40c8-SIN Connection: - keep-alive Content-Type: - text/event-stream; charset=utf-8 Date: - Mon, 11 Nov 2024 23:44:04 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 openai-organization: test_openai_org_id openai-processing-ms: - '1225' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9991' x-ratelimit-remaining-tokens: - '199961' x-ratelimit-reset-requests: - 1m13.588s x-ratelimit-reset-tokens: - 11ms x-request-id: - req_102cd878d03c1cec0c12b95f928d03ee status: code: 200 message: OK version: 1 test_chat_completion_multiple_tools_streaming_with_content.yaml000066400000000000000000000221041527070072300541700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "system", "content": "You're a helpful assistant." }, { "role": "user", "content": "What's the weather in Seattle and San Francisco today?" } ], "model": "gpt-4o-mini", "stream": true, "stream_options": { "include_usage": true }, "tool_choice": "auto", "tools": [ { "type": "function", "function": { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. Boston, MA" } }, "required": [ "location" ], "additionalProperties": false } } } ] } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '602' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.54.3 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.54.3 x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.6 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |+ data: {"id":"chatcmpl-ASYMbACebDoWcuraMEWQhU48q4dAp","object":"chat.completion.chunk","created":1731368641,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"role":"assistant","content":null},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMbACebDoWcuraMEWQhU48q4dAp","object":"chat.completion.chunk","created":1731368641,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_fHCjJqt9Pysde6vcJcvbXGBx","type":"function","function":{"name":"get_current_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMbACebDoWcuraMEWQhU48q4dAp","object":"chat.completion.chunk","created":1731368641,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMbACebDoWcuraMEWQhU48q4dAp","object":"chat.completion.chunk","created":1731368641,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMbACebDoWcuraMEWQhU48q4dAp","object":"chat.completion.chunk","created":1731368641,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n\": \"S"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMbACebDoWcuraMEWQhU48q4dAp","object":"chat.completion.chunk","created":1731368641,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"eatt"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMbACebDoWcuraMEWQhU48q4dAp","object":"chat.completion.chunk","created":1731368641,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"le, W"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMbACebDoWcuraMEWQhU48q4dAp","object":"chat.completion.chunk","created":1731368641,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"A\"}"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMbACebDoWcuraMEWQhU48q4dAp","object":"chat.completion.chunk","created":1731368641,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"id":"call_3J9foSw3CUb48lrqIXoTky6U","type":"function","function":{"name":"get_current_weather","arguments":""}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMbACebDoWcuraMEWQhU48q4dAp","object":"chat.completion.chunk","created":1731368641,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\"lo"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMbACebDoWcuraMEWQhU48q4dAp","object":"chat.completion.chunk","created":1731368641,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"catio"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMbACebDoWcuraMEWQhU48q4dAp","object":"chat.completion.chunk","created":1731368641,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"n\": \"S"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMbACebDoWcuraMEWQhU48q4dAp","object":"chat.completion.chunk","created":1731368641,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"an F"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMbACebDoWcuraMEWQhU48q4dAp","object":"chat.completion.chunk","created":1731368641,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"ranci"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMbACebDoWcuraMEWQhU48q4dAp","object":"chat.completion.chunk","created":1731368641,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"sco, C"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMbACebDoWcuraMEWQhU48q4dAp","object":"chat.completion.chunk","created":1731368641,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"A\"}"}}]},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMbACebDoWcuraMEWQhU48q4dAp","object":"chat.completion.chunk","created":1731368641,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null} data: {"id":"chatcmpl-ASYMbACebDoWcuraMEWQhU48q4dAp","object":"chat.completion.chunk","created":1731368641,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_9b78b61c52","choices":[],"usage":{"prompt_tokens":75,"completion_tokens":51,"total_tokens":126,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} data: [DONE] headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1225d8af0c3d93-SIN Connection: - keep-alive Content-Type: - text/event-stream; charset=utf-8 Date: - Mon, 11 Nov 2024 23:44:03 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 openai-organization: test_openai_org_id openai-processing-ms: - '1399' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9992' x-ratelimit-remaining-tokens: - '199961' x-ratelimit-reset-requests: - 1m6.779s x-ratelimit-reset-tokens: - 11ms x-request-id: - req_d745d48bf030ac78e4790ee49848d05f status: code: 200 message: OK version: 1 test_chat_completion_n_1_is_not_reported.yaml000066400000000000000000000067001527070072300502170ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4o-mini", "n": 1 } headers: Accept: - application/json Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '97' Content-Type: - application/json Host: - api.openai.com User-Agent: - OpenAI/Python 1.109.1 X-Stainless-Arch: - x64 X-Stainless-Async: - 'false' X-Stainless-Lang: - python X-Stainless-OS: - Linux X-Stainless-Package-Version: - 1.109.1 X-Stainless-Runtime: - CPython X-Stainless-Runtime-Version: - 3.12.12 authorization: - Bearer test_openai_api_key x-stainless-read-timeout: - '600' x-stainless-retry-count: - '0' method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ClubqNLub25QPdqxjOslny04PLCYZ", "object": "chat.completion", "created": 1765535058, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "This is a test. How can I assist you today?", "refusal": null, "annotations": [] }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 12, "total_tokens": 24, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "service_tier": "default", "system_fingerprint": "fp_11f3029f6b" } headers: CF-RAY: - 9acc82ddddd4edba-MXP Connection: - keep-alive Content-Type: - application/json Date: - Fri, 12 Dec 2025 10:24:19 GMT Server: - cloudflare Set-Cookie: test_set_cookie Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '850' openai-organization: test_openai_org_id openai-processing-ms: - '405' openai-project: - proj_Pf1eM5R55Z35wBy4rt8PxAGq openai-version: - '2020-10-01' x-envoy-upstream-service-time: - '529' x-openai-proxy-wasm: - v0.1 x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '10000000' x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - '9999993' x-ratelimit-reset-requests: - 6ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_2b15771f24a4465fb5fc43b797f39e04 status: code: 200 message: OK version: 1 test_chat_completion_no_content.yaml000066400000000000000000000062371527070072300464360ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4o-mini", "stream": false } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '106' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.54.3 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.54.3 x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.6 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASYMQRl3A3DXL9FWCK9tnGRcKIO7q", "object": "chat.completion", "created": 1731368630, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "This is a test.", "refusal": null }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 5, "total_tokens": 17, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_0ba0d124f1" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e122593ff368bc8-SIN Connection: - keep-alive Content-Type: - application/json Date: - Mon, 11 Nov 2024 23:43:50 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '765' openai-organization: test_openai_org_id openai-processing-ms: - '287' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - '199977' x-ratelimit-reset-requests: - 8.64s x-ratelimit-reset-tokens: - 6ms x-request-id: - req_58cff97afd0e7c0bba910ccf0b044a6f status: code: 200 message: OK version: 1 test_chat_completion_streaming.yaml000066400000000000000000000107261527070072300462570ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4", "stream": true, "stream_options": { "include_usage": true } } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '142' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.54.3 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.54.3 x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.6 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |+ data: {"id":"chatcmpl-ASYMZ4oSykiIFK4lXLReDiKyAjsQl","object":"chat.completion.chunk","created":1731368639,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMZ4oSykiIFK4lXLReDiKyAjsQl","object":"chat.completion.chunk","created":1731368639,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"\"This"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMZ4oSykiIFK4lXLReDiKyAjsQl","object":"chat.completion.chunk","created":1731368639,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMZ4oSykiIFK4lXLReDiKyAjsQl","object":"chat.completion.chunk","created":1731368639,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMZ4oSykiIFK4lXLReDiKyAjsQl","object":"chat.completion.chunk","created":1731368639,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" test"},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMZ4oSykiIFK4lXLReDiKyAjsQl","object":"chat.completion.chunk","created":1731368639,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":".\""},"logprobs":null,"finish_reason":null}],"usage":null} data: {"id":"chatcmpl-ASYMZ4oSykiIFK4lXLReDiKyAjsQl","object":"chat.completion.chunk","created":1731368639,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} data: {"id":"chatcmpl-ASYMZ4oSykiIFK4lXLReDiKyAjsQl","object":"chat.completion.chunk","created":1731368639,"model":"gpt-4-0613","system_fingerprint":null,"choices":[],"usage":{"prompt_tokens":12,"completion_tokens":5,"total_tokens":17,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} data: [DONE] headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1225c87b273e53-SIN Connection: - keep-alive Content-Type: - text/event-stream; charset=utf-8 Date: - Mon, 11 Nov 2024 23:43:59 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 openai-organization: test_openai_org_id openai-processing-ms: - '207' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '10000' x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - '9978' x-ratelimit-reset-requests: - 8.64s x-ratelimit-reset-tokens: - 132ms x-request-id: - req_c367cf360ee88481fb7cd6c5d45bf9dc status: code: 200 message: OK version: 1 test_chat_completion_streaming_not_complete.yaml000066400000000000000000000075741527070072300510360ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4", "stream": true } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '99' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.54.3 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.54.3 x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.6 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |+ data: {"id":"chatcmpl-ASYMZbRqo8Bkz53FVzaTj7W7feOn4","object":"chat.completion.chunk","created":1731368639,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMZbRqo8Bkz53FVzaTj7W7feOn4","object":"chat.completion.chunk","created":1731368639,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"This"},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMZbRqo8Bkz53FVzaTj7W7feOn4","object":"chat.completion.chunk","created":1731368639,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMZbRqo8Bkz53FVzaTj7W7feOn4","object":"chat.completion.chunk","created":1731368639,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMZbRqo8Bkz53FVzaTj7W7feOn4","object":"chat.completion.chunk","created":1731368639,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" test"},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMZbRqo8Bkz53FVzaTj7W7feOn4","object":"chat.completion.chunk","created":1731368639,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-ASYMZbRqo8Bkz53FVzaTj7W7feOn4","object":"chat.completion.chunk","created":1731368639,"model":"gpt-4-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]} data: [DONE] headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1225ccb98e823b-SIN Connection: - keep-alive Content-Type: - text/event-stream; charset=utf-8 Date: - Mon, 11 Nov 2024 23:43:59 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 openai-organization: test_openai_org_id openai-processing-ms: - '205' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '10000' x-ratelimit-remaining-requests: - '9998' x-ratelimit-remaining-tokens: - '9978' x-ratelimit-reset-requests: - 16.601s x-ratelimit-reset-tokens: - 132ms x-request-id: - req_d13225164a822ec3ebab5591ed0b6d6a status: code: 200 message: OK version: 1 test_chat_completion_tool_calls_no_content.yaml000066400000000000000000000217631527070072300506520ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "system", "content": "You're a helpful assistant." }, { "role": "user", "content": "What's the weather in Seattle and San Francisco today?" } ], "model": "gpt-4o-mini", "tool_choice": "auto", "tools": [ { "type": "function", "function": { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. Boston, MA" } }, "required": [ "location" ], "additionalProperties": false } } } ] } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '543' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.54.3 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.54.3 x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.6 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASYMW6w3m9qqpHUVhYTbQbw61zMqA", "object": "chat.completion", "created": 1731368636, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": null, "tool_calls": [ { "id": "call_eqbDFUdPqay2WjsSzZEiAn0U", "type": "function", "function": { "name": "get_current_weather", "arguments": "{\"location\": \"Seattle, WA\"}" } }, { "id": "call_tn3sgasg6GaftTdancBYJNJN", "type": "function", "function": { "name": "get_current_weather", "arguments": "{\"location\": \"San Francisco, CA\"}" } } ], "refusal": null }, "logprobs": null, "finish_reason": "tool_calls" } ], "usage": { "prompt_tokens": 75, "completion_tokens": 51, "total_tokens": 126, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_0ba0d124f1" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1225ba6a3440b0-SIN Connection: - keep-alive Content-Type: - application/json Date: - Mon, 11 Nov 2024 23:43:57 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '1308' openai-organization: test_openai_org_id openai-processing-ms: - '761' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9994' x-ratelimit-remaining-tokens: - '199961' x-ratelimit-reset-requests: - 45.694s x-ratelimit-reset-tokens: - 11ms x-request-id: - req_f98710550c77b43865572fc1a7a0bc53 status: code: 200 message: OK - request: body: |- { "messages": [ { "role": "system", "content": "You're a helpful assistant." }, { "role": "user", "content": "What's the weather in Seattle and San Francisco today?" }, { "role": "assistant", "tool_calls": [ { "id": "call_eqbDFUdPqay2WjsSzZEiAn0U", "function": { "arguments": "{\"location\": \"Seattle, WA\"}", "name": "get_current_weather" }, "type": "function" }, { "id": "call_tn3sgasg6GaftTdancBYJNJN", "function": { "arguments": "{\"location\": \"San Francisco, CA\"}", "name": "get_current_weather" }, "type": "function" } ] }, { "role": "tool", "content": "50 degrees and raining", "tool_call_id": "call_eqbDFUdPqay2WjsSzZEiAn0U" }, { "role": "tool", "content": "70 degrees and sunny", "tool_call_id": "call_tn3sgasg6GaftTdancBYJNJN" } ], "model": "gpt-4o-mini" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '746' content-type: - application/json cookie: - test_cookie host: - api.openai.com user-agent: - OpenAI/Python 1.54.3 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.54.3 x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.6 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASYMYObbcUyZ77rbvypWmcZPIVSf1", "object": "chat.completion", "created": 1731368638, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Today, the weather in Seattle is 50 degrees and raining, while San Francisco is enjoying 70 degrees and sunny weather.", "refusal": null }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 99, "completion_tokens": 25, "total_tokens": 124, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_0ba0d124f1" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1225c12f6340b0-SIN Connection: - keep-alive Content-Type: - application/json Date: - Mon, 11 Nov 2024 23:43:58 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '870' openai-organization: test_openai_org_id openai-processing-ms: - '722' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9993' x-ratelimit-remaining-tokens: - '199948' x-ratelimit-reset-requests: - 53.254s x-ratelimit-reset-tokens: - 15ms x-request-id: - req_2bb3d10a2a75f5d64a24aef595a0d8dd status: code: 200 message: OK version: 1 test_chat_completion_tool_calls_with_content.yaml000066400000000000000000000217501527070072300512050ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "system", "content": "You're a helpful assistant." }, { "role": "user", "content": "What's the weather in Seattle and San Francisco today?" } ], "model": "gpt-4o-mini", "tool_choice": "auto", "tools": [ { "type": "function", "function": { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. Boston, MA" } }, "required": [ "location" ], "additionalProperties": false } } } ] } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '543' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.54.3 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.54.3 x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.6 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASYMU9Ntix7ePttk0MSuerJstef6U", "object": "chat.completion", "created": 1731368634, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": null, "tool_calls": [ { "id": "call_JpNb8OiAkbIbHzDggfpdDHpi", "type": "function", "function": { "name": "get_current_weather", "arguments": "{\"location\": \"Seattle, WA\"}" } }, { "id": "call_vaFQc3zK6hHTRZKXRI5Eo2cJ", "type": "function", "function": { "name": "get_current_weather", "arguments": "{\"location\": \"San Francisco, CA\"}" } } ], "refusal": null }, "logprobs": null, "finish_reason": "tool_calls" } ], "usage": { "prompt_tokens": 75, "completion_tokens": 51, "total_tokens": 126, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_0ba0d124f1" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1225ae3ea281e4-SIN Connection: - keep-alive Content-Type: - application/json Date: - Mon, 11 Nov 2024 23:43:55 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '1308' openai-organization: test_openai_org_id openai-processing-ms: - '748' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9996' x-ratelimit-remaining-tokens: - '199961' x-ratelimit-reset-requests: - 30.375s x-ratelimit-reset-tokens: - 11ms x-request-id: - req_a0c3b432eb349a35b6d8dde6e01451e4 status: code: 200 message: OK - request: body: |- { "messages": [ { "role": "system", "content": "You're a helpful assistant." }, { "role": "user", "content": "What's the weather in Seattle and San Francisco today?" }, { "role": "assistant", "tool_calls": [ { "id": "call_JpNb8OiAkbIbHzDggfpdDHpi", "function": { "arguments": "{\"location\": \"Seattle, WA\"}", "name": "get_current_weather" }, "type": "function" }, { "id": "call_vaFQc3zK6hHTRZKXRI5Eo2cJ", "function": { "arguments": "{\"location\": \"San Francisco, CA\"}", "name": "get_current_weather" }, "type": "function" } ] }, { "role": "tool", "content": "50 degrees and raining", "tool_call_id": "call_JpNb8OiAkbIbHzDggfpdDHpi" }, { "role": "tool", "content": "70 degrees and sunny", "tool_call_id": "call_vaFQc3zK6hHTRZKXRI5Eo2cJ" } ], "model": "gpt-4o-mini" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '746' content-type: - application/json cookie: - test_cookie host: - api.openai.com user-agent: - OpenAI/Python 1.54.3 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.54.3 x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.6 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASYMVzdmBGDbUoHFmt6R16tdtZUzR", "object": "chat.completion", "created": 1731368635, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Today, the weather in Seattle is 50 degrees and raining, while in San Francisco, it's 70 degrees and sunny.", "refusal": null }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 99, "completion_tokens": 25, "total_tokens": 124, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_9b78b61c52" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e1225b4cbf581e4-SIN Connection: - keep-alive Content-Type: - application/json Date: - Mon, 11 Nov 2024 23:43:56 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '859' openai-organization: test_openai_org_id openai-processing-ms: - '531' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9995' x-ratelimit-remaining-tokens: - '199947' x-ratelimit-reset-requests: - 37.973s x-ratelimit-reset-tokens: - 15ms x-request-id: - req_22b79d2dddb920f55e33727d06724978 status: code: 200 message: OK version: 1 test_chat_completion_with_content.yaml000066400000000000000000000062371527070072300467750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4o-mini", "stream": false } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '106' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.54.3 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.54.3 x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.6 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASYMQRl3A3DXL9FWCK9tnGRcKIO7q", "object": "chat.completion", "created": 1731368630, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "This is a test.", "refusal": null }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 5, "total_tokens": 17, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_0ba0d124f1" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e122593ff368bc8-SIN Connection: - keep-alive Content-Type: - application/json Date: - Mon, 11 Nov 2024 23:43:50 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '765' openai-organization: test_openai_org_id openai-processing-ms: - '287' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - '199977' x-ratelimit-reset-requests: - 8.64s x-ratelimit-reset-tokens: - 6ms x-request-id: - req_58cff97afd0e7c0bba910ccf0b044a6f status: code: 200 message: OK version: 1 test_chat_completion_with_content_span_unsampled.yaml000066400000000000000000000062371527070072300520660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4o-mini", "stream": false } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '106' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.54.3 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.54.3 x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.6 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASYMQRl3A3DXL9FWCK9tnGRcKIO7q", "object": "chat.completion", "created": 1731368630, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "This is a test.", "refusal": null }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 5, "total_tokens": 17, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_0ba0d124f1" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e122593ff368bc8-SIN Connection: - keep-alive Content-Type: - application/json Date: - Mon, 11 Nov 2024 23:43:50 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '765' openai-organization: test_openai_org_id openai-processing-ms: - '287' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - '199977' x-ratelimit-reset-requests: - 8.64s x-ratelimit-reset-tokens: - 6ms x-request-id: - req_58cff97afd0e7c0bba910ccf0b044a6f status: code: 200 message: OK version: 1 test_chat_completion_with_context_manager_streaming.yaml000066400000000000000000000170371527070072300525520ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4o-mini", "stream": true, "stream_options": { "include_usage": true } } headers: Accept: - application/json Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '148' Content-Type: - application/json Host: - api.openai.com User-Agent: - OpenAI/Python 1.109.1 X-Stainless-Arch: - x64 X-Stainless-Async: - 'false' X-Stainless-Lang: - python X-Stainless-OS: - Linux X-Stainless-Package-Version: - 1.109.1 X-Stainless-Runtime: - CPython X-Stainless-Runtime-Version: - 3.12.12 authorization: - Bearer test_openai_api_key x-stainless-read-timeout: - '600' x-stainless-retry-count: - '0' method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |+ data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"yKkTGHRZw"} data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":"This"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"bFd3AyW"} data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"LqISoMXL"} data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"mUrlylOKo"} data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" test"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"nGqQio"} data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"gUhadpfoFR"} data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" How"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"atLjmXM"} data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" can"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Ac67p8z"} data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" I"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"tl4DibKXq"} data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" assist"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"p5ei"} data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"97qAT5t"} data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" further"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ZK4"} data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":"?"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"5iGjeBvFWd"} data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"SdS98"} data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":12,"total_tokens":24,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"parVcHlnsA"} data: [DONE] headers: CF-RAY: - 9aed607a4c274be7-MXP Connection: - keep-alive Content-Type: - text/event-stream; charset=utf-8 Date: - Tue, 16 Dec 2025 10:08:00 GMT Server: - cloudflare Set-Cookie: test_set_cookie Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC openai-organization: test_openai_org_id openai-processing-ms: - '274' openai-project: - proj_Pf1eM5R55Z35wBy4rt8PxAGq openai-version: - '2020-10-01' x-envoy-upstream-service-time: - '618' x-openai-proxy-wasm: - v0.1 x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '10000000' x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - '9999993' x-ratelimit-reset-requests: - 6ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_50a8341c02184cbd8b364182eb40ed38 status: code: 200 message: OK version: 1 test_chat_completion_with_raw_response.yaml000066400000000000000000000062631527070072300500310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4o-mini" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '90' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.54.3 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - MacOS x-stainless-package-version: - 1.54.3 x-stainless-raw-response: - 'true' x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.12.6 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { "id": "chatcmpl-ASYMQRl3A3DXL9FWCK9tnGRcKIO7q", "object": "chat.completion", "created": 1731368630, "model": "gpt-4o-mini-2024-07-18", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "This is a test.", "refusal": null }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 5, "total_tokens": 17, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "system_fingerprint": "fp_0ba0d124f1" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - 8e122593ff368bc8-SIN Connection: - keep-alive Content-Type: - application/json Date: - Mon, 11 Nov 2024 23:43:50 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 content-length: - '765' openai-organization: test_openai_org_id openai-processing-ms: - '287' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - '199977' x-ratelimit-reset-requests: - 8.64s x-ratelimit-reset-tokens: - 6ms x-request-id: - req_58cff97afd0e7c0bba910ccf0b044a6f status: code: 200 message: OK version: 1test_chat_completion_with_raw_response_streaming.yaml000066400000000000000000000171311527070072300520760ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "Say this is a test" } ], "model": "gpt-4o-mini", "stream": true, "stream_options": { "include_usage": true } } headers: Accept: - application/json Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '148' Content-Type: - application/json Host: - api.openai.com User-Agent: - AsyncOpenAI/Python 1.109.1 X-Stainless-Arch: - x64 X-Stainless-Async: - async:asyncio X-Stainless-Lang: - python X-Stainless-OS: - Linux X-Stainless-Package-Version: - 1.109.1 X-Stainless-Raw-Response: - 'true' X-Stainless-Runtime: - CPython X-Stainless-Runtime-Version: - 3.12.12 authorization: - Bearer test_openai_api_key x-stainless-read-timeout: - '600' x-stainless-retry-count: - '0' method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |+ data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"v3JkrR4kf"} data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":"This"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"c2Yj6Tq"} data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"vYP94Gjb"} data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"axQhTg4rR"} data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" test"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Sd4wYC"} data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ZufRh78gtk"} data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" How"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Si8PiPK"} data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" can"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"DtALgOW"} data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" I"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xYwawpnRk"} data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" assist"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Swpx"} data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"L6Pd0pV"} data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" today"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Viytd"} data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":"?"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"LqmsdvgjP8"} data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"vbpp9"} data: {"id":"chatcmpl-CnMM0oFYQitzT43PYAvCrmNt6GIKs","object":"chat.completion.chunk","created":1765880036,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":12,"total_tokens":24,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"xEbQODa0Ga"} data: [DONE] headers: CF-RAY: - 9aed6932dfb8ed9e-MXP Connection: - keep-alive Content-Type: - text/event-stream; charset=utf-8 Date: - Tue, 16 Dec 2025 10:13:56 GMT Server: - cloudflare Set-Cookie: test_set_cookie Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC openai-organization: test_openai_org_id openai-processing-ms: - '228' openai-project: - proj_Pf1eM5R55Z35wBy4rt8PxAGq openai-version: - '2020-10-01' x-envoy-upstream-service-time: - '241' x-openai-proxy-wasm: - v0.1 x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '10000000' x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - '9999993' x-ratelimit-reset-requests: - 6ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_279d1848f0cf450dbffc9d7776f157f7 status: code: 200 message: OK version: 1 test_embeddings_model_not_found.yaml000066400000000000000000000036461527070072300463750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "This is a test for embeddings with bad model", "model": "non-existent-embedding-model" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '98' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - Linux x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.9.2 method: POST uri: https://api.openai.com/v1/embeddings response: body: string: |- { "error": { "message": "The model `non-existent-embedding-model` does not exist or you do not have access to it.", "type": "invalid_request_error", "param": null, "code": "model_not_found" } } headers: CF-RAY: - 939c18494d21ebf9-NRT Connection: - keep-alive Content-Type: - application/json; charset=utf-8 Date: - Sat, 03 May 2025 01:48:13 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '234' openai-organization: test_openai_org_id strict-transport-security: - max-age=31536000; includeSubDomains; preload vary: - Origin x-request-id: - req_e843ad138683a94b77b77e73be3b1fd5 status: code: 404 message: Not Found version: 1 test_embeddings_no_content.yaml000066400000000000000000001355351527070072300453730ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "This is a test for embeddings", "model": "text-embedding-3-small" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '77' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - Linux x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.9.2 method: POST uri: https://api.openai.com/v1/embeddings response: body: string: |- { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": [ 0.009139273, -0.010902632, 0.025990667, -0.040262204, -0.022861907, -0.034992713, 0.03507505, 0.0278295, -0.00016617251, 0.020652562, 0.019897819, -0.01844322, -0.021901323, -0.026402347, -0.017372854, 0.012364092, -0.02548293, 0.0061957687, -0.00823358, 0.0140313925, 0.01568497, -0.04215593, 0.008261025, 0.030875925, 0.015341904, -0.0027994171, 0.016110372, 0.038231257, 0.052969363, -0.021791542, 0.006576572, -0.02944877, -0.01250818, -0.025976945, -0.017688474, 0.033702787, -0.020734899, 0.04487301, -0.037545122, 0.016329933, -0.002789125, 0.0055164983, -0.052914474, 0.007821901, 0.0002815284, 0.05113053, -0.058623087, -0.028103953, -0.034059577, 0.04748031, -0.06208119, 0.022916798, -0.0033843443, 0.0040516076, -0.01912935, 0.012391537, -0.033098992, 0.0040104394, -0.0055267904, -0.013729494, 0.045065127, -0.009653872, -0.012096501, -0.014161757, -0.00014548135, -0.009544091, -0.026306288, 0.044900455, 0.0027479574, -0.016563218, 0.05310659, 0.023602929, -0.02950366, 0.015204677, 0.017455189, 0.01370891, -0.0046931403, 0.016014313, -0.021585701, -0.045888484, -0.03392235, 0.026731689, -0.014655772, 0.00055062067, -0.024138112, -0.009859712, -0.0834885, -0.018058985, -0.021695483, -0.04550425, -0.016165262, -0.050526734, -0.064770825, -0.019705702, 0.014257816, 0.008658982, 0.008446281, -0.048193887, -0.030821033, 0.015890809, -0.0040516076, 0.017400298, -0.009736208, 0.0017959496, 0.00082035613, -0.023836214, 0.016233874, -0.015781028, -0.05398484, -0.015287013, -0.047041185, -0.010127303, 0.015698692, 0.011924968, 0.05977579, 0.00095200766, 0.0030241252, -0.04388498, -0.029256653, -0.026169062, -0.023685265, -0.021969935, 0.040371988, -0.036310088, -0.021654315, -0.029723223, -0.018333437, -0.02647096, -0.03669432, -0.012226866, -0.0021441614, -0.02142103, 0.038176365, 0.050746296, -0.04888002, -0.02293052, -0.07893258, 0.022916798, -0.013070808, 0.05387506, 0.0013971356, -0.021544535, 0.018841175, -0.023575483, -0.023575483, -0.013331538, -0.03293432, 0.049868047, 0.03090337, -0.01579475, 0.0020858403, -0.00090912444, -0.05124031, -0.00536898, -0.049593594, -0.037847023, -0.025675047, -0.009207887, 0.03611797, -0.012521903, -0.01866278, 0.024892855, 0.029256653, -0.012117085, -0.0025181032, 0.016480882, 0.017276796, -0.005972776, -0.013441319, -0.04621783, 0.0026227382, -0.0237676, -0.026772857, 0.00026115886, -0.02417928, -0.021105409, -0.0013096537, -0.07898748, 0.023355922, -0.023328476, -0.01136234, 0.026100447, -0.020213438, 0.0069162073, -0.05096586, 0.06070893, 0.018374605, 0.06361812, 0.0063878857, 0.0045730677, -0.00599336, 0.037270673, 0.06367302, 0.005561097, -0.0002618021, -0.0263749, 0.053655494, -0.012281757, 0.048962355, -0.015451685, 0.0015146356, 0.047233302, -0.02568877, 0.021503367, 0.026663076, -0.013441319, -0.02835096, -0.024563514, -0.03529461, 0.00028109955, 0.04824878, -0.024892855, -0.005139126, 0.0031013151, -0.020460445, 0.03340089, -0.03891739, -0.0073827766, 0.03518483, -0.0066074478, -0.03976819, 0.05513754, 0.074212, 0.023012856, -0.06773492, -0.0040687607, 0.0011432669, -0.028625412, 0.027788332, -0.010703654, -0.007547448, -0.026004389, 0.013098253, -0.01620643, 0.017496357, -0.02835096, 0.0010240515, -0.005530221, -0.030519135, 0.017345408, -0.04575126, -0.008796208, 0.034361474, -0.013407012, 0.000549763, 0.018058985, 0.011822049, -0.027280595, 0.023204973, 0.008789347, 0.0027908406, 0.021064242, -0.0042608776, -0.024892855, -0.025400594, -0.014504823, -0.04528469, 0.037270673, 0.028378405, 0.0033105852, -0.0022625192, 0.017866869, -0.030244684, -0.020981906, 0.0120141655, -0.034581035, 0.027472712, 0.010429201, 0.0066143093, -0.0672409, -0.015534021, -0.014587159, 0.04591593, -0.0134207355, -0.05294192, 0.037407897, -0.03438892, -0.024975192, -0.010806574, 0.00012007304, -0.0034478116, -0.042210817, 0.024453731, 0.047754765, -0.028433295, -0.0054856227, -0.013695188, -0.018827453, -0.045833595, -0.011657377, -0.0018628475, -0.061367616, 0.060379583, 0.010298836, -0.021764096, 0.020295775, 0.016590664, 0.011369201, 0.022999132, -0.026896361, 0.0466844, -0.013503071, 0.043171402, 0.06850339, -0.037270673, 0.040564105, -0.028213734, 0.038807604, 0.037654907, -0.0016973182, -0.029091982, 0.0065422654, -0.015067451, 0.0341968, -0.052475348, 0.00019458265, 0.03641987, -0.0064976667, 0.025455484, -0.027692273, -0.02246395, -0.03252264, 0.015698692, -0.035514176, -0.0026964974, -0.013571684, -0.020858401, 0.011664238, -0.003955549, 0.0023843076, -0.0007435952, 0.036310088, 0.028899865, 0.008899127, -0.03216585, -0.037188336, -0.03899972, 0.011684822, 0.021023074, -0.0021355848, -0.031315047, -0.008905989, 0.037407897, -0.012967888, -0.01141037, 0.052036226, 0.015534021, 0.03408702, 0.041140452, -0.015122342, 0.005128834, -0.010209639, -0.009907741, 0.0028491616, 0.05867798, -0.042814616, -0.005760075, 0.0014074276, 0.010092997, -0.028007895, 0.0126179615, 0.0017547817, -0.015959423, 0.046355054, 0.029750668, -0.030601472, -0.0089746015, 0.031342495, 0.010031245, -0.0054341624, -0.022436505, 0.023589207, 0.01724935, 0.009763653, -0.020309497, 0.012391537, -0.015328181, 0.045120016, 0.040152423, 0.03987797, 0.017633583, -0.028049061, -0.029146872, -0.06268499, -0.011959274, 0.0052557685, 0.020776067, -0.017276796, -0.034004685, 0.0016930299, 0.030189792, -0.024165556, -0.019993877, -0.014449933, 0.04295184, 0.023726432, 0.053490825, -0.017551247, 0.00079291087, 0.005945331, -0.03913695, -0.063288786, 0.00025022365, -0.010442924, 0.01084088, 0.025345704, 0.029311543, 0.0076023387, 0.021077964, -0.018360883, -0.006528543, -0.027898112, 0.04528469, -0.027376654, 0.006171754, 0.028570522, 0.019582197, -0.038395926, 0.002845731, 0.038478263, -0.02412439, -0.037106, -0.055110093, 0.010484092, 0.013146282, -0.019637087, 0.09089872, 0.0073004407, 0.012055333, -0.0591171, -0.03919184, -0.0010060405, 0.03362045, 0.03002512, -0.039329067, -0.031973734, 0.059501335, 0.028625412, -0.0118014645, -0.017674752, 0.008315915, 0.0012830662, -0.033538114, -0.037106, -0.010017522, -0.03002512, -0.01652205, 0.020776067, -0.026800303, -0.041195344, -0.009564675, -0.06400236, -0.0040687607, -0.04127768, 0.012467012, -0.041305125, -0.011684822, 0.011293727, 0.055027757, 0.010936938, 0.0018302562, -0.050307173, -0.044790674, 0.05686659, 0.060214914, -0.005121973, -0.018937234, -0.0073827766, 0.052228343, -0.025139865, -0.003272848, 0.04278717, -0.013153144, 0.009482339, 0.01334526, -0.038341038, 0.009146135, -0.03356556, -0.02089957, -0.031095486, 0.0017582123, -0.033373445, -0.031424828, -0.022134608, -0.009530368, 0.1397513, -0.028103953, -0.007046572, 0.018786285, -0.01501256, -0.029366435, -0.0012676283, -0.02777461, -0.01646716, 0.04017987, -0.03183651, 0.0068819006, -0.031452276, 0.036831547, -0.026155338, -0.017345408, 0.008631536, -0.012384676, 0.025222199, 0.02490658, 0.017921759, -0.041963812, 0.011911245, -0.034800597, 0.006765258, -0.003784016, 0.00599336, -0.038588043, 0.006446207, 0.01939008, -0.0044701477, -0.028460741, 0.068338715, -0.007904236, -0.028268624, 0.02756877, -0.016837671, 0.0052248924, -0.056262795, 0.011444676, 0.0023054024, -0.017221905, -0.056153014, -0.042183373, -0.004497593, -0.028103953, -0.014806721, 0.028543077, 0.0011792887, -0.01974687, 0.0030978844, -0.013139421, 0.024632126, 0.015671248, -0.0061957687, 0.035569064, -0.0033311693, 0.035514176, -0.008350222, -0.0023774463, 0.039411403, 0.032193296, -0.007437667, -0.01912935, 0.010724238, 0.00515971, -0.015643802, 0.029531106, -0.020350665, 0.019938985, 0.021242635, 0.026745412, -0.0025558404, -0.035349503, 6.362799e-05, 0.009894018, 0.028954756, 0.06888762, 0.04451622, -0.048660457, 0.036337532, 0.010257668, 0.0057189073, 0.04701374, -0.024330229, -0.04992294, -0.009647011, 0.012288618, 0.019650811, -0.003955549, 0.017949203, -0.012755187, 0.028213734, -0.028323514, -0.024220448, -0.008405113, -0.0006243798, 0.015040006, 0.00083879597, -0.0061202943, 0.034169357, 0.015822196, -0.019938985, -0.015808472, 0.023150083, 0.040097535, 0.003814892, 0.026443513, -0.03466337, 0.024193002, -0.020158548, 0.04248527, -0.0026073002, -0.02011738, 0.024590958, 0.003674235, -2.9401814e-05, -0.021352418, 0.01146526, 0.021983659, 0.0053929947, -0.034937825, 0.035020158, 0.024810521, -0.0031802203, -0.011334895, -0.033071544, -0.0008688142, -0.008315915, 0.02518103, 0.0028011324, 0.011026136, -0.0042334325, 0.024879133, -0.016686723, -0.011746574, -0.02365782, -0.011842633, 0.008960879, -0.022052271, 0.03809403, 0.015245846, 0.00990088, -0.008885405, 0.012041611, -0.020048767, 0.0038046, 0.025084972, -0.04967593, -0.013887305, -0.020885848, -0.027404098, 0.03628264, 0.015259568, -0.045668922, 0.0041922643, -0.018415773, 0.03101315, 0.012117085, -0.009461756, -0.011746574, 0.0055096373, 0.006401608, -0.0011741428, -0.010278252, 0.017070955, -0.028597968, 0.004446133, -0.008357083, 0.0148341665, -0.058074184, -0.021517089, -0.0046142354, -0.0020000737, -0.044159435, -0.008947156, 0.020611394, -0.0017350554, -0.0200076, 0.0016432853, 0.020048767, -0.07437667, 0.027637383, -0.012192559, 0.011931829, -0.0014357305, -0.015053729, 0.037243225, -0.030656362, 0.014024531, -0.035925854, -0.029091982, -0.0066074478, 0.002830293, -0.007259273, -0.018854897, 0.021242635, -0.018539278, -0.036255196, 0.016178984, 0.010388033, 0.03798425, 0.0038835052, -0.0004200412, -0.017674752, -0.06103827, -0.012446428, 0.04012498, 0.002454636, -0.0018988693, 0.044461332, 0.004446133, 0.02997023, -0.010230223, -0.0030995996, 0.008336499, 0.017606137, -0.017921759, -0.040783666, 0.00859723, -0.034745708, 0.0010823727, 0.035047606, 0.03315388, 0.0020858403, -0.0045318995, -0.00786993, 0.019980153, 0.017811978, -0.0031716435, -0.013674604, 0.013894166, -0.0051837247, -0.0076435064, -0.012727742, -0.026910083, -0.015973145, -0.00813066, 0.0021870446, -0.00823358, -0.023753878, -0.010641902, -0.009345113, -0.017606137, -0.01579475, 0.010271391, 0.0029778113, -0.0010017522, -0.020240884, 0.015781028, 0.0053072283, -0.001751351, 0.0025524097, -0.038533155, -0.018703949, -0.012837524, 0.029613443, 0.024604682, -0.0013456757, -0.03960352, 0.046382498, 0.046739288, -0.0054959147, 0.014202925, -0.019513585, -0.010285113, 0.0115956245, -0.0051151114, 0.005540513, -0.029640887, -0.022834461, 0.034251694, -0.030683808, -0.0043020453, 0.022326725, 0.03529461, 0.017729642, 0.0026175922, 0.01771592, 0.01803154, -0.03180906, -0.030299574, -0.0020618257, 0.03685899, 0.02704731, 0.0053346734, -0.025675047, 0.024110666, 0.0048166444, 0.047343083, -0.028460741, -0.017180737, 0.014449933, -0.052036226, 0.026800303, -0.01302964, 0.027760886, 0.010868325, -0.035486728, -0.0063501485, -0.011746574, -0.021379862, 0.004600513, -0.008041463, -0.057360604, -0.032220744, 0.002293395, 0.030985706, -0.024042053, 0.01626132, -0.0001327236, -0.037490234, -0.065758854, -0.009338251, 0.029119426, -0.026512127, 0.018758839, 0.034416363, 0.005633141, 0.020323219, -0.027513878, 0.025661323, 0.029311543, -0.03496527, -0.019664533, -0.0026278843, 0.028296068, -0.019321468, 0.03845082, -0.003574746, -0.018209934, 0.0010077559, -0.01741402, 0.004418688, 0.06394747, 0.020199716, -0.05618046, -0.0066108783, 0.004374089, -0.0054993453, -0.0014357305, -0.00489898, -0.007478835, -0.0019657672, 0.018168766, 0.01793548, 0.014367597, -0.018319715, 0.00070457143, 0.01313256, 0.045476805, -0.0014623181, 0.01771592, -0.0060997102, -0.019376358, -0.02725315, 0.054588635, 7.231184e-05, 0.01704351, 0.00396241, 0.01803154, -0.013057086, 0.018058985, 0.0063981777, 0.012762048, -0.009598982, -0.007938543, 0.029997677, 0.02527709, 0.02069373, -0.0014460224, 0.024865411, -0.032248188, 0.007938543, 0.010456647, -0.002313979, 0.0023997454, 0.009365697, -0.01985665, 0.024165556, -0.015671248, -0.01959592, 0.008542339, -0.014340151, 0.03913695, 0.014889057, -0.04237549, -0.004617666, 0.007732704, 0.03721578, 0.028735194, 0.020460445, 0.059226885, 0.02527709, -0.008048324, 0.030244684, -0.011794603, 0.006223214, -0.024247892, -0.05269491, -0.035129942, 0.019376358, 0.043610528, -0.035733737, -0.008247303, 0.020597672, -0.013770662, -0.009111828, 0.02548293, -0.026196506, 0.041552134, -0.0020412416, 0.035898406, 0.01417548, -0.017427744, -0.01276891, -0.04325374, -0.0017942343, 0.021091687, 0.019842926, 0.002444344, -0.0023774463, 0.0072455504, 0.015314459, 0.0008164967, -0.025894608, -0.023108914, 0.00020927015, -0.0076435064, 0.046629507, 0.016343655, -0.009166718, 0.00064367725, 0.056482356, 0.011677961, -0.016275043, -0.056427468, -0.00084823027, 0.003996717, -0.013866721, -0.0062781046, 0.014985115, -0.016727889, 0.037353005, 0.019170519, -0.04435155, 0.006092849, 0.0042505856, 0.00823358, 0.020035043, -0.043802645, -0.0033843443, 0.014600881, -0.022505118, -0.014449933, -0.012638545, -0.02501636, 0.012988472, -0.038972277, 0.025222199, -0.0035009868, -0.019047014, 0.013009056, 0.031068042, -0.021077964, 0.01568497, 0.015547743, -0.011959274, -0.0030429938, -0.014312706, -0.0033791985, 0.043116514, -0.011554457, 0.01292672, -0.025057528, 0.011506428, 0.031232713, -0.0023208403, -0.023314754, 0.024398841, -0.023081468, 0.009269639, -0.040866002, 0.011527012, -0.0020360956, 0.013461903, 0.021860154, 0.03329111, 0.009557814, 0.028062785, 0.007883653, -0.012700297, 0.04481812, 0.05884265, -0.01646716, -0.0012058764, 0.021132855, 0.0073827766, 0.017290518, -0.02423417, -0.0048818267, 0.02006249, -0.013166866, 0.016398547, 0.009962631, 0.021187745, -0.00025794262, -0.03814892, 0.0045113154, 0.012679713, -0.00072901486, -0.015341904, 0.04251272, -0.012954165, 0.02136614, -0.0132148955, -0.014806721, -0.013461903, -0.0022625192, 0.004075622, -0.01777081, 0.007458251, -0.003459819, -0.0074719735, 0.0460806, -0.0019468985, -0.019760592, 0.023644097, -0.045202352, -0.00095972663, 0.008411974, 0.006885331, -0.00656628, 0.0089746015, -0.03216585, 0.0010231938, -0.00025880028, 0.021077964, 0.029229209, -0.011451537, -0.010216501, -0.00531409, -0.047919434, -0.008617814, 0.026237674, -0.014545991, 0.01838833, -0.011273143, -0.017125847, -0.024700738, 0.046821624, -0.006182046, 0.017496357, -0.00014944805, -0.017619861, -0.019170519, 0.002674198, 0.0016707305, 0.018209934, 0.039164394, -0.015108619, 0.015822196, 0.015149787, 0.0055988343, 0.018827453, -0.031781618, 0.01292672, -0.0027445266, -0.010436063, -0.0033912056, 0.029668333, -7.520646e-05, 0.017866869, 0.010971245, 0.01443621, -0.04767243, -0.008535477, 0.005310659, 4.4457847e-06, -0.012583654, -0.024083221, 0.0026724827, 0.021640591, 0.03002512, 0.027061032, -0.0030086872, -0.026964974, 0.013057086, -0.0037497096, 0.018594168, 0.024028331, -0.010202778, -0.027404098, -0.017510079, 0.026868915, -0.012803216, 0.014463655, -0.011677961, -0.0066314624, 0.0111565, 0.019101905, 0.0052523375, -0.024275338, 0.015918255, -0.0062986887, -0.003428943, -0.006765258, 0.027733441, -0.0022590884, -0.027925558, 0.02809023, -0.011053581, 0.022326725, 0.0014322998, -0.0012358946, -0.023232417, -0.0018662781, 0.015081174, 0.018223656, 0.008103215, -0.0029246362, -0.011575041, -0.0060070828, -0.0052146004, 0.030354464, -0.019623365, 0.027911836, 0.02282074, -0.012851246, -0.01871767, -0.009125551, 0.0011844347, -0.049868047, 0.001081515, -0.021324972, 0.02944877, 0.006562849, 0.027074754, 0.052667465, -0.023589207, 0.02089957, 0.019115629, -0.0075817546, -0.011430954, 0.0034495268, 0.009132412, -0.03935651, 0.014134312, -0.0330441, -0.021448476, -0.048138995, -0.016865116, -0.009612705, 0.015108619, -0.017839422, -5.7034693e-05, -0.013393289, 0.00021763238, 0.0087756235, 0.015822196, 0.005019053, 0.015341904, 0.09150252, 0.005862995, -0.02240906, 0.017400298, 0.013400151, 0.006947083, -0.028378405, 0.010923216, -0.018594168, -0.009619566, -0.0073621925, 0.020103658, 0.012377815, -0.022834461, -0.0048063523, 0.012940443, -0.012631684, -0.03147972, -0.010648763, 0.005019053, 0.0030206945, -0.009269639, -0.0023620082, 0.024385119, -0.022642344, -0.013804968, -0.031781618, 0.00865212, 0.026416069, 0.03235797, -0.013118837, 0.030162347, -0.010923216, 0.030601472, 0.0076640905, 0.03257753, -0.042210817, 0.029640887, 0.0041305125, -0.016425991, 0.013688327, -0.008123798, 0.001618413, -0.004096206, -0.017510079, -0.027349208, -0.0063501485, -0.036035635, 0.0048509506, -0.010470369, -0.008350222, 0.036557093, -0.03482804, 0.026251396, 0.040866002, -0.035623956, -0.00063381414, 0.0057291994, 0.0426225, -0.018909788, 0.012700297, 0.032220744, -0.035678845, 0.035706293, 0.008295332, -0.015287013, -0.02835096, 0.008062047, 0.0027599647, 0.034334026, 0.009194165, 0.023753878, -0.009461756, -0.043445855, 0.0015909678, 0.021915045, 0.022422783, -0.005880148, -0.03268731, 0.028460741, -0.02512614, 0.0033791985, 0.021077964, 0.042128485, -0.0021750373, -0.0071426304, 0.0081649665, -0.023081468, 0.0010043252, 0.042128485, -0.021613147, -0.009351974, 0.021105409, -0.0099969385, -0.0005875002, 0.00792482, -0.008487448, -0.03743534, 0.037929356, 0.016727889, 0.007897375, 0.011616209, -0.03384001, 0.031177823, 0.008068908, -0.0072249663, -0.07843857, 0.01062818, 0.016110372, 0.014045115, 0.013516794, -0.02131125, 0.02089957, 0.008535477, -0.0060688346, 0.016672999, -0.02058395, 0.0041579576, 0.0014194349, -0.043802645, 0.003346607, 0.006583433, 0.019541029, 0.008055186, 0.027170813, -0.02652585, 0.013324677, 0.012226866, 0.0026656215, -0.015136064, 0.042183373, -2.0181918e-05, 0.010497814, -0.00022813877, 0.041140452, 0.0092765, 0.008261025, -0.0271022, -0.010669347, 0.025976945, 0.04012498, 0.009077522, 0.014093144, -0.008329638, 0.0292841, -0.013756939, 0.00891285, 0.004061899, -0.052200895, 0.005029345, -9.938188e-05, -0.043034177, -0.017962927, -0.019691978, 0.03356556, -0.007863069, 0.016563218, 0.024426287, 0.0035575926, -0.0356514, -0.0010875186, -0.0071700756, -0.02287563, 0.023849936, 0.005231754, -0.0010720807, 0.01626132, -0.018072708, -0.008487448, 0.0035678847, 0.0099832155, 0.0008722449, 0.01573986, 0.0011527012, -0.012693436, -0.0420187, 0.012906136, 0.02475563, -0.04372031, -0.0063192723, 0.019362636, -0.026964974, -0.028268624, 0.0036399283, -0.00044941623, 0.024028331, 0.020185994, -0.019664533, -0.006947083, -0.018594168, 0.009743069, -0.019637087, -0.014847889, -0.015067451, -0.0192803, -0.004528469, -0.046355054, -0.024138112, -0.0066143093, 0.013276648, -0.0023517162, 0.0027016434, 0.026786579, -0.013063947, 0.019513585, 0.04185403, -0.013242341, 0.003137337, -0.021599425, -0.031534612, 0.059226885, -0.0101890545, -0.0010300551, 0.008713872, 0.049868047, -0.0036262057, 0.009104967, -0.01443621, -0.0034014976, -0.004195695, 0.00037780125, 0.0041236514, 0.02434395, -0.031232713, -0.034992713, 0.0022899644, 0.028625412, -0.023671541, 0.0058389804, 0.016371101, -0.035102494, -0.04048177, -0.03987797, 0.006394747, -0.020336943, -0.0014400188, -5.0548606e-05, 0.037133444, 0.018580444, 0.009935186, -0.034635928, -0.007520003, 0.03326366, 0.04575126, -0.01829227, -0.0124121215, -0.01073796, 0.0056194183, -0.018580444, -0.019184241, 0.007760149, 0.029750668, -0.005670878, -0.02397344, -0.034004685, -0.004209418, -0.0007864784, 0.0048166444, -0.015410516, -0.039548628, -0.036557093, 0.01245329, 0.046986297, 0.00013529659, 0.03397724, -0.034471255, 0.015959423, 0.011972997, -0.024151834, 0.02704731, 0.015245846, -0.023698987, -0.016220152, 0.001084088, -0.014381319, 0.030821033, 0.0033912056, -0.0136128515, -0.009729347, -0.043610528, 0.017510079, -0.00792482, -0.0111565, 0.00349241, 2.2205471e-05, 0.001196442, 0.008144382, 0.0054753306, -0.0017539241, -0.008837376, 0.020981906, -0.021434752, -0.028131397, 0.0033534684, -0.012693436, 0.0055370824, -0.0067995647, 0.0031064611, -0.046053156, -0.004315768, -0.03831359, 0.03438892, 0.034279138, -0.028954756, 0.0023568622, 0.021750374, -0.008315915, 0.037407897, -0.0023997454, 0.04643739, -0.039164394, 0.012933581, 0.004885257, -0.0020892709, 0.012494457, 0.01829227, -0.028625412, 0.03554162, -0.017688474, 0.041826587, -0.016700445, 0.0068613165, -0.0056743086, 0.02799417, 0.007197521, -0.031891398, 0.034581035, -0.011293727, 0.019678256, 0.03384001, 0.014669495, -0.014669495, 0.0009983216, 0.011492705, 0.04092089, -0.005972776, -0.017153291, 0.013887305, 0.023232417, -0.049401477, -0.027760886, 0.03444381, -0.0069127763, 0.009612705, 0.019980153, 0.003814892, -0.0068235793, -0.010957522, -0.0022076287, -0.030683808, -0.014340151, 0.01974687, 0.04794688, -0.03326366, -0.017002342, 0.010051829, 0.026896361, -0.0045318995, 0.005300367, -0.029531106, -0.0134207355, -0.017866869, 0.030080011, -0.025428038, -0.0020343803, -0.009372558, -0.01417548, -0.008768762, 0.008020879, -0.043994762, -0.029201763, 0.026169062, 0.028131397, -0.00552336, 0.0025627017, -0.036721766, -0.057195935, -0.015616356, -0.0072249663, -0.054588635, 0.008473726, -0.0015763874, -0.030080011, 0.005049929, 0.017235627, 0.026814025, 0.010758544, -0.011300588, -0.0041133594 ] } ], "model": "text-embedding-3-small", "usage": { "prompt_tokens": 6, "total_tokens": 6 } } headers: CF-RAY: - 939c18244b2cd496-NRT Connection: - keep-alive Content-Type: - application/json Date: - Sat, 03 May 2025 01:48:08 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-allow-origin: - '*' access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '33312' openai-model: - text-embedding-3-small openai-organization: test_openai_org_id openai-processing-ms: - '170' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload via: - envoy-router-687976cc6-vxtlt x-envoy-upstream-service-time: - '145' x-ratelimit-limit-requests: - '5000' x-ratelimit-limit-tokens: - '5000000' x-ratelimit-remaining-requests: - '4999' x-ratelimit-remaining-tokens: - '4999993' x-ratelimit-reset-requests: - 12ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_cadfd4b7a8dfbacbbdc0756d743a4434 status: code: 200 message: OK version: 1 test_embeddings_token_metrics.yaml000066400000000000000000001355671527070072300461000ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "This is a test for embeddings token metrics", "model": "text-embedding-3-small" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '91' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - Linux x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.9.2 method: POST uri: https://api.openai.com/v1/embeddings response: body: string: |- { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": [ 0.011905322, -0.013637613, 0.031408645, -0.03914042, -0.009457374, -0.021697115, 0.015797962, 0.024265453, 0.03734793, 0.007664887, 0.022218809, -0.00943062, -0.04120044, -0.024920914, 0.019717352, 0.015209383, -0.03485985, -0.02204491, -0.0012699567, 0.020091902, 0.00094724225, -0.0041802395, 0.017095506, 0.03635805, 0.017563693, 0.016252771, 0.02680704, 0.006440913, 0.042698637, -0.0053373296, 0.027355487, -0.010928818, -0.0054409998, -0.03603701, -0.030472273, 0.032398526, -0.016854726, 0.040772382, -0.014058981, -0.0058891214, -0.020359438, 0.0094439965, -0.06046298, 0.004524691, -0.029535899, 0.041173685, -0.007210077, -0.03726767, -0.01708213, 0.030793315, -0.07389326, 0.0140456045, -0.008795223, 0.0047755055, -0.010768296, 0.0030365258, -0.011838438, -0.005203562, -0.021335943, -0.029188104, 0.0333349, -0.015904974, 0.015436788, -0.016627321, 0.0039026714, -0.001693833, -0.00785885, 0.03544843, -0.005240348, -0.03362919, 0.06977321, 0.031328388, -0.013396831, 0.00069600984, 0.001989794, -0.014085734, -0.00065253535, -0.0016018677, -0.0037622154, -0.042778898, -0.052490428, 0.002847579, -0.0075578727, -0.016881479, -0.02010528, -0.007109751, -0.07608704, -0.041334208, -0.0010433878, -0.045106456, -0.0458288, -0.03151566, -0.049868584, -0.021295812, 0.022058288, -0.0196906, 0.027449124, -0.039568473, -0.033281393, 0.007852161, -0.010119524, 0.014928471, -0.01791149, 0.03515414, 0.0006128231, -0.008915614, 0.025201827, -0.001780782, -0.039648734, -0.02340934, -0.050510667, 0.004614984, -0.015490295, 0.022927778, 0.06297781, 0.014580675, 0.011397005, -0.030632794, -0.0057185674, -0.003160261, -0.02787718, -0.013008905, 0.041601744, -0.014446908, -0.022218809, -0.03780274, -0.026044564, -0.05211588, -0.044945933, -0.0049728127, -0.011256549, -0.017309535, 0.047594532, 0.034271274, -0.011343498, -0.04740726, -0.03242528, 0.037749235, -0.03734793, 0.04211006, -0.0065613035, -0.010514138, 0.03764222, 0.0029729863, -0.049065977, -0.026914053, -0.044277094, 0.040852644, 0.012707927, -0.0008072042, 0.015557179, -0.013711185, -0.029535899, 0.0020984802, -0.05331979, -0.03900665, -0.0066783503, -0.0030699677, 0.020814247, -0.051527303, -0.015583932, 0.018058633, 0.018219156, 0.00096145505, 0.009544322, 0.02390428, 0.0036919874, -0.008567818, -0.021269059, -0.037829496, 0.00048323566, -0.060837533, -1.2658287e-05, -0.043527998, -0.04537399, -0.032906845, -0.020386191, -0.069024116, 0.0276364, -0.0013686104, -0.01840643, -0.00694923, 0.0054610646, 0.025201827, -0.056663983, 0.055700853, 0.010774985, 0.06773994, 0.03186346, 0.03023149, -0.02841225, 0.061319094, 0.07260909, 0.0028024325, 0.012192923, -0.021295812, 0.044357356, 0.0142328795, 0.028010948, 0.0005919219, -0.0049560918, 0.03764222, -0.008541065, -0.00024600708, 0.020881131, -0.011330121, -0.040290818, -0.055861376, -0.02961616, 0.015289644, 0.03574272, 0.0055981767, 0.016105626, 0.019503325, -0.03370945, 0.034565564, -0.056342937, 0.0076916404, 0.048798442, -0.008086255, -0.008888861, 0.046149842, 0.072234534, -0.006076396, -0.057306066, 0.017884737, 0.0012699567, -0.012975463, 0.014901717, -0.028331991, -0.00084106415, 0.006009512, -0.016613944, -0.010801738, -0.010841869, -0.03734793, -0.012427015, 1.1698138e-05, -0.0021118568, -0.0057586976, -0.0374817, -0.008260153, 0.034137506, -0.007297026, -0.009156397, -0.006547927, -0.0003235505, 0.001728947, 0.027315356, 0.019489948, -0.00868821, 0.0043708584, -0.02659301, -0.008574507, -0.0112899905, -0.020626973, -0.019209037, 0.07207402, 0.020814247, 0.005323953, -0.00015090664, 0.0024228666, -0.042457856, -0.015062238, 0.002914463, -0.04823662, -0.014286386, 0.004347449, -0.031702936, -0.044972688, 0.017764345, -0.009711532, 0.055700853, -0.0056282743, -0.048049342, 0.034431797, -0.02324882, 0.0037388061, -0.0010433878, -0.020841, 0.018928124, -0.03330815, 0.025482738, 0.057894643, -0.036679093, -0.017042, 0.02352973, 0.0042738765, -0.0054945066, 0.01617251, -0.0027639742, -0.058750756, 0.07533795, 0.011223107, -0.043795533, 0.008019371, 0.034592316, -0.0038625412, -0.01985112, -0.030177984, 0.021202175, -0.018834487, -0.010554268, 0.015396657, -0.017242651, 0.03991627, -7.832723e-05, 0.031141112, 0.005216939, 0.004293942, -0.027181588, 0.0050196312, -0.007919045, 0.060248952, -0.059285827, 0.015610686, 0.072288044, -0.008828665, 0.03384322, -0.027663153, -0.040718876, -0.06345938, 0.00876847, -0.041066673, 0.009156397, -0.0138583295, -0.010855245, 0.021362696, 0.017563693, 0.0060061677, 0.008735028, 0.03528791, 0.051928606, -0.0003787297, -0.027529385, -0.010353616, 0.0040063416, 0.009203215, 0.009370425, 0.021777375, -0.017135637, -0.025763651, 0.026753532, -0.022192055, 0.0040598484, 0.03255905, 0.009149708, 0.021763999, 0.025469363, -0.023931034, -0.0031435401, -0.013329947, -0.0012816614, -0.015490295, 0.03801677, -0.057092037, -0.010808427, -0.005534637, 0.008507623, -0.024546366, -0.003875918, 0.0020884476, 0.015182629, 0.034244522, 0.04505295, -0.044812165, -0.019222412, 0.032238007, -0.008005994, -0.041093424, -0.025817158, 0.032933597, 0.004484561, -0.005193529, -0.025081437, 0.006828839, 0.025402479, 0.03801677, 0.029963955, 0.012620979, 0.051848345, -0.03386997, -0.04074563, -0.028653033, 0.011891945, 0.0010241587, 0.028439004, -0.023302326, -0.020626973, 0.040718876, -0.0023476225, -0.013885083, -0.013162738, -0.026004434, 0.0314354, 0.047755055, 0.024171816, -0.037080396, 0.011825061, 0.010788362, -0.045427497, -0.03825755, 0.012968775, -0.01096226, 0.013370078, 0.050029106, 0.026338853, 0.0030816724, 0.0048457333, 0.027582891, -0.0036217594, -0.03694663, 0.023583239, -0.04475866, 0.030124476, 0.023757137, 0.025964303, -0.003178654, 0.0037488388, 0.048744936, -0.0666698, -0.03060604, -0.022245562, 0.015369904, -0.0031218028, -0.04098641, 0.06383393, -0.015851468, 0.00380569, -0.05628943, -0.03210424, -0.008895549, 0.056556966, 0.031783197, -0.0333349, -0.036144022, 0.032478787, 0.023984542, -0.0099121835, 0.023061544, 0.017349666, -0.01617251, -0.024840655, -0.037829496, -0.030204738, -0.027582891, -0.00041676988, -0.026633142, 0.024171816, -0.044143327, -0.0030532468, -0.06469004, 0.010708101, -0.01902176, -0.024225323, -0.05741308, 0.0030632794, 0.011035832, 0.046417378, 0.010086082, 0.0079391105, -0.05671749, -0.033147626, 0.06335236, 0.045935813, -0.021844259, -0.025656637, 0.0043976116, 0.057306066, -0.009189839, 0.018393053, -0.009490816, -0.02006515, 0.008447427, 0.022927778, -0.04098641, 0.018352922, -0.049306758, -0.016359786, -0.014874964, -0.013436962, -0.044116575, -0.03186346, -0.028305236, -0.009417243, 0.12659772, -0.012513964, -0.03135514, 0.0014764606, -0.017617201, -0.016453423, -0.0036585454, -0.014420154, -0.015329774, 0.019075269, -0.036465064, -0.0026653206, -0.009497504, 0.028198224, -0.018607082, -2.2116079e-05, 0.014580675, -0.021804128, 0.0583227, 0.025161697, 0.031114358, -0.022967907, 0.008413985, -0.039648734, 0.017322913, -0.0333349, -0.0048992406, -0.028840307, -0.0020784151, 0.016988494, -0.0039528343, -0.001956352, 0.06447601, -0.022272315, 0.0031318353, 0.016747711, -0.023917658, 0.011363563, -0.031114358, 0.013269752, -0.0018209122, -0.012212987, -0.06656279, -0.03400374, -0.0018928124, -0.035555445, -0.029428884, 0.014473661, 0.006588057, -0.020533336, 0.02390428, -0.023342457, 0.032532293, 0.0061733774, 0.01083518, 0.056342937, -0.019155528, 0.02287427, -0.014968601, -0.008213335, 0.018259285, 0.0028559396, 0.003128491, -0.0014630838, 0.008072878, 0.016226018, -0.012306625, 0.03590324, -0.012567472, 0.0040096855, 0.013925213, 0.036304545, -0.019503325, -0.021536592, 0.010841869, -0.0018075355, 0.03164943, 0.07640809, 0.019102022, -0.056235924, 0.03528791, 0.03172969, 0.025924172, 0.04815636, -0.011356874, -0.035020374, -0.018540198, 0.001641162, 0.022954531, -0.020707233, 0.023944411, -0.014714442, 0.011497331, -0.015329774, -0.012507277, -0.010594399, 0.008654768, 0.0062068193, -0.01009277, -0.040959656, 0.05703853, -0.005484474, 0.00583227, -0.012741369, 0.022553228, 0.05024313, 0.022526475, 0.037107147, -0.037214164, -0.008521, 0.007323779, 0.027114704, 0.0039260807, -0.007082998, 0.02655288, 0.009484127, -0.009698155, -0.01878098, 0.009470751, 0.035394922, 0.035769474, -0.01931605, 0.04946728, 0.023797266, 0.0020048427, -0.003768904, -0.055914883, -0.0055847997, -0.031248126, 0.015570556, -0.015062238, -0.008547753, 0.021282434, -0.0035114011, -0.0118852565, -0.0140456045, -0.04235084, -0.019102022, -0.013791446, -0.029883696, 0.048316877, 0.0032104237, -0.017857982, 0.017055377, 0.024599873, -0.039969776, 0.030044217, 0.022365952, -0.053801354, -0.009785105, -0.00019772533, -0.031194618, 0.06485056, 0.008574507, -0.03180995, 0.032238007, -0.012212987, 0.02605794, 0.00827353, -0.023502978, -0.0314354, 0.017470056, 0.006671662, -0.014941848, -0.0058055166, -0.0013853314, -0.038658854, 0.0014689361, 0.015945105, -0.013490468, -0.051634315, -0.013543976, 0.020198917, -0.02407818, -0.00860126, -0.004765473, 0.024894161, -0.004400956, -0.0007323779, 0.009417243, 0.046979204, -0.050323393, 0.030873576, 0.009029317, 0.019663846, -0.01389846, 0.018767603, -0.009791792, -0.009250034, 0.02010528, -0.014594052, -0.037374683, -0.024720263, -0.027368864, -0.018995008, -0.03271957, 0.011985582, -0.010955571, -0.036331296, -0.005083171, -0.0015667537, 0.0537746, 0.021081783, 0.017296158, -0.02340934, -0.041708756, -0.0042504673, 0.03590324, -0.015035485, -0.024024671, 0.042163566, 0.006531206, 0.036117267, -0.003681955, 0.021456333, 0.0065245177, 0.024840655, -0.01890137, -0.038284305, 0.0020733988, 0.014981978, -0.0039561787, 0.0120123355, 0.03924743, 0.015891599, -0.020519959, 0.009357048, 0.009604518, -0.016292902, -0.0122598065, -0.013383455, 0.009163084, -0.013176114, -0.0019847776, -0.014928471, -0.019744107, -0.014072358, -0.023957787, 0.025964303, 0.01071479, -0.012607602, -0.015276267, 0.0019981544, -0.015904974, -0.023663498, 0.013844953, 0.018232532, -0.010708101, -0.0149552245, -0.00060906087, -0.00639075, 0.021536592, -0.0019948103, -0.03606376, -0.0024563086, -0.019476572, 0.035207648, 0.007490989, -0.007123128, -0.023543108, 0.033361655, 0.036919873, -0.022031534, 0.036304545, -0.029589407, -0.022392707, 0.010400436, 0.0013134312, 0.023128428, -0.028224977, -0.03750845, 0.042243827, -0.031328388, 0.0024211947, 0.038899634, 0.018299416, 0.013283129, -0.0062436056, 0.036144022, -0.015704323, -0.022981284, -0.036224283, 0.0061566564, 0.004310663, 0.021897767, -0.0114170695, -0.018219156, 0.020626973, -0.018553574, 0.050055858, -0.03271957, -0.022740502, -0.011751489, -0.05422941, 0.043956053, -0.009089513, 0.0010174703, 0.0007934094, -0.04462489, -0.009938938, -0.011637786, 3.1874326e-05, -0.0034127475, 0.012674486, -0.04165525, -0.06046298, 0.0032438657, 0.046551146, -0.03914042, -0.014981978, -0.00023890068, -0.023837397, -0.029482393, -0.014687689, 0.04877169, -0.031248126, 0.013129296, 0.037454944, -0.0011604344, -0.0142328795, -0.059499856, 0.033227887, 0.01567757, -0.04491918, -0.021830883, -0.0015316397, 0.042083304, -0.018005127, 0.015731078, 0.0058055166, -0.0019647125, 0.010433878, -0.007210077, 0.02287427, 0.06731188, 0.021737244, -0.03189021, 0.00036911515, -0.006026233, 0.0051065804, 0.013497157, 0.0045681656, 0.017978374, -0.006511141, 0.02006515, 0.00785885, 0.012025713, 0.0058088605, -0.0026736811, 0.004625017, 0.01583809, 0.0027689906, 0.026071317, -0.013497157, -0.005424279, -0.010667971, 0.05901829, -0.013309882, 0.012674486, -0.0096312715, 0.0157177, -0.010293421, 0.015771206, 0.0060462984, 0.0067820204, -0.013022282, 0.01886124, 0.022860894, 0.04235084, 0.0045146584, -0.0009840283, 0.041039918, -0.022927778, 0.01104252, 0.0031552447, -0.026245214, -0.018219156, 0.020025019, -0.03584973, 0.016573813, -0.027582891, -0.007437482, 0.028331991, -0.008173204, 0.041842524, 0.004574854, -0.050938725, 0.012079219, 0.013296505, -0.0011412053, 0.0102867335, 0.014660936, 0.04235084, 0.017697461, 0.02726185, 0.032452036, 0.0041969605, 0.032692816, -0.018352922, -0.0450797, -0.030766562, 0.0060362658, 0.04826337, -0.034672577, 0.0031134423, 0.042671885, -0.007718394, -0.004942715, 0.028947322, -0.01302897, 0.04430385, -0.005257069, 0.02886706, 0.00010732764, -0.016720958, -0.0044644955, -0.034779593, -0.004367514, 0.0051801526, 0.003474615, -0.014888341, -0.0029713141, 0.0005814713, 0.036893122, 0.00022364281, 0.024706887, -0.010982325, 0.013329947, -0.001204745, 0.031141112, 0.032987107, -0.0154234115, 0.01120973, 0.053507064, -0.001834289, 0.0032689471, -0.051500548, 0.02018554, -0.0083136605, -0.004350793, -0.010647906, 0.0026502719, -0.0027556138, 0.038765866, -0.0022071663, -0.048076097, 0.005240348, 0.028519265, 0.013283129, 0.008514311, -0.031034097, 0.0075913146, 0.0045949193, 0.013283129, 0.0026803694, -0.012727993, -0.018165648, -0.018152272, -0.02870654, 0.008982498, 0.0012582521, -0.024666756, -0.00031017375, 0.029937202, 0.0016419981, 0.024439352, -0.0014229534, 0.018433183, -0.004434398, -0.016025366, 0.004645082, 0.036919873, -0.025001176, 0.03606376, -0.01670758, -0.0038625412, 0.051393535, -0.011718047, -0.037454944, 0.01654706, -0.00736391, 0.00024684315, -0.01583809, -0.015517049, -0.0031569167, 0.017068753, -0.003912704, 0.033762958, -0.007959176, 0.03437829, 0.021402827, -0.024292206, 0.035715964, 0.050189625, -0.018874617, -0.0024161784, 0.020841, -0.0074843005, 0.012808253, -0.013249687, -0.016346408, 0.012654421, 0.00044101526, 0.030097723, 0.022526475, 0.040611863, -0.005136678, -0.056663983, 0.009256722, 0.013831576, -0.0057252557, -0.003108426, 0.0276364, -0.007905669, 0.032452036, 0.009470751, -0.003631792, -0.029027583, 0.0048992406, 0.021750621, -0.007109751, 0.0037053642, 0.012881826, 0.003788969, 0.0032973727, 0.010781674, -0.025616506, 0.0028492513, -0.054443438, -0.014834833, 0.015690947, 0.02469351, -0.038284305, 0.013644301, -0.0095041925, 0.00039754077, -0.0006475191, 0.032746322, 0.027034445, -0.011504019, 0.0049661244, -0.0100392625, -0.048611168, -0.0024880785, 0.01961034, -0.01795162, 0.024225323, 0.008320348, -0.02253985, -0.020613596, 0.038846128, 0.007718394, 0.008380544, 0.0017590447, -0.011590968, -0.009089513, -0.0038290992, 0.011122781, 0.026713401, 0.04272539, -0.02518845, -0.0033793056, 0.025857288, 0.019677224, 0.01633303, -0.032157745, 0.01787136, 0.009524258, -0.005203562, -0.017831229, 0.03504713, -0.014473661, 0.008367167, 0.0054443437, 0.01919566, -0.040023282, -0.016011989, -0.0024546366, 0.010206472, -0.014487037, -0.020814247, 0.0031435401, 0.025964303, 0.016801218, 0.02204491, 0.0046551144, -0.035341416, 0.003474615, 0.009109578, 0.004641738, 0.0038558529, 0.0034077312, -0.035421677, -0.024252078, 0.0151424995, -0.021777375, -0.0060061677, -0.0011035831, 0.011891945, 0.0037588712, 0.02332908, 0.01617251, -0.028546019, 0.035020374, 0.011825061, 0.0033408473, -0.0030983933, 0.033736203, 0.0014814768, -0.03485985, 0.037668973, -0.020974768, 0.028840307, -0.005818893, -0.01985112, -0.02192452, -0.0015868188, 0.020533336, 0.022165302, 0.019810991, -0.017215898, -0.0003948236, -0.0006859773, -0.027275225, -0.008694898, 0.013089165, 0.0072033885, 0.0008803584, -0.0122598065, -0.03341516, -0.00014045605, 0.0102265375, -0.025576377, 0.00024349894, -0.028171469, 0.020506583, 0.018580329, 0.0439293, 0.047219984, -0.02825173, 0.029776681, 0.026954183, -0.012507277, -0.001030011, 0.0011378612, -0.0050162873, -0.027342109, 0.029321872, -0.031997226, -0.032906845, -0.041708756, -0.034271274, 2.2534103e-07, 0.015530425, -0.014553921, -0.018299416, -0.007470924, 0.01654706, 0.010059328, 0.018727472, 0.012661109, 0.021001523, 0.097168826, 0.0116043445, -0.00052838225, 0.020239048, -0.0047755055, 0.0104138125, -0.018754225, 0.009229968, -0.018847864, 0.009738286, -0.008413985, 0.035582196, 0.023342457, -0.024961045, -0.015824715, 0.00992556, -0.01340352, -0.0040431274, 0.0037087083, -0.007912357, 0.027743412, -0.02117542, 0.009209903, 0.031087603, -0.02245959, -0.01567757, -0.009524258, 0.00901594, 0.007631445, 0.03879262, -0.016560437, 0.024586497, -0.008046125, 0.035796225, 0.023770513, 0.02249972, -0.044143327, 0.050029106, 0.013844953, -0.019102022, 0.02639236, -0.018540198, -0.0080661895, -0.024385845, -0.025830535, -0.03221125, -0.0032037355, -0.035475183, 0.003595006, -0.0075846263, -0.006267015, 0.022740502, -0.03531466, 0.0045447564, 0.036786105, 0.01848669, 0.012433704, 0.024747018, 0.03499362, -0.028599525, 0.030873576, 0.016801218, -0.016774464, 0.015690947, 0.0028074489, -0.025616506, -0.018580329, -0.011550837, 0.009765039, 0.015517049, 0.002563323, 0.021523217, -0.010674659, -0.045186717, 0.002183757, 0.007337156, 0.020158786, -0.0071298163, -0.013791446, 0.01348378, -0.052276403, -0.0023609991, -0.001395364, 0.031408645, -0.02382402, 0.0042972863, 0.031087603, -0.02200478, -0.012159481, 0.037615467, -0.030579288, -0.004802259, -0.0005802172, -0.00492265, -0.011530772, 0.009370425, -0.015169253, -0.020158786, 0.016761089, 0.019209037, 0.0038290992, 0.0017590447, -0.021456333, 0.030097723, 0.024653379, -0.01302897, -0.046925694, 0.019102022, 0.009343671, 0.0061232145, 0.033976987, -0.022272315, 0.033227887, 0.0017256028, 0.0036451689, 0.018152272, -0.006634876, 0.02324882, -0.015249513, -0.014540545, 0.018887993, 0.021349318, 0.008527689, -0.01617251, 0.026873924, -0.004434398, 0.0042738765, 0.009979067, -0.020158786, 0.008427363, 0.043447737, 0.013477092, 0.0004401792, -0.004738719, 0.008480869, 0.014620805, 0.005534637, -0.01472782, -0.015864845, 0.011356874, 0.02068048, 0.01124986, -0.0191154, -0.007678264, 0.032532293, -0.004344105, 0.0021018244, 0.020466452, -0.020921262, -0.0051600877, -0.0138583295, -0.030846823, 0.0046919007, 0.008714963, 0.047353752, -0.027475877, 0.0076247565, 0.013470403, -0.018045258, -0.011062586, -0.013002217, -0.01480808, -0.029883696, -0.0014430187, 0.012353444, -0.0011646147, 0.012005648, 0.007149881, -0.0020717266, 0.02419857, 0.023048168, -0.01298884, 0.022860894, 0.0013309882, -0.005798828, -0.011998959, 0.005818893, 0.017550318, -0.06324535, -0.015624063, 0.003508057, -0.0009773399, -0.031488907, -0.010193096, 0.009711532, -0.015316397, 0.028091209, -0.03590324, 0.0059259073, 0.0055981767, 0.010253292, -0.044196837, -0.0150488615, 0.010614464, -0.008099631, 0.0040063416, -0.03234502, -0.029642913, -0.013778069, 0.00872834, -0.0020550056, 0.018607082, 0.013303194, -0.0129821515, 0.017964996, 0.07153895, -0.03780274, -0.012413639, -0.024506235, -0.012707927, 0.038765866, -0.011564214, 0.01344365, 0.009851988, 0.034726083, -0.021295812, -0.009457374, -0.015169253, 0.017149014, -0.0037856249, -0.009785105, 0.015811337, 0.01319618, -0.03764222, -0.037214164, -0.020359438, 0.016319655, -0.01298884, 0.012079219, -0.0023760481, -0.041494727, -0.016854726, -0.036893122, 0.0138182, -0.014058981, -0.010400436, -0.01923579, 0.026124824, 0.023476224, 0.002533225, -0.00959783, -0.016560437, 0.0120725315, 0.052142635, -0.01195214, 0.0039595226, 0.0016787841, 0.020626973, -0.011303367, -0.024666756, -0.0012699567, -0.011096028, -0.00068848545, -0.038284305, -0.040772382, 0.0027539418, 0.006738546, 0.0031702935, -0.023141805, -0.03172969, -0.055112276, 0.023436094, 0.038846128, -0.003128491, 0.026847169, -0.017470056, 0.012808253, 0.0025800439, -0.025295464, 0.025148321, -0.0038090341, -0.048129603, -0.0024847344, -0.013022282, -0.0046551144, 0.028813554, 0.025469363, -0.014634182, -0.004344105, -0.057680614, 0.028492512, 0.009671402, -0.017122261, 0.02064035, -0.0075578727, -0.0035682523, 0.018714096, 0.0033124217, 0.029642913, -0.0058222376, 0.012714616, -0.022365952, -0.034351535, -0.0059794146, -0.03151566, 0.01939631, 0.008648079, -0.018433183, -0.029642913, -0.008286906, -0.033495422, 0.030472273, 0.007504366, -0.03694663, -0.0011269925, 0.00802606, -0.00034863196, 0.022700371, -0.007036179, 0.04288591, -0.06527862, 0.00562493, 0.0018894682, -0.0048490777, 0.0059994794, 0.013356701, -0.03833781, 0.009664713, -0.012065843, 0.020533336, -0.01096226, 0.0018058635, -0.022058288, 0.0154234115, 0.025870666, -0.028519265, 0.03769573, -0.008969122, 0.014326517, 0.026138201, 0.023074921, 0.0028241698, 0.01484821, 0.0242387, 0.026445866, 0.0072702724, -0.030686302, 0.012132727, -0.0006951738, -0.053346545, -0.02552287, 0.033976987, -0.011430447, -0.017068753, -0.0035883174, 0.003282324, -0.016573813, -0.03330815, 0.0046919007, -0.016399914, 0.008494247, 0.013463715, 0.049734816, -0.030766562, -0.017898113, 0.039568473, 0.020546712, -0.012928644, -0.003875918, -0.050564177, -0.0018008471, -0.005430967, 0.03855184, -0.02010528, -0.0020834312, -0.0042638443, -0.0078120315, -0.016346408, -0.002847579, -0.025924172, -0.0050564175, 0.0016010316, 0.0076581985, -0.016185887, -0.010500762, -0.0229144, -0.043822285, -0.008220023, -0.017630577, -0.041949537, 0.03809703, 0.018005127, -0.009892118, 0.005574767, 0.036224283, 0.010801738, 0.009423932, -0.008293595, -0.00019061891 ] } ], "model": "text-embedding-3-small", "usage": { "prompt_tokens": 8, "total_tokens": 8 } } headers: CF-RAY: - 939c184b69e9db4c-NRT Connection: - keep-alive Content-Type: - application/json Date: - Sat, 03 May 2025 01:48:14 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-allow-origin: - '*' access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '33317' openai-model: - text-embedding-3-small openai-organization: test_openai_org_id openai-processing-ms: - '113' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload via: - envoy-router-8496694fc7-rgktz x-envoy-upstream-service-time: - '65' x-ratelimit-limit-requests: - '5000' x-ratelimit-limit-tokens: - '5000000' x-ratelimit-remaining-requests: - '4999' x-ratelimit-remaining-tokens: - '4999989' x-ratelimit-reset-requests: - 12ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_5c609a3f38e3e7a89eab47e70560bc9d status: code: 200 message: OK version: 1 test_embeddings_with_batch_input.yaml000066400000000000000000004174571527070072300465660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": [ "This is the first test string for embeddings", "This is the second test string for embeddings", "This is the third test string for embeddings" ], "model": "text-embedding-3-small" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '191' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - Linux x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.9.2 method: POST uri: https://api.openai.com/v1/embeddings response: body: string: |- { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": [ 0.008279107, -0.026522232, 0.01812191, -0.03011025, -0.021976603, -0.029407192, 0.043710772, 0.0180613, 0.019746214, 0.028534431, 0.021612952, -0.029213244, -0.012018644, -0.052608084, -0.009485213, 0.017018836, -0.014982394, 0.018618898, -0.034280106, 0.024243357, 0.015758181, -0.027298018, -0.02469186, 0.036001384, 0.016643064, -0.02304331, -0.0034667999, 0.032049716, 0.028970812, -0.026134338, -0.01343082, -0.038983315, 0.011891366, -0.042765282, 0.011327708, 0.0631297, -0.01257018, 0.0090427715, -0.04882612, 0.000837911, -0.007788178, 0.016303657, -0.025891904, 0.01242472, -0.025479767, 0.042862255, -0.023273623, 0.008206376, -0.030837549, 0.024473669, -0.06652377, -0.011927731, -0.01054586, 0.0048577627, -0.02492217, 0.0016894588, -0.015115732, 0.026134338, -0.033528563, -0.019055279, 0.032146692, 0.0062790294, -0.01669155, -0.034934677, 0.0015296043, 0.008667, -0.02094626, 0.059638657, 0.033480074, 0.027491966, 0.050377693, -0.008327593, -0.037310526, 0.0108792065, -0.0040971274, -0.018897695, -0.011242856, 0.0061699343, 0.000960643, -0.05076559, -0.03134666, 0.011061031, -0.017200662, -0.03495892, -0.026934369, 0.020558367, -0.071226984, -0.011818636, -0.018170396, -0.016558213, -0.030498141, -0.08422142, -0.027394993, -0.0187886, 0.011115579, -0.004381987, -0.017164296, -0.053771764, -0.024000922, -0.003315279, -0.010885267, 0.03098301, -0.031831525, 0.004339561, -0.0044880514, -0.034910433, 0.008879129, -0.014388432, -0.046110865, -0.036074113, -0.060414445, -0.0052183825, -0.0060668997, 0.0051032263, 0.060559902, 0.026764665, -0.008982164, -0.01900679, -0.021225058, -0.037892368, -0.022776634, -0.01899467, 0.05721432, -0.015358166, -0.045989648, -0.054256633, -0.024897927, -0.051008023, -0.03988032, -0.0017561282, 0.0015379379, -0.015333923, 0.032680046, 0.05900833, -0.045189615, -0.022509957, -0.06889962, 0.024400938, -0.00917005, 0.029673869, -0.013576279, -0.052705057, 0.0030440565, -0.013649009, -0.0010151905, -0.029019298, -0.04080157, 0.044583533, 0.021297788, -0.010994362, -0.03459527, -0.019479537, -0.034910433, 0.0163279, -0.03032844, -0.03386797, -0.023710003, -0.026885882, 0.033261884, -0.004227435, -0.0046759374, 0.0068063224, 0.025479767, 0.0051183784, -0.007618475, 0.0028592008, 0.018594654, -0.0059972005, -0.017588556, -0.06686318, 0.002897081, -0.0430562, -0.040995516, -0.008024551, -0.028922325, -0.01444904, -0.012824736, -0.06967541, 0.0135884015, -0.0022061453, 0.031758796, 0.045965403, 4.7054364e-06, 0.0011197401, -0.03381948, 0.04768668, 0.012436842, 0.041213706, 0.012000461, 0.009770073, 0.011139822, 0.022594808, 0.057553727, -0.020364419, 0.013067169, -0.009279145, 0.037698418, -0.019176494, 0.044365343, -0.01669155, 0.015249071, 0.031516362, -0.041116733, 0.014436918, 0.0041062185, -0.016667308, -0.021697804, -0.024279721, -0.037431743, 0.022485713, 0.043686528, -0.0038183287, -0.019321956, -0.019249225, -0.035274085, 0.029285975, -0.0114610465, 0.005424451, 0.033480074, -0.035007406, -0.0050971657, 0.034061916, 0.06361457, 0.029067785, -0.027128316, -0.0076730223, -0.0104973735, -0.00961249, 0.013842956, -0.024461547, 0.01047919, -0.02824351, 0.009697342, -0.039904565, -0.0048426106, -0.003278914, 0.004506234, 0.015649086, -0.02174629, 0.021382641, -0.051832296, -0.03129817, 0.02463125, 0.0059699267, 0.028655646, 0.014424797, -0.00026156308, -0.026740422, 0.025285821, 0.0232615, -0.0074851364, 0.034280106, 0.010897389, 0.00073411915, -0.02406153, 0.014509649, -0.041553114, 0.04790487, 0.043444093, 0.0163279, 0.016461238, 0.033964943, -0.020388663, 0.02094626, 0.0041304617, -0.030376926, 0.006172965, 0.0020046225, 0.023297865, -0.05944471, -0.010945875, -0.02375849, 0.046183594, -0.013612645, -0.06225694, 0.04746849, -0.027104072, -0.03498316, -0.011091336, 0.023334231, -0.035734706, -0.054693013, 0.009964019, 0.03006176, -0.017176418, 0.007048756, 0.023734245, -0.04196525, -0.035128623, -0.03141939, 0.0015189978, -0.028922325, 0.060996283, 0.009503395, -0.010927693, 0.02101899, 0.01777038, 0.013321725, 0.026667692, -0.010933754, 0.0445108, -0.022812998, 0.053432357, 0.063808516, -0.032049716, 0.057311293, -0.0111277, 0.023576664, 0.032195177, 0.0070669386, -0.009685221, -0.008242741, -0.024740346, 0.027055586, -0.06031747, 0.023637272, 0.049747366, 0.0005147925, 0.009248841, -0.023600908, -0.03755296, -0.010727685, 0.0023637272, -0.022837242, 0.018824967, 0.00016676777, 0.0001857079, -0.0142672155, -0.0017682498, 0.015370288, -0.010448887, 0.009509456, 0.035371058, -0.015649086, -0.03966213, -0.029189002, -0.009654917, -0.026473746, 0.0046244203, 0.010394339, -0.05483847, -0.028049564, 0.055032417, 0.0099094715, -0.011679237, 0.060414445, -0.010873145, 0.014667231, 0.04817155, 0.024073653, 0.005672945, 0.013964173, -0.019540146, -0.006394185, 0.069820866, -0.074911974, -0.002051594, 0.01899467, -0.012558059, -0.021297788, 0.0341104, -0.004094097, -0.020025013, 0.033092182, 0.0053032343, -0.054983933, 0.01415812, 0.05028072, -0.0067881397, -0.03149212, -0.023006946, 0.021709926, 0.021297788, -0.0007530593, -0.012085313, 0.018594654, -0.022412984, 0.04201374, 0.046571486, 0.038983315, 0.0200735, -0.027758643, -0.050523154, -0.028073806, -0.0017440064, 0.0016591547, -0.003769842, 0.0032910355, 0.0068245046, 0.008436688, 0.015709694, 0.0050032227, -0.011024666, 0.0038728763, 0.02247359, 0.03980759, 0.023794854, -0.007618475, 0.01408539, -0.020182595, -0.00013324375, -0.07025725, -0.012776249, -0.014061146, 0.011570142, 0.026788909, 0.021055356, 0.00722452, 0.024400938, 0.008485175, 0.021964481, -0.012861101, 0.03243761, -0.012164104, -0.0095337, 0.049892828, 0.027928347, -0.05740827, -0.0019273468, 0.053432357, -0.036074113, -0.038692396, -0.06725107, 0.010145845, -0.002500096, 0.019249225, 0.05697189, -0.0028864746, -0.008151828, -0.044413827, -0.03779539, 0.009697342, 0.05575972, 0.01393993, -0.053868737, -0.041044, 0.005324447, 0.051735323, -0.02780713, -0.001719763, 0.013030804, 0.034571026, -0.03689839, -0.03619533, -0.004721394, -0.015249071, -0.0041728877, 0.015940007, -0.023734245, -0.03828026, -0.017115809, -0.06836627, 0.0020925046, -0.051783808, 0.032534584, -0.051056508, -0.015515748, 0.00772757, 0.035831682, 0.0048001846, -0.028801108, -0.045189615, -0.052608084, 0.04455929, 0.04591692, -0.023128161, -0.045092642, 0.0032698226, 0.044244125, -0.041650087, -0.019418929, 0.015564235, -0.03481346, 0.009485213, 0.013370211, -0.012151983, -0.015333923, -0.02686164, -0.009012468, -0.023382718, -0.00077957544, 0.0047850325, -0.03452254, -0.0068911742, -0.03692263, 0.14332673, -0.02831624, -0.020922016, 0.019661361, -0.021394761, -0.035007406, -0.016776403, -0.046765435, -0.012012583, 0.045335077, -0.0074002845, 0.02030381, -0.06308121, 0.011242856, -0.032534584, 0.0005250202, 0.02167356, 0.0014258124, 0.0063456986, 0.033989187, 0.0027546515, -0.01857041, 0.014485406, -0.012630789, -0.028049564, 0.005136561, -0.014218728, -0.0068729916, -0.0017500673, 0.026934369, 0.021952359, -0.008151828, 0.048074577, -0.008254862, -0.054353606, 0.021346275, -0.019952282, 0.013770226, -0.054983933, -0.005815375, -0.021334153, -0.015976371, -0.05032921, -0.032534584, -0.019443171, -0.01552787, -0.02065534, 0.040171243, -0.018982548, -0.033795238, -0.0029455677, -0.0012568665, 0.020037133, 0.045989648, 0.0068002613, 0.027176803, 0.0076305964, 0.042668305, -0.0073639196, -0.022061454, 0.035661977, -0.00048145792, 0.021115964, -0.00040948545, 0.020170473, 0.018582532, -0.005809314, 0.014364189, -0.013891444, 0.013333846, 0.017685529, 0.035589248, -0.016000615, -0.014933907, 0.006709349, 0.022316009, 0.0131035345, 0.040195484, 0.040947028, -0.057117347, 0.03200123, -0.0010348883, 0.012333808, 0.040219728, -0.033043694, -0.029892059, -0.009976141, 0.016146075, 0.03251034, -0.0048517017, 0.004969888, -0.022909971, 0.056826428, -0.045407806, -0.019491658, 0.018049179, -0.0029728415, 0.007309372, 0.007994247, -0.013042926, 0.0375772, 0.013273237, -0.044098664, -0.0052456562, 0.0065275235, 0.06540857, 0.011315587, 0.035734706, -0.02267966, 0.011418621, -0.012836857, 0.045092642, -0.019964404, -0.0032849747, 0.023152405, -0.004166827, 0.017637042, 0.00037671902, 0.02247359, 0.036510494, 0.0218069, -0.022703903, 0.03495892, 0.029867815, 0.01242472, -0.012158043, -0.017879475, -0.014533892, -0.005394147, 0.020970503, 0.008242741, -0.011582264, -0.0047910935, -0.004203192, 0.0010280699, 0.0048698843, -0.04169857, -0.0077154483, 0.0043244087, -0.030013274, 0.04126219, 0.0035001347, -0.020970503, 0.019249225, 0.01032767, -0.0071457294, 0.008145767, 0.0048517017, -0.04152887, -0.013952051, -0.012776249, -0.001288686, 0.025843417, 0.019176494, -0.044074424, 0.0022773603, -0.0388621, 0.025091873, -0.0067578354, -0.02926173, -0.026061608, 0.0095276395, 0.0060487175, -0.0029849634, 0.004948675, 0.025019145, -0.004500173, 0.044098664, -0.009060955, 0.012158043, -0.026279798, -0.012412598, -0.0032273969, -0.0036728685, -0.03481346, 0.016655186, 0.03481346, -0.0013955082, -0.013636888, -0.011436803, 0.032098204, -0.06327516, 0.004421382, -0.0127156405, 0.0044547166, 0.01864314, 0.0012083797, 0.03408616, -0.011879245, 0.011303465, -0.05372328, -0.03503165, -0.007970003, 0.0095276395, 0.011103457, -0.0052941428, 0.007842726, -0.0023516056, -0.05149289, 0.017115809, 0.014170242, 0.06565101, -0.008279107, -0.0018227973, -0.018934062, -0.045335077, -0.046426028, 0.011303465, 0.01170348, 0.0066426797, 0.043007713, 0.004660785, 0.012309564, -0.016182441, 0.035783194, 0.0075881705, 0.02065534, -0.011600446, -0.045238104, 0.0106913205, 0.004806245, 0.010854963, 0.01661882, 0.020425027, 0.014182364, 0.0047850325, -0.03503165, 0.015370288, 0.01790372, -0.00094170286, -0.009248841, 0.014364189, 0.0003873255, -0.026910126, -0.029989032, -0.018073423, -0.01870375, 0.0051395916, -2.2325668e-05, -0.00044206245, -0.062450886, 0.0023879707, -0.012485328, -0.003963789, 0.0028834443, 0.016073346, 0.0284617, -0.020182595, 0.014291459, -0.019370442, -0.0039001498, 0.0072730067, -0.01857041, -0.02521309, -0.0036061993, -0.009709464, 0.032486096, 0.024352452, 0.02029169, -0.05561426, 0.04334712, 0.043298636, -0.017746137, 0.027855616, -0.01762492, -0.023079675, 0.007951821, -0.002939507, -0.003963789, -0.032607313, -0.008757913, 0.035783194, -0.009363997, 0.0033849787, 0.0100125065, 0.019200739, -0.008200315, -0.00241979, 0.050959535, 0.008545783, -0.006945722, -0.014412675, -0.0049153403, 0.025649471, 0.026255555, 0.0050304965, -0.038595423, 0.0015833942, 0.013806592, 0.023140283, -0.032316394, -0.022946337, 0.023855463, -0.049747366, 0.026764665, -0.011533777, -0.00053865707, 0.01640063, -0.030498141, -0.0038062069, -0.024461547, -0.027055586, 0.008400323, -0.007600292, -0.050814074, -0.03646201, 0.009812498, 0.009224597, -0.0026197976, -0.004736546, -0.0090427715, -0.038304504, -0.0715179, -0.023794854, 0.00278041, -0.022437226, 0.008345775, 0.0062366035, 0.009109441, 0.045625996, -0.022631174, 0.026013121, 0.042644065, -0.025479767, -0.010515556, 0.002247056, 0.022885729, -0.048341252, 0.04514113, -0.029576894, -0.011442864, -0.0016818829, -0.00946097, 0.027516209, 0.06899659, 0.025188847, -0.0436138, -0.014885421, 0.0068245046, 0.0021879629, -0.005836588, -0.029819328, -0.010988302, -0.015297558, 0.034255862, -0.009182171, 0.021976603, -0.025964634, 0.0065941927, -0.011248917, 0.042813767, -0.014400554, 0.01914013, -0.017661285, -0.007242703, -0.010139784, 0.044753235, 0.016994594, 0.019321956, 0.024897927, 0.017649163, -0.03524984, 0.023673637, 0.024146384, 0.021370519, -0.00932157, -0.019309834, 0.018909818, 0.002810714, 0.0020955352, 0.019043157, 0.036098357, -0.026958613, 0.01921286, -0.0046698763, 0.026594961, -0.013927808, 0.012727762, -0.018400708, 0.047153328, 0.00089548895, -0.028510187, 1.6880384e-05, -0.019152252, 0.006921478, 0.015139976, -0.023431204, 0.0073760413, -0.008679122, 0.06361457, 0.021843264, 0.021964481, 0.042886496, 0.026279798, 0.019491658, 0.031467877, 0.0043183477, 0.0053547514, -0.021964481, -0.042353142, -0.04695938, 0.01900679, 0.032825503, -0.011491351, -0.01047919, 0.0008265469, -0.003388009, -0.020667462, 0.003554682, -0.008321532, 0.024825197, 0.003524378, 0.019176494, 0.0133944545, -0.018909818, 0.012448964, -0.02926173, 0.0056365803, 0.02521309, 0.037431743, 0.010994362, -0.016133953, -0.00772757, 0.022049332, 0.020315932, -0.011903488, -0.04295923, 0.0008242741, 0.0017015806, 0.04247436, -0.0032273969, 0.00020720494, -0.03076482, 0.024667615, 0.02442518, -0.018606776, -0.051541377, -0.021697804, -0.0061941775, -0.0053699035, -0.012055009, 0.006921478, 0.008709426, 0.025988879, 0.024752466, -0.06405095, 0.012727762, -0.0028213207, 0.0076366575, 0.013406576, -0.04126219, 0.007939699, 0.008436688, -0.018909818, -0.007854847, -0.017527947, -0.014121755, -0.0030092066, -0.033577047, 0.040001538, 0.0013818713, -0.015152098, -0.0010947391, 0.039953053, -0.039080292, 0.029310217, -0.002348575, -0.044825967, -0.010151905, -0.006472976, -0.00874579, 0.014655109, -0.018812845, -0.014097512, -0.027467722, 0.01812191, 0.041165218, 0.0028637464, -0.025673714, 0.019673483, -0.018691627, 0.00938824, -0.020400785, 0.00917005, 0.008588209, 0.019988647, 0.008012429, 0.035201352, 0.03735901, -0.015261193, 0.022534199, -0.0014083874, 0.059250765, 0.007236642, -0.02795259, -0.015043003, 0.0022985733, 0.015721817, -0.0013197478, -0.034619514, -0.031661823, 0.015333923, -0.010988302, 0.029964788, -0.010703442, 0.016776403, 0.005597185, -0.031831525, 0.035201352, 0.0038607544, 0.002297058, -0.022412984, 0.04516537, -0.024752466, 0.013842956, -0.011279222, 0.01408539, -0.026134338, 0.010436765, 0.013758104, -0.0047698803, -0.023516055, -0.0045759333, -0.028340483, 0.035055894, 0.0041304617, 0.0034031612, 0.025697958, -0.019188616, 0.0061517516, 0.0011894397, -0.011582264, -0.015467261, 0.02463125, -0.019964404, 0.004239557, -0.011642871, 0.0187886, 0.013042926, 0.006776018, 0.008994285, 0.005485059, -0.034692243, -0.009382179, 0.03922575, -0.010339792, 0.0197947, -0.003981971, -0.004297135, -0.00017529083, 0.040777326, 0.018679505, -0.002231904, -0.00028978387, -0.0069881473, -0.026667692, -0.006624497, -0.021722047, 0.010897389, 0.0063032727, -0.009048833, 0.008394262, 0.0014008115, 0.005639611, 0.014412675, -0.041625842, 0.005721432, 0.004075914, 0.003915302, 0.002441003, 0.01574606, 0.018364342, 0.034255862, 0.0023894857, 0.012315625, -0.031686068, -0.018982548, -0.0037365074, 0.012921709, -0.0070608775, -0.019297712, -0.003221336, 0.014703596, 0.014376311, 0.054305118, 0.00014489194, -0.02269178, -0.009545822, 0.017540067, -0.022352375, 0.0381348, 0.0022288735, -0.026110094, -0.0403167, 0.016303657, -0.0170552, -0.0100125065, -0.020715948, -0.021419005, 0.0147520825, 0.0026197976, -0.00086215435, -0.0013205053, 0.015333923, 0.0020455332, -0.0304739, -0.028413214, 0.015212706, 0.00816395, -0.019685606, 0.03857118, -0.0107822325, -0.0040062144, 0.009951898, -0.0021425067, -0.038886342, -0.0007083606, 0.006166904, 0.029625382, -0.0070002694, 0.0070608775, -0.020328054, -0.00097503746, -0.0012068646, 0.016206684, -0.020206837, 0.01740673, -0.00035096047, -0.012533816, -0.006969965, -0.018279491, -0.0070669386, -0.046426028, -0.017673407, -0.02722529, 0.026837396, 0.032898236, 0.02347969, 0.024970656, -0.024607006, 0.0059699267, 0.041383408, 0.019418929, -0.0015909702, 0.007685144, 0.0147520825, -0.004736546, 0.003112241, -0.04349258, -0.024534276, -0.049795855, -0.0022000845, 0.015855156, 0.00019413626, -0.024461547, 0.011006484, -0.0013818713, 0.013152021, 0.01914013, -0.006191147, 0.028922325, 0.02434033, 0.040243972, 0.019394685, -0.024049409, 0.015612721, 0.016000615, -0.013709618, -0.025334308, 0.017697651, -0.016546091, 0.003257701, 0.0018152213, 0.017067323, 0.02787986, -0.0019894703, -0.0069760256, 0.009600369, 0.0030758758, -0.007569988, 0.0040728836, -0.0031304234, -0.0053547514, 0.014994516, -0.0059578046, 0.018109787, -0.043735016, 0.004991101, -0.026619205, 0.0111701265, 0.04101976, 0.014570257, -0.009115502, 0.0478079, -0.015200584, 0.012533816, 0.005918409, 0.016352143, -0.046983626, 0.030861793, -0.008588209, 0.0016652156, -0.004212283, -8.101637e-05, 0.0028258662, 0.0021197784, -0.0076366575, -0.03006176, 0.0100125065, -0.018097665, 0.0166067, -0.020194715, -0.0019697726, 0.012388355, -0.025261577, 0.018885575, 0.031613335, -0.02030381, 0.011855002, -0.0069881473, 0.03316491, -0.03350432, 0.0132005075, 0.007309372, -0.01777038, 0.016012738, 0.019891674, -0.020328054, -0.027710157, 0.007848786, -9.943564e-05, 0.052995976, -0.00047009383, 0.029334461, -0.029916301, -0.0065032803, 0.008897312, 0.005169896, 0.0018000692, 0.0064669154, -0.032970965, 0.022146307, -0.030934522, 0.0027531362, 0.0058244662, 0.034207378, 0.010897389, 0.0034425566, -0.003597108, -0.026570719, 0.02130991, 0.026716178, -0.01812191, -0.022291766, -0.0035334693, -0.00028580645, -0.009770073, -0.0041213706, -0.0015879398, -0.026449502, 0.01835222, 0.0023758488, 0.014097512, 0.0183401, -0.029504165, 0.032922477, -0.004463808, -0.0052698995, -0.0686087, 0.01148529, -0.0055850632, 0.034183133, 0.015661208, -0.016449118, 0.0332134, -0.0078063607, -0.0027546515, 0.0058274968, -0.013539915, 0.009073076, -0.003575895, -0.032898236, 0.008618513, 0.0020864438, 0.026182825, 3.4589397e-05, 0.014497527, -0.046038132, 0.0018167365, 0.0190674, -0.019346198, -0.0035668039, 0.029601138, -0.011539837, -0.011551959, 0.0016667307, 0.061917532, -0.020994747, 0.012897465, -0.02095838, 0.005569911, 0.016182441, 0.03437708, -6.794769e-05, -0.008109403, -0.00050645886, 0.05808708, 0.008139707, 0.011133761, 0.015733939, -0.052414138, 0.016594578, 0.012382294, -0.03857118, -0.0067881397, -0.003894089, 0.048220035, -0.005833558, 4.6166548e-05, 0.0147520825, -0.0016167288, -0.012533816, -0.000402667, -0.0058214357, -0.039977293, -0.008806399, 0.013236873, -0.0079821255, 0.0036152906, -0.030643603, -0.023431204, 0.025843417, 0.023031188, -0.0040274276, 0.002348575, 0.0085276, -0.018473437, -0.025746444, 0.033189155, 0.016449118, -0.04494718, -0.01972197, -0.005930531, -0.019928038, -0.0066426797, -0.006363881, 0.0027031342, 0.027758643, 0.018073423, -0.02448579, 0.00556082, -0.010533738, -0.0038304504, -0.049941313, -0.022909971, -0.013152021, -0.025552498, -0.015661208, -0.06720258, -0.038959075, -0.004797154, -0.0072487635, 8.158458e-05, -0.0027137408, 0.050135262, -0.036098357, 0.0042728917, 0.0388621, -0.021637196, -0.0180613, -0.0068911742, -0.026279798, 0.039540913, 0.00061365997, 0.013745983, 6.4218852e-06, 0.035080135, -0.014776326, -0.026958613, -0.018218882, -0.0069942083, 0.0017788563, 0.016982472, -0.0067699575, 0.02838897, -0.022619052, -0.031031497, -0.0166067, 0.0145581355, -0.02145537, 0.013152021, 0.002572826, -0.02867989, -0.025673714, -0.022449348, -0.005715371, -0.014788447, 0.0013932354, 0.010030689, 0.04196525, 0.020194715, -0.01589152, -0.012533816, 0.010733746, 0.020255323, 0.048317008, -0.015515748, 0.004585025, -0.0021758412, 0.01321263, 0.0045213858, -0.0021940237, 0.018000692, 0.04327439, 0.0031789101, -0.0082669845, -0.02766167, 0.006254786, 0.00018381389, 0.025843417, -0.01777038, -0.035201352, -0.0416016, 0.013830835, 0.039904565, -0.0030016305, 0.021964481, -0.023528177, 0.03689839, 0.0003109968, -0.035831682, 0.03272853, 0.021649318, -0.038037825, -0.015115732, -0.0012303503, -0.027491966, 0.020825043, 0.024328208, -0.02304331, -0.0038122677, -0.036437765, 0.016812768, -0.004348652, -0.0008530631, 0.012655032, 0.016521847, -0.0010917087, 0.006624497, -0.0015349075, 0.0021122023, -0.025722202, -0.0069093565, -0.015709694, -0.032970965, 0.0051517133, -0.020982625, -0.0047850325, 0.0006629043, 0.03944394, -0.033358857, -0.0098185595, -0.034280106, 0.020134108, 0.021928117, -0.03944394, -0.01784311, 0.019249225, -0.012267138, 0.01892194, 0.0034122523, 0.026594961, -0.042789523, 0.0070002694, -0.0039031804, 0.0030667845, 0.0038122677, -0.0020061377, -0.01610971, 0.04421988, -0.026279798, 0.0020622006, -0.03408616, 0.014994516, 0.0023197862, 0.04058338, 0.010091297, -0.025334308, 0.025649471, -0.017115809, 0.0031364842, 0.044850208, -0.002137961, -0.0018773449, 0.008909433, 0.012024704, 0.0437835, -0.0010454947, -0.0071457294, -0.0060153827, 0.037237797, -0.05207473, -0.0083639575, 0.020970503, -0.0018030996, 0.002203115, 0.019188616, 0.0015833942, 0.01147923, 0.005360812, -0.0009765527, -0.025528254, 0.004448656, 0.024388816, 0.05076559, -0.009200354, -0.017661285, 0.02766167, 0.035855923, 0.004560781, 0.009636734, -0.04043792, -0.016727915, -0.024291843, 0.033358857, -0.020388663, -0.023952436, -0.016243048, -0.015636966, -0.0023091796, 0.01581879, -0.022425104, -0.025261577, 0.036292303, 0.018364342, -0.011085275, 0.00397288, -0.026449502, -0.05207473, -0.019661361, -0.006782079, -0.03524984, 0.011939853, 0.0010318578, -0.03798934, -0.0005863862, 0.016049102, 0.008479114, 0.003127393, -0.0129095875, -0.010388278 ] }, { "object": "embedding", "index": 1, "embedding": [ 0.011355308, -0.023255175, 0.017673438, -0.057228275, -0.019777419, -0.00010336192, 0.030841883, 0.020161087, 0.014047165, 0.020755151, 0.0024505188, -0.03626273, -0.032030012, -0.027401255, -0.025012618, 0.017797202, -0.009115187, 0.008737708, -0.036609266, 0.02429479, -0.009628806, -0.028416116, -0.009721628, 0.05871344, 0.015136285, -0.017475417, -0.009238951, 0.035866685, 0.031931, -0.026881449, -0.010625103, -0.038539976, 0.018737804, -0.032673582, -0.00047610298, 0.050297517, 0.0007019715, 0.014591726, -0.04066871, 0.019245235, 0.011231544, 0.00046836777, -0.032723088, 0.013613993, -0.005160941, 0.033242896, -0.012846659, -0.00043394603, -0.017698191, 0.009430784, -0.060347117, -0.009238951, -0.011120157, 0.0013250437, -0.0180076, 0.013514983, -0.01728977, 0.02772304, -0.027128976, -0.04985197, 0.03901028, -0.002838827, -0.010915947, -0.031584464, -0.011107781, 0.011380061, -0.024864102, 0.068119474, 0.023626465, 0.0105694095, 0.046634115, -0.00057008595, -0.050248012, 0.015606587, 0.007295863, 0.0034065924, 0.0035458263, 0.0012198447, 0.029703258, -0.041163765, -0.04502519, 0.014381327, -0.02653491, -0.016101642, -0.032079518, 0.012586756, -0.06490162, -0.018873945, -0.017920965, -0.033440918, -0.033589434, -0.07554529, -0.022648733, -0.026064608, 0.004876285, 0.014851629, -0.015086779, -0.06460459, -0.028762653, 0.007147346, -0.0006613616, 0.019356623, -0.03529737, 0.023824487, 0.003217853, -0.023725476, 0.021621495, -0.008607756, -0.059505526, -0.041361786, -0.074901715, -0.0003007068, 0.0035798613, 0.018750181, 0.090990975, 0.015519952, 0.010142425, -0.009393655, -0.017859083, -0.03366369, -0.020631388, -0.04049544, 0.06643628, -0.0022277443, -0.034629047, -0.046634115, -0.033985477, -0.055050038, -0.036114212, 0.0055167614, -0.0010883459, -0.0054734442, 0.028762653, 0.055693608, -0.0455945, -0.01790859, -0.0729215, 0.016188275, -0.009758757, 0.01576748, -0.012797154, -0.049901474, -0.0022757028, -0.016906105, -0.0053156456, -0.016188275, -0.02725274, 0.048861858, 0.014703113, -0.003391122, -0.047104415, -0.0007240169, -0.05460449, 0.013118939, -0.074753195, -0.028663643, -0.028218094, -0.0056374306, 0.027029965, 0.0005051101, -0.015346683, -0.0042729373, 0.030965647, 0.0065285284, 0.0065718456, 0.004786556, 0.029232956, 0.0068441257, -0.03730234, -0.0784661, 0.007370121, -0.01865117, -0.04594104, -0.0033787456, -0.019963065, -0.016497685, -0.018156117, -0.057723332, -0.0076485886, 0.011800856, 0.020086829, 0.018069481, 0.0064233295, 0.0048793787, -0.022896262, 0.04448063, 0.007178287, 0.027203234, 0.0230324, 0.019715536, 0.03190625, 0.027599277, 0.08470379, -0.018626418, 0.006689421, -0.021250205, 0.026336888, -0.025742823, 0.034158748, -0.024406176, 0.015346683, 0.009121375, -0.01504965, 0.0058602053, 0.015210543, 0.00038966187, -0.01623778, -0.028663643, -0.01841602, 0.036312234, 0.049084634, 0.016126394, -0.024047263, -0.014195682, -0.03802017, 0.040718216, -0.019703161, -0.0071225935, 0.029851774, -0.034505285, -0.0033013932, 0.0405697, 0.056238167, 0.01841602, -0.036757782, -0.0035427322, -0.005847829, 0.0056374306, 0.019727914, -0.032945864, 0.021089314, -0.0159655, 0.0018069482, -0.057327285, 0.01246918, -0.016485307, -0.0072896746, 0.029802268, -0.03173298, 0.0083973585, -0.031361688, -0.018762557, 0.03321814, 0.0061139204, 0.013514983, 0.005816888, 0.0034530037, -0.030470591, 0.045173705, -0.0038242943, -0.01641105, 0.028193342, 0.023218047, -0.016386297, -0.030074548, -0.01114491, -0.026658673, 0.058861956, 0.03366369, 0.014505091, 0.010031038, 0.023020025, -0.021819517, 0.019715536, -0.006448082, -0.040346924, 0.0102104945, -0.0023329433, 0.027277492, -0.08410972, -0.029183451, -0.027277492, 0.03757462, 0.010755055, -0.06604024, 0.036708277, -0.03757462, -0.018428396, -0.0031018245, 0.02653491, -0.02151011, -0.055842124, 0.00065788074, 0.02483935, 0.0040563513, 0.0030909951, 0.012203088, -0.03264883, -0.0047246744, -0.030148806, 0.017203137, -0.041535053, 0.043985575, 0.010717926, -0.035594404, 0.010179554, 0.026039856, 0.0075681424, 0.021225452, -0.026460651, 0.031881496, -0.028069578, 0.038787503, 0.058465913, -0.03913404, 0.03366369, -0.0084530525, 0.037079565, 0.020445742, 0.014307069, -0.01009292, -0.021114066, -0.03722808, 0.034876574, -0.053911414, 0.011974126, 0.036733028, -0.007846611, 0.004876285, -0.035965696, -0.0012894617, 0.0037995416, 0.0026098643, -0.013923402, 0.02534678, 0.01518579, -0.01504965, -0.007382497, 0.00630266, 0.025185887, -0.010204307, 0.011274861, 0.03289636, -0.0054610674, -0.035173606, -0.020581882, -0.016151147, -0.014665984, -0.012617696, 0.00315133, -0.05707976, -0.023737853, 0.054356962, -0.018019976, -0.011763727, 0.030841883, -0.017821955, 0.027648782, 0.033416163, 0.016299663, 0.015247672, -0.007821858, -0.01681947, -0.015420942, 0.055297565, -0.065495685, 0.013069433, 0.013366465, 0.010977829, -0.018205622, 0.030767623, 0.008781025, -0.019245235, 0.0382677, 0.012122642, -0.031980507, -0.019789794, 0.059951074, -0.0039728107, -0.032203283, -0.032673582, 0.031386442, 0.012073137, 0.026856696, -0.023416068, 0.0332924, -0.020705646, 0.04460439, 0.037624124, 0.028812159, 0.032772593, -0.034505285, -0.053713392, -0.021770012, -0.0002639645, 0.026584415, -0.0028512033, 0.006547093, 0.00079518097, 0.037723135, 0.007988939, -0.0049969545, 0.00513, -0.004257467, 0.017809577, 0.030718118, 0.04586678, 0.004504994, 0.021101689, 0.0055105733, 0.00096690294, -0.07742649, -0.012481556, -0.0148145, 0.0052475757, 0.026411146, 0.0064913994, 0.001663073, 0.047203425, 0.01660907, 0.0032611701, 0.0026748402, 0.038911268, -0.020099204, -0.0040996685, 0.05133713, 0.035495393, -0.039802365, 0.007141158, 0.0474262, -0.021411099, -0.027178481, -0.059753053, -0.008019879, 0.009467914, -0.017005116, 0.05316883, 0.006256249, -0.016361544, -0.029034935, -0.04321824, 0.007821858, 0.04594104, 0.004885567, -0.050223257, -0.05178268, 0.027525019, 0.047624223, -0.027426008, -0.030841883, 0.017066997, 0.028663643, -0.023873994, -0.036138963, -0.01773532, -0.0063862004, -0.013589241, 0.026015103, -0.023465574, -0.034505285, -0.03730234, -0.06485211, -0.005965404, -0.031386442, 0.027227987, -0.039554838, -0.027500266, 0.0031776298, 0.041262776, -0.010445646, -0.008657262, -0.064307556, -0.051485647, 0.021584367, 0.06742639, -0.011318179, -0.041559808, 0.026584415, 0.0378469, -0.037401352, 0.011899867, 0.026188372, -0.042153873, 0.0010365199, 0.007920869, 0.001974029, 0.01032807, -0.03455479, -0.0037129072, -0.00798275, -0.0064171413, 0.016943233, -0.035866685, -0.0123268515, -0.04059445, 0.11425853, -0.021770012, -0.0054270327, 0.015643716, -0.020136334, -0.026163619, -0.015408565, -0.03299537, -0.012908541, 0.03655976, -0.0061912728, 0.019121472, -0.0756443, 0.0050371774, -0.02636164, -0.004610193, 0.01349023, 0.011540953, 0.029208204, 0.05143614, 0.006064415, -0.028143836, 0.014901134, -0.008910977, -0.016906105, 0.022141304, -0.013341713, -0.0005952254, -0.009517419, 0.0018193246, 0.021163572, 0.008292159, 0.046015296, 0.021955658, -0.049109384, 0.028069578, -0.01654719, 0.00930702, -0.051287625, -0.0027537395, -0.021906152, -0.0061201085, -0.02388637, -0.027178481, -0.030792376, -0.025544802, -0.02530965, 0.033589434, -0.027525019, -0.017549675, -0.010544657, 0.0037593185, 0.022277443, 0.037351847, 0.0023809017, 0.031980507, 0.0019956876, 0.028317105, -0.004604005, -0.019517515, 0.03995088, 0.0062500606, 0.029109193, -0.0009204916, 0.019319493, 0.020680893, -0.034752812, 0.03381221, -0.009665935, -0.0033416164, -0.003428251, 0.030445838, -0.012500121, -0.023502702, 0.0059344633, 0.025421038, 0.014529844, 0.03792116, 0.04012415, -0.040074646, 0.02789631, 0.011813233, 0.009084246, 0.043713294, -0.010018662, -0.017760072, -0.02415865, 0.013514983, 0.03319339, -0.005789041, 0.0081127025, -0.032203283, 0.040643957, -0.027500266, -0.025173511, 0.010018662, -0.02530965, 7.019715e-05, 0.013514983, -0.0054208445, 0.03438152, 0.024468059, -0.060347117, -0.016683329, 0.00693076, 0.04638659, 0.0041677384, 0.021596743, -0.0048422497, 0.015037274, -0.019109096, 0.061535247, -0.018118987, -0.014034789, 0.015408565, -0.0033075814, 0.026906202, -0.007512449, 0.013576864, 0.034431025, 0.018750181, -0.0149135105, 0.039183546, 0.026856696, 0.03054485, -0.016312039, -0.028911171, 0.011695658, 0.013948155, 0.031064657, 0.008094138, -0.014492715, -0.019170977, 0.0037995416, -0.012797154, 0.006992642, -0.025421038, -0.003536544, -0.011602835, -0.03173298, 0.04311923, 0.009319397, -0.025544802, -0.0008764008, -0.007939433, -0.017586803, 0.012531062, 0.030025043, -0.042203378, -0.020817032, -0.008422111, -0.007821858, 0.048564825, 0.008162208, -0.040544946, -0.0091894455, -0.045000434, 0.02990128, -0.02215368, -0.048168782, -0.031584464, 0.024925984, 0.0141214235, -0.009325585, 0.008242654, 4.425998e-05, -0.023490326, 0.03658451, -0.008570627, 0.0045235585, -0.04368854, -0.018601665, -0.0027831334, 0.001021823, -0.047203425, -0.0012639355, 0.024616575, -0.014925887, -0.032599326, -0.0010867988, 0.037723135, -0.055198554, 0.010427081, -0.010352823, 0.012135019, 0.017809577, 0.010773619, 0.032228034, -0.004579252, -0.005795229, -0.042153873, -0.041337032, -0.024319543, 0.0024365953, -0.0046504163, 0.00013991086, 0.010148613, -0.013750133, -0.051683668, 0.03027257, 0.0037840712, 0.063862, -0.017017491, -0.009703064, -0.023119036, -0.043713294, -0.014257563, 0.0127228955, -0.00046295312, 0.012698143, 0.03693105, 0.0035922376, 0.014096671, -0.007413438, 0.04183209, 0.005671466, 0.027153729, -0.021250205, -0.0451242, 0.0011417189, 0.004681357, 0.013143691, 0.004678263, 0.036807287, 0.003186912, 0.0075310133, -0.03502509, 0.0020838692, 0.017586803, -0.011225356, -0.014109047, 0.019455634, -0.003295205, -0.016200652, -0.021869024, -0.016052136, -0.015953125, 0.0069802655, 0.0138367675, -0.0011393984, -0.012432051, -0.0008578363, -0.0039604343, 0.005321834, -0.0130446805, 0.013552112, 0.032772593, -0.030866636, 0.0074010617, -0.004712298, 0.0059746867, 0.0072896746, -0.005925181, -0.03576767, 0.0034344392, 0.01532193, 0.029703258, 0.0064233295, 0.009034741, -0.0314607, 0.033341907, 0.038416214, -0.012821906, 0.04002514, -0.015631339, -0.024331918, 0.0084344875, -0.008514934, 0.0015377623, -0.03037158, -0.009795886, 0.036435995, -0.009220386, 0.015841737, 0.0022942673, 0.033317152, 0.0018812063, 0.005504385, 0.044455875, 0.0069369483, -0.015161037, -0.03000029, -0.015581834, 0.022995273, 0.026906202, 0.0031714416, -0.03574292, 0.011169663, 0.00036935692, 0.03393597, -0.026039856, -0.021151194, 0.02184427, -0.032772593, 0.03108941, 0.0036077083, 0.003499415, 0.00533421, -0.033886466, -0.0037129072, -0.03618847, -0.031237926, 0.01790859, -0.012159771, -0.039183546, -0.029133946, 0.004050163, 0.016893728, -0.005368245, 0.010117672, -0.009505043, -0.021683378, -0.08782263, -0.023391316, 0.00798275, -0.01196175, 0.014925887, 0.03319339, 0.008768649, 0.040965743, -0.021794764, 0.027822051, 0.05133713, -0.042747937, -0.03257457, -0.0053775273, 0.02238883, -0.024171026, 0.038911268, -0.013193197, -0.012896164, -0.014566973, 0.0051764115, 0.035049845, 0.076188855, 0.02052, -0.066881835, -0.0018889415, -0.0006025739, -0.011027334, 0.0010504433, -0.026584415, -0.01759918, -0.019529892, 0.041337032, -0.013255078, 0.015433318, -0.02745076, 0.014901134, -0.0032766405, 0.04604005, -0.005953028, 0.030297322, -0.03173298, -0.018972956, -0.011404813, 0.048812352, 0.020755151, 0.004684451, -0.003926399, 0.017574428, -0.03626273, 0.021559615, 0.024109144, 0.022574475, -0.01518579, -0.015161037, 0.016732834, 0.027153729, -0.00456997, 0.035272617, 0.034133993, -0.014839252, 0.017376406, -0.01060035, 0.020606635, -0.0065347166, 0.017797202, -0.019170977, 0.030520096, -0.0071164053, -0.0154952, 0.00018245458, -0.02707947, 0.024319543, 0.004467865, -0.02201754, -0.0024721774, 0.0068069967, 0.051584657, 0.027401255, 0.0237131, 0.04703016, 0.037450857, -0.004526653, 0.037351847, 0.0013289114, 0.0045173704, -0.03163397, -0.034728058, -0.035619155, 0.017883835, 0.04457964, -0.015383812, -0.009610241, 0.014591726, -0.0024443306, -0.011986502, 0.002357696, -0.016831845, 0.0287379, 0.012772401, 0.047178674, 0.01919573, -0.026213124, 0.0018007599, -0.029331967, -0.0038181061, 0.016052136, 0.03591619, 0.014604102, -0.022772497, -0.019554645, 0.029307215, 0.007147346, 0.00135057, -0.03366369, 0.011479071, 0.0013095733, 0.036807287, -0.006206743, -0.0019848582, -0.01960415, 0.027178481, 0.02898543, -0.011936997, -0.043886565, 0.00506193, -0.012821906, 0.009424596, -0.012834283, 0.021584367, -0.0077723525, 0.022116551, 0.03829245, -0.05123812, 0.009610241, 0.0025541708, 0.0127600245, 0.0053094574, -0.019542268, 0.017821955, 0.0083787935, -0.020532377, 0.012747648, -0.009814451, 0.007233981, -0.011813233, -0.032153778, 0.017487792, -0.0076052714, -0.018341761, 0.004016128, 0.037079565, -0.026782436, 0.030767623, -0.0072463574, -0.020074451, -0.003743848, -0.0025510767, -0.0060303803, 0.017364029, -0.039158795, -0.0067079854, -0.021274958, 0.013774886, 0.023527455, 0.01627491, -0.029851774, 0.01654719, 0.0007742958, 0.011782292, -0.04002514, 0.0074629434, -0.0024551598, 0.034505285, 0.018193245, 0.023700723, 0.03646075, -0.00394187, 0.030940894, -0.0019647467, 0.051931195, 0.01586649, -0.021633873, 0.0019678408, 0.009591677, 0.011355308, 0.024963113, -0.038044922, -0.012667201, 0.01773532, -0.008781025, 0.021411099, -0.024826972, 0.009808263, 0.0072216047, -0.025569554, 0.021213077, -0.012407298, 0.015111532, -0.014665984, 0.03767363, -0.031188421, 0.005331116, -0.0105879735, 0.0045235585, -0.031980507, 0.012630072, -0.00436576, -0.01019193, -0.010408516, 0.008354041, -0.0049072257, 0.031262677, -0.006206743, -0.0019477293, 0.013873897, -0.006547093, 0.015532329, 0.000527929, -0.008576816, -0.017821955, 0.043267746, -0.031262677, -0.0023344904, -0.021213077, 0.0009057947, 0.010389952, 0.0029703258, 0.011324367, 8.890672e-05, -0.029109193, 0.0051825996, 0.027178481, -0.01759918, 0.02626263, -0.01131199, -0.0019121472, -0.01896058, 0.040520195, 0.013428348, -0.0040718215, 0.023812111, -0.01199269, -0.028935924, -0.0068998192, -0.018849192, 0.0074010617, 0.024641328, -0.021386346, 0.0180076, -0.01012386, -0.00366959, 0.019257613, -0.027574524, 0.0044152653, -0.009121375, -0.009467914, 0.011157286, 0.02725274, 0.023836864, 0.03366369, 0.023750229, 0.022463089, -0.017364029, -0.00686269, -0.0022880791, 0.00023166994, -0.025012618, -0.014071918, -0.009703064, 0.01100877, 0.008304535, 0.03858948, 0.0074938843, -0.031163668, -0.013787262, 0.016101642, -0.007005018, 0.026683426, -0.0024072016, -0.021856647, -0.035049845, 0.012110266, -0.026683426, -0.012883788, -0.0230324, -0.025148759, 0.01660907, 0.0072896746, -0.005442503, 1.4648579e-05, 0.018675923, -5.967604e-06, -0.027426008, -0.03183199, 0.018985333, 0.005980875, 0.0025588118, 0.023490326, -0.037252836, 0.0006895952, 0.0036108023, 0.0019291647, -0.034802318, -0.004328631, 0.013750133, 0.03794591, 0.0035891435, 0.004356478, -0.020445742, -0.0012855941, -0.0020529283, 0.004771086, -0.023638843, 0.023106659, 0.0084159225, -0.018019976, -0.010241436, -0.009121375, -0.0105879735, -0.04247566, -0.029109193, -0.018626418, 0.032129023, 0.030718118, 0.0151981665, 0.01777245, -0.034431025, -0.004251279, 0.053762898, 0.026881449, -0.0029950785, 0.014591726, 0.011825609, -0.012413486, 0.008836719, -0.019084344, -0.026757684, -0.05306982, -0.0051114354, 0.007964186, -0.0043874187, -0.019864053, -0.008335477, 0.005748818, 0.011670905, 0.014975392, -0.00477418, 0.014938263, 0.0062129316, 0.04725293, 0.011714222, -0.02863889, 0.020321978, 0.02330468, -0.00082689535, -0.040099397, 0.0239235, -0.025693318, -0.00066909683, 0.0024164838, 0.02151011, 0.018886322, -0.0123825455, -0.01678234, 0.032327045, 0.0011943185, 0.012407298, 0.0043131607, 0.0003534997, -0.008849096, 0.0041522677, -0.0055229496, 0.027326997, -0.037327092, 0.017302148, -0.018911075, 0.016262533, 0.051733173, 0.019393751, -0.015643716, 0.046015296, -0.00927608, 0.022141304, 0.0088119665, 0.02571807, -0.03722808, 0.026411146, -0.01719076, 0.0044833357, -0.007308239, 0.006048945, -0.008762461, -0.009734005, -0.018898698, -0.023935875, 0.016287286, -0.022141304, 0.012982799, -0.01660907, -0.009424596, 0.042525165, -0.008533498, 0.019554645, 0.034505285, -0.018539783, -0.008156019, -0.0063862004, 0.020123957, -0.030198311, 0.008496369, 0.01919573, -0.0314607, 0.0054796324, 0.014938263, -0.021361593, -0.012920917, 0.0065037757, -0.008960483, 0.034950834, 0.01773532, 0.016559565, -0.015606587, -0.031856745, 0.0087562725, 0.014319445, 0.004802027, -0.0028326388, -0.045445982, 0.026831942, -0.023997758, 0.010662232, 0.017586803, 0.023255175, 0.017017491, 0.0019090531, 0.017141255, -0.03792116, 0.022710616, 0.021794764, -0.026609167, -0.030495344, 0.005550796, -0.010798371, 0.005609584, 0.0039325873, 6.5265944e-05, -0.014183305, 0.014740242, 0.0048670024, 0.016262533, 0.010557033, -0.04294596, 0.024678456, -0.0043905126, -0.004802027, -0.07534726, 0.0159655, 0.0011958656, 0.038416214, 0.01896058, -0.020445742, 0.054109436, -0.011219168, -0.003499415, 0.015643716, -0.01681947, 0.023131412, -0.014294692, -0.035990447, -0.00070622587, 0.0033106755, 0.02082941, 0.004288408, 0.021522485, -0.03985187, -0.012277346, 0.022166057, -0.010699361, 0.005442503, 0.0223517, 0.0033663693, -0.020062076, 0.003774789, 0.05326784, -0.010903571, 0.014356574, -0.028242847, 0.0051454706, 0.014703113, 0.02092842, -0.0040285042, 0.008626321, 0.010136236, 0.05207971, 0.015816985, -0.003815012, 0.015903618, -0.036064707, 0.023440821, 0.007233981, -0.036733028, -0.0151981665, -0.008929541, 0.023948252, -0.0145174675, -0.0023468668, 0.017722944, -0.010420892, -0.007970374, 0.008583004, -0.0004180888, -0.017859083, -0.007512449, 0.007691906, -0.0046658865, -0.00013207895, -0.040544946, -0.044629145, 0.012896164, 0.022005163, -0.002801698, 0.014170929, 0.02789631, -0.0021395627, -0.016497685, 0.030718118, 0.04712917, -0.045396477, 0.011163474, -0.0016862787, -0.014034789, -0.026460651, 0.00547035, 0.010451834, 0.03037158, 0.018576913, -0.023168541, -0.0017976658, -0.018911075, 0.0049969545, -0.040049892, -0.02320567, -0.008979048, -0.026287382, 0.0006609748, -0.061832283, -0.022227937, -0.01956702, 0.0050340835, -0.004016128, -0.017339276, 0.029109193, -0.028787406, -0.00025081463, 0.04294596, -0.013638746, -0.021089314, -0.006410953, -0.032624077, 0.03655976, -0.005959216, 0.010495151, -0.0047834623, 0.030569602, -0.039703354, -0.009065682, -0.022772497, -0.0014843893, 0.013539735, 0.028663643, -0.026906202, 0.030074548, -0.016386297, -0.027797299, 0.0016445084, 0.015297177, -0.018725429, 0.010253812, 0.016114017, -0.027326997, -0.021807142, -0.017623933, 0.0028481092, -0.009498854, -0.0019554645, 0.0011231544, 0.035396382, 0.015532329, -0.0032487938, -0.018329386, 0.01196175, 0.022401206, 0.06302041, 0.0004919602, 0.010427081, -0.01066842, 0.021547237, -0.004495712, -0.010031038, 0.0040377867, 0.047797494, -0.0022246502, -0.014703113, -0.031881496, 0.009560736, -0.0013567582, 0.026237877, -0.0034406274, -0.0314607, -0.04574302, 0.016163522, 0.04514895, -0.014591726, 0.021794764, -0.022463089, 0.04641134, -0.0152352955, -0.040619206, 0.03264883, 0.022376454, -0.03794591, 0.014084294, -0.018391266, -0.032104272, 0.015074403, 0.01386152, -0.0071287816, -0.009399843, -0.03373795, 0.026336888, -0.012258782, -0.005052648, 0.008242654, 0.0059468397, -0.0076857177, -0.00630266, 0.021373969, -0.0001493865, -0.017351653, -0.015309554, -0.016472932, -0.037624124, -0.009102811, -0.012958046, 0.03591619, -0.006435706, 0.02853988, -0.03190625, 0.01318082, -0.023589337, 0.031064657, 0.023564585, -0.03175773, -0.014963016, 0.008787214, -0.017450664, 0.027178481, -0.011342932, 0.031015152, -0.054356962, -0.013824391, -0.00927608, 0.003289017, 0.0065965983, 0.012896164, -0.015099156, 0.040718216, -0.008125079, 0.01532193, -0.052921303, 0.0037995416, -0.0013575316, 0.027871557, 0.024307165, -0.0173269, 0.02061901, -0.018576913, 0.0013722286, 0.0364855, -0.0027475513, -0.0042203376, -0.0052723284, 0.026708178, 0.035074595, -0.0026500875, -0.004436924, 0.008471617, 0.04514895, -0.053564873, -0.020024946, 0.02106456, -0.0038738, 0.009628806, 0.018762557, 0.0013258173, -0.0058137937, -0.008162208, 0.0038057298, -0.042723186, 0.0021612213, 0.012388733, 0.044827167, -0.0134035945, -0.0161759, 0.013601617, 0.040792473, 0.003326146, 0.02096555, -0.029604247, -0.023799734, -0.0083973585, 0.046163812, -0.019826924, -0.019703161, -0.009498854, -0.02521064, -0.0021302805, 0.013552112, -0.023997758, -0.029232956, 0.028564632, 0.021274958, -0.010303318, 0.0040006577, -0.029975537, -0.05891146, -0.02972801, -0.0013304584, -0.030223064, 0.0037314717, 0.0016816376, -0.03846572, 0.0036169905, 0.013230326, 0.009251327, 0.0034901327, -0.017302148, 0.0038521413 ] }, { "object": "embedding", "index": 2, "embedding": [ 0.0073202015, -0.023429625, 0.028185267, -0.057266884, -0.008801671, -0.019284, 0.031098407, 0.017565994, 0.017765183, 1.2607371e-05, -5.276373e-05, -0.028533848, -0.03067513, -0.034185838, -0.029529793, 0.0055492856, -0.006591916, 0.004830337, -0.033015605, 0.02773709, -0.0014666858, -0.014827143, -0.0040989392, 0.03821942, 0.01434162, -0.00487391, 0.0010255129, 0.032592326, 0.025745198, -0.03291601, -0.02057873, -0.04028601, -0.002259552, -0.026790941, -0.011272862, 0.05437864, -0.008067161, 0.015810639, -0.044244893, 0.009243622, 0.00926852, 0.00500774, -0.03436013, 0.0071583604, 0.0069155986, 0.04207871, -0.013806298, -0.00062363327, -0.024736803, 0.018325403, -0.063342154, 0.007768377, -0.009349441, 0.013507515, -0.018350301, 0.012760555, -0.019022565, 0.010681518, -0.013482616, -0.045614317, 0.04499185, -0.014565707, -0.01525042, -0.033239692, -0.017852329, 0.024139237, -0.014627954, 0.06772432, 0.023952497, 0.0137440525, 0.038518205, 0.010762439, -0.04578861, 0.0141050825, -0.0034484623, 0.013731603, -0.00038378927, -0.0064051766, 0.0147897955, -0.05437864, -0.030027766, 0.008702076, -0.0367504, -0.0054528033, -0.030973915, 0.0060970555, -0.053681478, -0.021574676, -0.019358696, -0.024935992, -0.049224623, -0.08948573, -0.035679758, -0.017516198, -0.009685573, 0.002125722, -0.008036038, -0.0569681, -0.031496786, 0.0036196406, -0.006146853, 0.031671077, -0.022471027, 0.018051518, -0.004151849, -0.022856956, 0.021338139, -0.016445555, -0.04792989, -0.040111717, -0.06468668, 0.008633605, -0.0016137435, 0.013843646, 0.08097039, 0.03179557, 0.011272862, -0.013495065, -0.012113191, -0.046585366, -0.024201483, -0.024811499, 0.078380935, -0.0011787952, -0.018449895, -0.05288472, -0.023678612, -0.06413891, -0.047158033, 0.0021132724, 0.013258529, -0.0127045335, 0.03291601, 0.041281953, -0.03358827, -0.034185838, -0.07185749, 0.034111142, -0.017304558, 0.021387937, -0.008347271, -0.04581351, 0.0018938532, -0.035231583, 0.0007286744, -0.008683402, -0.03331439, 0.039987225, 0.02738851, 0.0008800115, -0.053034116, -0.003747246, -0.055274993, 0.016955977, -0.057665262, -0.023678612, -0.025035588, 0.003379991, 0.039065976, -0.0021910807, -0.01652025, -0.011011425, 0.01947074, 0.009119129, 0.0016759901, 0.015374913, 0.04247709, 0.0023233548, -0.03533118, -0.080621816, 0.021101601, -0.029704083, -0.06045391, 0.01185798, -0.024811499, -0.005253614, -0.0029209224, -0.043672223, 0.01010885, 0.012972194, 0.03747246, 0.028259963, -0.017702937, 0.008073386, -0.035405874, 0.049224623, 0.006691511, 0.019620132, 0.015374913, 0.032243744, 0.022657767, 0.027986078, 0.06339195, -0.022533273, 0.014715099, -0.025794996, 0.025272125, -0.03358827, 0.048427865, -0.011472051, 0.013370573, 0.034609117, -0.023579016, 0.0009943896, 0.021325689, -0.00039098653, -0.008421967, -0.036700603, -0.020777918, 0.02903182, 0.052038167, 0.0042296574, -0.013270978, -0.006100168, -0.0368002, 0.041854624, -0.02738851, -0.011721037, 0.02644236, -0.034086246, -0.0028508948, 0.028733036, 0.06448749, 0.012872599, -0.019719727, -0.010009255, -0.0052193785, 0.007855522, 0.030426145, -0.03946435, 0.02591949, -0.018449895, 0.0037534707, -0.049523406, 0.0014153323, -0.0088950405, -0.013034441, 0.017092919, -0.029280806, 0.0027232892, -0.025160082, -0.007513166, 0.026168477, 0.010955404, 0.014715099, 0.008577583, 0.00920005, -0.035679758, 0.016607396, 0.012088292, -0.01037651, 0.03341398, 0.0050388635, -0.0116774645, -0.030251853, 0.03194496, -0.03398665, 0.048029486, 0.03734797, 0.005919653, 2.349445e-06, 0.026143577, -0.021898359, 0.01487694, -0.0034795858, -0.03142209, 0.013445268, -0.0014192228, 0.02298145, -0.07444695, -0.0085340105, -0.043124452, 0.039763138, 0.0095797535, -0.049622998, 0.033239692, -0.02701503, -0.02187346, -0.0064425245, 0.018250708, -0.02552111, -0.050021376, 0.009249846, 0.03933986, -0.006025472, 3.6229474e-05, 0.026840739, -0.026367664, -0.01927155, -0.02920611, 0.0034515746, -0.0532831, 0.044618372, 0.015175724, -0.012250134, 0.016893731, 0.016433105, 0.0034235637, 0.015225521, -0.030027766, 0.030251853, -0.026940335, 0.034061346, 0.07195708, -0.028334659, 0.04299996, -0.018225808, 0.019769523, 0.029903272, -0.0021288341, -0.01766559, -0.0014534584, -0.036601007, 0.025247226, -0.06045391, 0.0011289979, 0.049324214, -0.005278513, -0.0028026537, -0.03341398, -0.004892584, -0.0015048118, 0.009299644, -0.018761128, 0.0032835088, 0.00076018675, -0.012075843, -0.010837135, -0.021661822, 0.01599738, -0.019234203, 0.008851469, 0.040659487, -0.01856194, -0.03271682, -0.00086834026, -0.018412547, -0.011353782, -0.014827143, 0.01203227, -0.06787371, -0.010525902, 0.056320734, -0.022271838, -0.021562226, 0.043448135, -0.027264016, 0.026915435, 0.064636886, 0.016632294, 0.012741881, -0.003821942, -0.0071023386, -0.033239692, 0.061151072, -0.073351406, -0.009585978, 0.01782743, -0.004883247, -0.023193087, 0.03139719, 0.0056426553, -0.023641264, 0.024749253, 0.0062371106, -0.029654287, -0.013706704, 0.041555837, 0.0035511693, -0.031023711, -0.03251763, 0.019010115, 0.024226382, 0.0078243995, -0.029131414, 0.02517253, -0.022159794, 0.049000535, 0.03732307, 0.034185838, 0.022969, -0.04979729, -0.041032966, -0.014055285, -0.013868545, 0.013320775, -0.019047463, 0.003361317, 0.0030843196, 0.038119826, 0.009349441, 0.0012387076, 0.008527786, -0.011777059, 0.038742293, 0.017889677, 0.03562996, -0.008384619, 0.011372456, 0.009585978, -0.0075754127, -0.038319014, -0.015760843, -0.018238258, 0.013582211, 0.027338712, 0.023653712, 0.004624923, 0.04058479, 0.011210615, 0.0127107585, 0.0008387731, 0.036127932, -0.026741145, 0.0077995006, 0.058910195, 0.028384455, -0.06299357, 0.025770098, 0.0533329, -0.025234777, -0.022620419, -0.061649047, -0.0067537576, 0.0069342726, -0.0032523854, 0.061101276, -0.00044272904, -0.018810926, -0.035530366, -0.037771244, 0.0165327, 0.03104861, 0.017105369, -0.04302486, -0.043448135, 0.022807159, 0.042402394, -0.027039928, -0.01487694, 0.0099221105, 0.030949015, -0.041456245, -0.037746347, -0.016034728, -0.016607396, -0.025160082, 0.027861584, -0.034434825, -0.018499693, -0.040186413, -0.07668783, -0.012225235, -0.019321349, 0.0368002, -0.037596952, -0.011061223, 2.1300013e-05, 0.03879209, -0.008004914, -0.0011507842, -0.054478236, -0.046535566, 0.025944388, 0.04576371, -0.008951063, -0.044369385, 0.029131414, 0.042178303, -0.038169622, 0.0066043655, 0.017877227, -0.04205381, 0.005564847, 0.014889389, 0.000404603, -0.006081494, -0.040759083, -0.0032088128, -0.019184407, -0.00036103034, 0.030924117, -0.045713913, -0.009673123, -0.045091446, 0.13116606, -0.029455097, -0.024836399, 0.009691797, -0.0072828536, -0.0137814, -0.0123684015, -0.047456816, -0.013196282, 0.044742864, -0.0011212172, -0.0032679471, -0.08022343, 0.003812605, -0.026243173, 0.010301814, 0.014254474, 0.008795447, 0.02278226, 0.040086817, 0.004413285, -0.036451615, 0.020167902, -0.0074695935, -0.016072076, 0.02885753, -0.026417462, 0.0072828536, -0.010289365, 0.0020634753, 0.01765314, -0.005592858, 0.05771506, 0.0035636185, -0.04633638, 0.024911094, -0.017864779, 0.023230435, -0.05218756, -0.01048233, -0.027413407, -0.00019957821, -0.037148777, -0.021587126, -0.036874894, -0.034634016, -0.036625907, 0.039763138, -0.028832631, -0.020317294, -0.007867971, 0.0056053074, 0.016395757, 0.05036996, -0.0067039602, 0.02828486, 0.0183752, 0.031496786, 0.0045346655, -0.026691347, 0.030226955, 0.009312093, 0.026716245, -0.0034858102, 0.022856956, 0.023242885, -0.039290063, 0.019557886, -0.020777918, -0.0017102257, -0.011254188, 0.030202057, -0.0017397929, -0.018499693, 0.018786028, 0.024313526, 0.007145911, 0.026616652, 0.0495732, -0.03582915, 0.0312727, 0.006100168, 0.016470453, 0.0440706, -0.018910522, -0.0048241126, -0.010911832, 0.0174664, 0.032044556, -0.009866088, 0.012424424, -0.014976535, 0.04501675, -0.034808304, -0.026517058, 0.015872886, -0.027089726, 0.016744338, 0.015337565, 0.001167902, 0.03585405, 0.031098407, -0.056868505, -0.012430648, 0.0067724315, 0.04339834, -0.0019140834, 0.022720013, -0.021437733, 0.0057764854, -0.023566568, 0.040111717, -0.011777059, 0.008229002, 0.016022278, -0.009112904, 0.022931652, -0.0014915845, 0.01691863, 0.03697449, 0.032990705, -0.019632582, 0.017914575, 0.016582498, 0.02848405, -0.015474508, -0.027139522, -0.0037441337, 0.0071583604, 0.03341398, -0.0020588066, -0.012293706, -0.009175151, 0.010532127, 0.0011803514, 0.0033519801, -0.013656907, -0.0028944674, -0.002061919, -0.028334659, 0.052436545, 0.0031310045, -0.024848849, 0.0059165405, -0.0012822802, -0.029479995, 0.015935132, 0.007369999, -0.053133707, -0.011179492, -0.007867971, -0.008863918, 0.043473035, 0.011727261, -0.030749826, -0.012306156, -0.027811786, 0.023989845, -0.0036507638, -0.033936854, -0.026193375, 0.0051540197, 0.020491585, -0.003076539, -0.000659036, 0.021201197, -0.029828576, 0.029529793, 0.0016152996, 0.0056239814, -0.04280077, -0.02169917, 0.012175437, 0.008969737, -0.058760803, -0.0014627954, 0.026218273, 0.001261272, -0.036725502, -0.003868627, 0.04392121, -0.06100168, 0.01598493, -0.007351325, 0.013134035, 0.021350589, 0.008814121, 0.03154658, -0.0005909538, 0.0130593395, -0.03789574, -0.027064826, -0.016619846, -0.008540235, -0.006473648, -0.004453745, 0.023280233, -0.010370285, -0.04150604, 0.026790941, 0.0020307957, 0.05071854, -0.022259388, -0.012380851, -0.017565994, -0.039837833, -0.0053998935, 0.00872075, 0.005393669, 0.0047867643, 0.03640182, -0.003793931, 0.006118842, -0.001232483, 0.039962325, 0.013283427, 0.032243744, -0.028733036, -0.059457965, 0.004830337, -0.0022081987, 0.012847701, 0.009324542, 0.0440955, 0.0072330562, -0.0052287155, -0.023728408, 0.0015087023, 0.019732175, 0.0013904338, -0.01765314, 0.015474508, -0.0042825667, -0.011882878, -0.023753308, -0.010189771, -0.020665875, 0.0099221105, 0.013644458, -0.009063107, -0.01543716, -0.011801958, -0.013333225, -0.0068844752, -0.022695115, 0.013818748, 0.020541383, -0.022670217, -0.0023700397, -0.015412262, 0.0013017323, -0.0029785004, -0.012654737, -0.030475942, 0.005592858, 0.023628814, 0.020628527, -0.00463426, 0.013644458, -0.035032395, 0.029579591, 0.044369385, -0.013569761, 0.04148114, -0.018487245, -0.030002868, 0.008646054, -0.012206561, -0.009691797, -0.035206683, -0.014615505, 0.04028601, -0.012405749, 0.026193375, -0.013856096, 0.03729817, -0.0018876286, 0.009803842, 0.051341005, -0.0040584793, -0.013955691, -0.034783408, -0.014889389, 0.03087432, 0.022844506, 0.014316721, -0.033214793, 0.01212564, 0.0063865026, 0.03876719, -0.027811786, -0.02351677, 0.018437447, -0.039016176, 0.026043983, 0.0018098203, 0.0032617224, 0.005546173, -0.05751587, -5.8988393e-05, -0.01577329, -0.03562996, 0.0046747206, -0.007818175, -0.047506616, -0.024039641, 0.017491298, 0.020354642, 0.005337647, 0.00528785, -0.01579819, -0.020479135, -0.07828134, -0.018275606, 0.018611737, -0.015574102, 0.005054425, 0.035530366, 0.014192227, 0.051390804, -0.032318443, 0.037173677, 0.06573242, -0.029529793, -0.013644458, 0.0034360131, 0.03640182, -0.028782833, 0.028309759, -0.02147508, -0.025595807, -0.0064425245, 0.009492609, 0.03709898, 0.07828134, 0.013432819, -0.06886965, -0.0015149269, -0.008229002, -0.0036725502, 0.003107662, -0.018288055, -0.025770098, -0.018972768, 0.03328949, -0.0005602195, 0.018898072, -0.037173677, 0.015872886, 0.004743192, 0.0440457, -0.02074057, 0.026766043, -0.035804253, 0.00069288263, -0.019831771, 0.034036446, 0.019844221, 0.005346984, 0.007500717, 0.016893731, -0.03931496, 0.03254253, 0.025570909, 0.010731316, -0.014640403, -0.012380851, 0.016308613, 0.021898359, 0.0073824483, 0.033662967, 0.032691922, -0.023031246, 0.010793563, -0.015325116, 0.010183546, -0.0044008354, 0.012287482, -0.015872886, 0.03142209, 0.00021066588, -0.026392564, -0.00532831, -0.023043696, 0.028957125, 0.00799869, -0.015847988, -0.0036756624, 0.012206561, 0.057217088, 0.032741718, 0.013295877, 0.054677423, 0.03182047, 0.012611164, 0.03236824, 0.0010978746, 0.0072455057, -0.019632582, -0.04797969, -0.038094927, 0.012119415, 0.04135665, -0.022844506, -0.0014262255, 0.008851469, -0.0013865433, -0.005695565, 0.015673697, -0.019520538, 0.0220353, 0.014192227, 0.040360704, 0.028932225, -0.041032966, 0.0035418323, -0.030550638, 0.006296245, 0.0015997379, 0.025284573, 0.012038494, -0.017192515, -0.024052091, 0.015847988, 0.00900086, -0.007519391, -0.032169048, 0.013843646, 0.012299931, 0.047904994, -0.004422622, -0.011353782, -0.025321921, 0.020665875, 0.013905893, -0.0035324953, -0.045166142, 0.010830911, -0.024935992, 0.0057266885, -0.0070463168, 0.016756788, -0.004951718, 0.0081978785, 0.026417462, -0.059258774, -0.0069902944, -0.0042203204, 0.009953233, 0.010507228, -0.010681518, 0.024139237, 0.0054808143, -0.011515623, 0.015947582, -0.008614931, 0.00097104715, -0.008110734, -0.03234334, 0.00762521, -0.009592203, -0.01948319, 0.010681518, 0.046610262, -0.026218273, 0.025446415, -0.006635489, -0.019869119, -0.010830911, -0.013183832, -0.00679733, 0.023217987, -0.030998813, -0.011409804, -0.023292683, 0.014578157, 0.029828576, 0.008116959, -0.031297598, 0.012573816, -0.0044661947, 0.020317294, -0.027911382, -0.016022278, -0.0044599697, 0.019943815, 0.023292683, 0.021636922, 0.028010976, 0.0053812196, 0.028085671, 0.003482698, 0.05273533, 0.01765314, -0.029355502, 0.0013204062, 0.0029395963, 0.018238258, 0.006853352, -0.023877801, -0.010830911, 0.02222204, -0.009336992, 0.018624187, -0.0062184366, 0.015387363, 0.011988698, -0.03936476, 0.030550638, -0.018138662, 0.016682092, -0.006205987, 0.037248373, -0.019246653, 0.0024634097, -0.006859577, 0.00091658137, -0.03251763, 0.01451591, 0.0038188298, -0.014179778, 0.01203227, 0.008764323, -0.011347557, 0.045489825, 0.0024805276, -0.005919653, 0.020827716, -0.016308613, -0.0065358942, 0.004901921, 0.0061810887, -0.022197142, 0.043074656, -0.022060199, -0.009878538, -0.014939187, -0.009318318, -3.4940775e-05, -0.004285679, 0.017341906, -0.0067350836, -0.041207258, 0.0014340064, 0.024139237, -0.02295655, 0.037596952, -0.013868545, -0.014939187, -0.013121586, 0.0495732, 0.017976822, 0.007662558, 0.024375774, -0.009505058, -0.027836686, -0.009940784, -0.01801417, 0.0032897335, 0.010494779, -0.02318064, 0.0113164345, -0.022010403, -0.003688112, 0.025819896, -0.021275893, 0.009175151, -0.008036038, -0.003295958, 0.01561145, 0.01673189, 0.02554601, 0.02666645, 0.03142209, 0.006965396, -0.011341332, -0.010227119, 0.005063762, -4.043598e-05, -0.008353495, -0.012187887, 0.002071256, 0.0155118555, 0.014964086, 0.042352594, 0.011540522, -0.041954216, -0.006946722, 0.014939187, -0.016209017, 0.03787084, 0.0073575494, -0.027463205, -0.03331439, 0.016582498, -0.018449895, -0.010413858, -0.038468406, -0.021997953, 0.017964372, 0.0202177, -0.019346246, -0.0006831566, 0.013246079, -0.005346984, -0.04631148, -0.029529793, 0.020504035, 0.00634293, -0.0038748516, 0.028434252, -0.030251853, -0.001873623, 0.012262583, 0.006215324, -0.03560506, 0.008658504, 0.002015234, 0.042178303, 0.008415742, 0.009162702, -0.010357836, 0.0046155863, -0.000157659, 0.0042825667, -0.007830624, 0.01450346, 0.0030267416, -0.0075816372, -0.01432917, -0.022570621, -0.0007391785, -0.028957125, -0.022670217, -0.03689979, 0.028882429, 0.016682092, 0.01947074, 0.023404727, -0.03012736, -0.0063398173, 0.049672797, 0.010139973, -0.01968238, 0.021114051, 0.0053034113, -0.026043983, 0.006909374, -0.0137440525, -0.022060199, -0.044568576, -0.008135633, 0.01618412, -0.004276342, -0.02721422, -0.0031387855, 0.013333225, 0.0062371106, 0.0078492975, 0.0055959704, 0.018462345, 0.011789508, 0.047481716, 0.018985217, -0.0367504, 0.020889964, 0.024276178, -0.005760924, -0.035032395, 0.01654515, -0.024487818, -0.0071396865, 0.0018985217, 0.017715385, 0.029504895, -0.024562513, 0.0005314305, 0.03508219, -0.008807896, 0.0081978785, 0.004933044, -0.0072143823, -0.013980589, 0.0058885296, 0.007152136, 0.022010403, -0.04187952, 0.0051166713, -0.01673189, 0.020055858, 0.041779928, 0.026168477, -0.019856669, 0.048004586, -0.006492322, 0.017678037, 0.0011173267, 0.02240878, -0.042925265, 0.025433965, -0.003501372, 0.0044879806, -0.017964372, -0.001242598, -0.0053625456, 0.0021039355, -0.013308326, -0.025844794, 0.021275893, -0.021935707, 0.007681232, -0.014615505, -0.0045222165, 0.03697449, -0.02920611, 0.023466973, 0.02479905, -0.010332937, -0.0070587657, 0.006090831, 0.021748966, -0.030600434, 0.0016542037, 0.017503748, -0.030550638, 0.012194111, 0.003295958, -0.011745936, -0.008415742, 0.014266924, -0.0071023386, 0.04541513, 0.020491585, 0.023217987, -0.009274745, -0.017976822, 0.015399812, 0.0049174824, -0.0026236947, -0.0031278923, -0.056519926, 0.03543077, -0.0033333062, 0.008795447, 0.012679635, 0.02128834, 0.0074446946, 0.0045595644, 0.008054712, -0.0312478, 0.034310333, 0.02900692, -0.025160082, -0.0151383765, 0.0038561777, -0.0065358942, 0.0067662066, 0.0040709283, -0.009193825, -0.01765314, 0.014266924, 0.0041580736, 0.026840739, 0.0001181713, -0.041854624, 0.021226095, -0.0001826549, -0.01304689, -0.07384938, 0.013582211, -0.0057640364, 0.029181212, 0.011721037, -0.018487245, 0.031920064, -0.0120073715, -0.009791392, 0.012667186, -0.011403579, 0.012604939, -0.00826635, -0.04541513, 0.005356321, 0.0029022484, 0.014578157, 0.00038378927, 0.019918917, -0.036650807, -0.008073386, 0.029828576, -0.009075556, -0.0032337115, 0.018424997, -0.0130593395, -0.014926738, 0.01175216, 0.057416275, -0.018312953, 0.012947296, -0.030749826, 0.0155118555, 0.02626807, 0.021786314, 0.008558909, 0.014204677, -0.0054185675, 0.059109382, 0.02151243, 0.0020930422, 0.022894304, -0.038468406, 0.021599574, 0.0020339082, -0.047207832, -0.022919202, -0.013196282, 0.032094352, -0.0047992137, 0.0029178099, 0.015088579, -0.01432917, -0.018661534, 0.008770548, -0.011528073, -0.027363611, -0.013681806, -0.0037161228, -0.006215324, -0.0062277736, -0.029504895, -0.016420657, 0.008926164, 0.027687293, -0.008515337, 0.005947664, 0.026342766, -0.0020603628, -0.034285434, 0.03179557, 0.02738851, -0.0404354, 0.0003981838, -0.0072828536, -0.028782833, -0.01232483, -0.005957001, 0.010513453, 0.026591754, 0.019147057, -0.019234203, 0.0042701177, -0.019645032, 0.0065358942, -0.041954216, -0.02699013, -0.00717081, -0.0127107585, 0.00045984684, -0.065383844, -0.028309759, -0.01506368, 0.019557886, -0.008646054, -0.006162415, 0.026143577, -0.02187346, 0.0039962325, 0.029778779, -0.008154307, -0.015648799, -0.0061157295, -0.039987225, 0.06030452, -0.008764323, 0.017130267, -0.003566731, 0.02242123, -0.03565486, -0.01543716, -0.009754044, -0.0137689505, 0.004581351, 0.024052091, -0.013532413, 0.024002293, -0.0013001761, -0.027761988, -0.0018051518, 0.019918917, -0.020927312, 0.013843646, 0.0063180313, -0.0247866, -0.025471313, -0.019433392, 0.0012005816, 0.0041954215, 0.0069965194, -0.003840616, 0.041680332, 0.021188747, -0.010874483, -0.017329456, 0.014615505, 0.022570621, 0.04753151, -0.0017942586, 0.012306156, 0.00025365496, 0.026367664, 0.0025458864, 0.0030656457, 0.0102769155, 0.041306853, 0.00698407, -0.016333511, -0.028085671, 0.019246653, -0.0033021828, 0.026965233, -0.0031699087, -0.037547156, -0.04302486, 0.010295589, 0.050793234, -0.016868832, 0.023354929, -0.011920227, 0.033762563, -0.0067537576, -0.05091773, 0.037696548, 0.014204677, -0.040858675, 0.0029442648, -0.01304689, -0.029679185, 0.008932389, 0.017105369, -0.011739711, -0.002286007, -0.028185267, 0.020603629, -0.0070774397, -0.015711045, 0.0040242434, -0.0012309267, -0.005091773, -0.0037752572, 0.016209017, 0.0010994308, -0.015661247, 0.002334248, -0.012748106, -0.022508375, -0.0056613293, -0.016283713, 0.03182047, -0.0026236947, 0.024052091, -0.020180352, 0.0012402638, -0.0247866, 0.020267498, 0.0312229, -0.03530628, -0.029878374, 0.014204677, -0.021275893, 0.0085215615, -0.0027372949, 0.027438307, -0.04282567, -0.011708587, -0.014528359, 0.0113102095, 0.0027092837, 0.0010978746, -0.0116587905, 0.039065976, -0.007805725, 0.013980589, -0.03510709, 0.013432819, 0.0025972398, 0.024462918, 0.025969287, -0.032816414, 0.028882429, -0.01893542, 0.0057578115, 0.03620263, -0.01046988, -1.8467306e-05, -0.0067848805, 0.019781973, 0.025160082, -0.007874196, -0.0032119253, 0.0044070603, 0.035206683, -0.05086793, -0.01543716, 0.017391704, -0.002295344, 0.00984119, 0.027338712, 0.010793563, -0.012299931, -0.0037503585, -0.0023295796, -0.030525738, 7.839183e-05, 0.027886482, 0.05198837, -0.010501004, -0.013681806, 0.009903436, 0.04150604, 0.016893731, 0.0039246487, -0.039215367, -0.0030080676, -0.007482043, 0.044717968, -0.013109136, -0.027164422, -0.015885336, -0.010407633, 0.001722675, 0.013719154, -0.0021288341, -0.02793628, 0.033712767, 0.017055571, -0.0064238505, 0.0005473812, -0.030650232, -0.04536533, -0.018275606, -0.0013242967, -0.029604489, 0.002899136, -0.019844221, -0.034833204, 0.0035418323, 0.015549203, 0.011534297, 0.016582498, -0.008664728, 0.011291536 ] } ], "model": "text-embedding-3-small", "usage": { "prompt_tokens": 24, "total_tokens": 24 } } headers: CF-RAY: - 939c182f4cd37365-NRT Connection: - keep-alive Content-Type: - application/json Date: - Sat, 03 May 2025 01:48:10 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-allow-origin: - '*' access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '99595' openai-model: - text-embedding-3-small openai-organization: test_openai_org_id openai-processing-ms: - '241' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload via: - envoy-router-cfdbfb9c8-hwbng x-envoy-upstream-service-time: - '87' x-ratelimit-limit-requests: - '5000' x-ratelimit-limit-tokens: - '5000000' x-ratelimit-remaining-requests: - '4999' x-ratelimit-remaining-tokens: - '4999967' x-ratelimit-reset-requests: - 12ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_5196e0fdd3335f673750fd441d67c9df status: code: 200 message: OK version: 1 test_embeddings_with_content.yaml000066400000000000000000001355201527070072300457240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "This is a test for embeddings", "model": "text-embedding-3-small" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '77' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - Linux x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.9.2 method: POST uri: https://api.openai.com/v1/embeddings response: body: string: |- { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": [ 0.009180067, -0.010902188, 0.026030775, -0.040260565, -0.022860976, -0.035018735, 0.035101064, 0.027800921, -0.00014118728, 0.020624276, 0.019938173, -0.018442467, -0.021955319, -0.02640127, -0.017372146, 0.012363588, -0.025468169, 0.006246974, -0.008157773, 0.013982793, 0.01568433, -0.04215421, 0.008274411, 0.030874666, 0.015341279, -0.002785581, 0.016109714, 0.03825714, 0.052912317, -0.02176321, 0.0065557207, -0.029420126, -0.012507671, -0.026030775, -0.017687753, 0.033728857, -0.020734053, 0.044898625, -0.03757104, 0.016288102, -0.0027169706, 0.0055128434, -0.05293976, 0.007835304, 0.0002523575, 0.05115589, -0.0586207, -0.028102808, -0.03408563, 0.047478374, -0.062078662, 0.022929586, -0.0034185117, 0.004120053, -0.01919718, 0.012397894, -0.0330702, 0.0039794017, -0.0054819686, -0.013722074, 0.045090735, -0.00966034, -0.012082286, -0.014161181, -0.00014526102, -0.009571146, -0.02626405, 0.044898625, 0.0027872962, -0.016589988, 0.053076982, 0.023560802, -0.029502459, 0.01521778, 0.017481923, 0.013680908, -0.004703241, 0.015917607, -0.021557378, -0.045886617, -0.033975855, 0.0267306, -0.014655175, 0.00055745925, -0.024109684, -0.0098661715, -0.0834851, -0.01805825, -0.021694599, -0.0455024, -0.016192047, -0.050552122, -0.06465841, -0.019691177, 0.014243512, 0.008720378, 0.008445936, -0.048246812, -0.030847222, 0.01587644, -0.0040720254, 0.017385868, -0.009742673, 0.0018250358, 0.00080617185, -0.023739187, 0.016260657, -0.015711775, -0.053955194, -0.015300113, -0.046956938, -0.010133752, 0.015725497, 0.011903899, 0.059828244, 0.0009811283, 0.003096043, -0.043910637, -0.029228017, -0.026195439, -0.023643134, -0.021955319, 0.040397786, -0.03630861, -0.021625988, -0.029749457, -0.01833269, -0.026442437, -0.036747713, -0.01224009, -0.002156081, -0.021433879, 0.038202252, 0.050771672, -0.04882314, -0.022929586, -0.07898426, 0.022929586, -0.013111441, 0.053900305, 0.0014133736, -0.021529933, 0.01885413, -0.02354708, -0.023588246, -0.0133653, -0.032932978, 0.049920905, 0.030929554, -0.015766663, 0.0021029077, -0.00094510784, -0.051265668, -0.0053687613, -0.04956413, -0.03784548, -0.025646556, -0.009159484, 0.036143944, -0.012562559, -0.018675743, 0.024823232, 0.029228017, -0.012150897, -0.0025248616, 0.016466489, 0.017276092, -0.005941658, -0.013420189, -0.046215944, 0.0026054787, -0.02375291, -0.026799211, 0.00025943297, -0.024178294, -0.021090828, -0.001303597, -0.07892937, 0.02335497, -0.023327526, -0.011361877, 0.026085662, -0.020198893, 0.0069433693, -0.050936338, 0.060706455, 0.01833269, 0.06356065, 0.0063739032, 0.0045831725, -0.005962241, 0.037296597, 0.06367043, 0.0055608703, -0.00023348967, -0.026373826, 0.05365331, -0.0123087, 0.04896036, -0.015409889, 0.0015600283, 0.04723138, -0.025715167, 0.021475045, 0.026689434, -0.013495659, -0.028322361, -0.0246174, -0.035238285, 0.00021505063, 0.048246812, -0.024960453, -0.005138917, 0.0031080497, -0.020473335, 0.033372086, -0.038888358, -0.0073756147, 0.03526573, -0.006596887, -0.039739124, 0.05510785, 0.07426386, 0.022998195, -0.067732155, -0.0040514423, 0.001194678, -0.028596802, 0.0277872, -0.010751245, -0.0075471406, -0.026017053, 0.013097719, -0.016178325, 0.017454479, -0.028404694, 0.001035159, -0.005444233, -0.030490449, 0.017317258, -0.045749396, -0.00880271, 0.034360074, -0.013420189, 0.00054245075, 0.018113138, 0.011800984, -0.027306927, 0.023245193, 0.0087341005, 0.0027426996, 0.021049662, -0.0043224534, -0.024823232, -0.025358392, -0.01449051, -0.0452554, 0.037296597, 0.02837725, 0.003288152, -0.0022349828, 0.017948473, -0.030270895, -0.020981051, 0.012013676, -0.034579627, 0.02745787, 0.01038761, 0.0066311923, -0.06718327, -0.015505943, -0.014614008, 0.04591406, -0.013406467, -0.052967206, 0.037406374, -0.034387518, -0.025056507, -0.010812994, 8.597737e-05, -0.0034373796, -0.042126767, 0.02448018, 0.047725372, -0.028432136, -0.0054991213, -0.013680908, -0.018826686, -0.045859173, -0.011684346, -0.0018644868, -0.061420005, 0.060377125, 0.010346443, -0.021776931, 0.020294948, 0.016548822, 0.011389322, 0.022943309, -0.02686782, 0.04665505, -0.013461354, 0.043197088, 0.06850059, -0.037269153, 0.04056245, -0.028267473, 0.0389158, 0.03765337, -0.0017023948, -0.029090798, 0.0065282765, -0.015066837, 0.03419541, -0.05247321, 0.00021890996, 0.03644583, -0.00648368, 0.025495613, -0.027718589, -0.022463035, -0.03249387, 0.015629442, -0.035567615, -0.0027564217, -0.013612297, -0.02084383, 0.011650041, -0.0039519574, 0.0023756342, -0.00072555465, 0.036281165, 0.028926132, 0.008871321, -0.03216454, -0.03718682, -0.038998134, 0.011691207, 0.021090828, -0.0021526504, -0.03128633, -0.008878182, 0.037433818, -0.0130016655, -0.011409905, 0.052006662, 0.015519666, 0.03408563, 0.041111335, -0.015162892, 0.005138917, -0.010161196, -0.009893616, 0.0027941572, 0.058675587, -0.04273054, -0.005770132, 0.0013936481, 0.010099446, -0.027951865, 0.0126311695, 0.0017547102, -0.015972493, 0.04638061, 0.029804345, -0.030682558, -0.008919348, 0.03136866, 0.010065141, -0.0053893444, -0.022408146, 0.023643134, 0.017248647, 0.009708367, -0.02030867, 0.012363588, -0.015313835, 0.045173068, 0.040150788, 0.039876346, 0.017715197, -0.028020475, -0.029145686, -0.06273732, -0.011945065, 0.005241832, 0.020816386, -0.01726237, -0.0340033, 0.001676666, 0.030216007, -0.024137128, -0.020034228, -0.014449344, 0.042977534, 0.023698023, 0.053488646, -0.017577976, 0.0007933074, 0.005975963, -0.039135356, -0.063341096, 0.00026114823, -0.010442498, 0.010874744, 0.02534467, 0.02931035, 0.007574585, 0.021077106, -0.018291524, -0.0065797344, -0.027910698, 0.0452554, -0.027361816, 0.0060960315, 0.028596802, 0.019595122, -0.03839436, 0.0028593373, 0.038531583, -0.024068518, -0.037104487, -0.055135295, 0.01045622, 0.013118302, -0.019622566, 0.09089502, 0.0072864215, 0.012116591, -0.059114695, -0.039245132, -0.0010445929, 0.033646524, 0.030023897, -0.039272577, -0.031944986, 0.059498914, 0.028624246, -0.011814706, -0.017687753, 0.008343021, 0.0012812987, -0.03348186, -0.03718682, -0.010030836, -0.030051341, -0.016548822, 0.020788942, -0.02678549, -0.041193668, -0.009516259, -0.06399975, -0.0040274286, -0.041275997, 0.012480226, -0.04130344, -0.011629458, 0.011293267, 0.05510785, 0.010957076, 0.0018867851, -0.05027768, -0.044816293, 0.056864273, 0.06021246, -0.00509432, -0.018977629, -0.007382476, 0.052226212, -0.025193729, -0.0032435553, 0.042757984, -0.013097719, 0.009481953, 0.013344717, -0.038339473, 0.009159484, -0.03361908, -0.02091244, -0.031039331, 0.0018010222, -0.033372086, -0.031396106, -0.022188593, -0.009468231, 0.13974561, -0.028130252, -0.007053146, 0.018744353, -0.014998226, -0.029365238, -0.0012984512, -0.0277872, -0.016466489, 0.040123343, -0.031835213, 0.006909064, -0.031450994, 0.036830045, -0.026181716, -0.01733098, 0.008638046, -0.012391033, 0.025193729, 0.024891842, 0.017975917, -0.041989546, 0.011883316, -0.03479918, 0.0067409687, -0.0037529871, 0.0059931157, -0.038613915, 0.0064596664, 0.019361846, -0.00443223, -0.02845958, 0.06833593, -0.007890193, -0.028267473, 0.027567647, -0.016836984, 0.0052624154, -0.056260504, 0.011437349, 0.002281295, -0.017248647, -0.05609584, -0.0422091, -0.004487118, -0.028075363, -0.014764952, 0.028514469, 0.0011363593, -0.01971862, 0.003070314, -0.013145747, 0.024603678, 0.01568433, -0.0062092384, 0.03554017, -0.0033567622, 0.035485283, -0.008349882, -0.002373919, 0.03943724, 0.032191984, -0.007437364, -0.019101126, 0.010716939, 0.005169791, -0.015670609, 0.029584792, -0.020322392, 0.019951895, 0.021214327, 0.026771767, -0.002593472, -0.035320617, 7.6007425e-05, 0.009969086, 0.02898102, 0.068829924, 0.044486962, -0.048658475, 0.03630861, 0.010270973, 0.0056980914, 0.047011826, -0.024301793, -0.050030682, -0.00966034, 0.012246951, 0.01965001, -0.003989693, 0.017975917, -0.012692918, 0.028240029, -0.028349806, -0.024233183, -0.008425353, -0.00061148993, 0.014970783, 0.0008327584, -0.0061028926, 0.034167964, 0.015835274, -0.01992445, -0.015766663, 0.023121694, 0.0400959, 0.0038147366, 0.026428714, -0.034634516, 0.02421946, -0.020198893, 0.042456098, -0.0026380687, -0.020157726, 0.024644844, 0.003691238, -6.051649e-05, -0.02136527, 0.011451071, 0.021982763, 0.005379053, -0.034908958, 0.03499129, 0.024795787, -0.003161223, -0.0113687385, -0.033042755, -0.00087821274, -0.00833616, 0.025221173, 0.0028078794, 0.011066853, -0.0042263987, 0.024905564, -0.016713487, -0.01177354, -0.023711745, -0.011855872, 0.008967375, -0.022065096, 0.037982702, 0.015258946, 0.009914199, -0.008871321, 0.012027398, -0.020020505, 0.0038215977, 0.025029063, -0.049728796, -0.013921044, -0.02084383, -0.027402982, 0.036281165, 0.015245224, -0.045694508, 0.0041063307, -0.0184013, 0.031039331, 0.01210973, -0.009475092, -0.0117460955, 0.005451094, 0.0063876254, -0.0011560847, -0.010277834, 0.017083982, -0.028624246, 0.00443223, -0.008377326, 0.014792396, -0.05801693, -0.021529933, -0.0046140472, -0.0020017074, -0.044157635, -0.00893307, 0.02056939, -0.0017469915, -0.01997934, 0.0016157742, 0.020020505, -0.07437364, 0.027649978, -0.01217148, 0.011903899, -0.0013970786, -0.015053115, 0.03724171, -0.03062767, 0.014078848, -0.035896946, -0.029090798, -0.0065797344, 0.0028884965, -0.007231533, -0.01885413, 0.021241771, -0.018552244, -0.03625372, 0.016178325, 0.010394471, 0.037927814, 0.0039004995, -0.00042624192, -0.017660309, -0.060980897, -0.012473365, 0.040150788, 0.0024356681, -0.0019039378, 0.044486962, 0.0044116466, 0.029969009, -0.010229806, -0.0029468155, 0.00833616, 0.017591698, -0.017893584, -0.04080945, 0.008562574, -0.034744292, 0.0010617455, 0.035046175, 0.03315253, 0.0021029077, -0.0045557288, -0.0078558875, 0.020006783, 0.017811252, -0.0031543619, -0.013674047, 0.013914183, -0.0051286253, -0.0076569174, -0.012686057, -0.026908986, -0.015999937, -0.008130329, 0.0021989623, -0.008171495, -0.023711745, -0.010662052, -0.009344732, -0.017591698, -0.015821552, 0.010312138, 0.0030017036, -0.0009596876, -0.020281225, 0.015794108, 0.00529329, -0.0016852422, 0.0026106245, -0.03850414, -0.018730631, -0.012830139, 0.029584792, 0.024603678, -0.0013413327, -0.039601907, 0.046408053, 0.046792272, -0.005543718, 0.014202347, -0.019567678, -0.010277834, 0.011595152, -0.005087459, 0.0055197044, -0.029694568, -0.022874698, 0.034250297, -0.030682558, -0.004295009, 0.02229837, 0.035293173, 0.01772892, 0.0026620824, 0.017715197, 0.018044528, -0.031780325, -0.03029834, -0.002077179, 0.03685749, 0.027046207, 0.0053138733, -0.025728889, 0.024109684, 0.0047649904, 0.047313713, -0.02845958, -0.017193759, 0.014504232, -0.052088995, 0.026826655, -0.013077136, 0.027746033, 0.0108678825, -0.03545784, -0.0063293064, -0.0117460955, -0.021406434, 0.004593464, -0.008054857, -0.057413157, -0.03221943, 0.002296732, 0.030956998, -0.023999907, 0.016288102, -9.664414e-05, -0.037461262, -0.06581107, -0.009289844, 0.029118242, -0.026538491, 0.018730631, 0.034414962, 0.0056603556, 0.020363558, -0.027512759, 0.025660278, 0.02931035, -0.034963846, -0.01965001, -0.0026123398, 0.028294917, -0.019375568, 0.038476694, -0.0035643086, -0.01825036, 0.0010171487, -0.017413313, 0.004387633, 0.06399975, 0.02024006, -0.05617817, -0.0066140397, 0.0043670502, -0.005502552, -0.0014468211, -0.004915933, -0.0074716695, -0.001984555, 0.018222915, 0.017975917, 0.014367011, -0.018277803, 0.0007041139, 0.013118302, 0.045474954, -0.0015102858, 0.017715197, -0.00608574, -0.019375568, -0.027279483, 0.0546413, 8.511974e-05, 0.01707026, 0.003941666, 0.018071972, -0.013090858, 0.01805825, 0.0063876254, 0.012775251, -0.00959859, -0.007972525, 0.030023897, 0.025303503, 0.020637998, -0.0013936481, 0.024891842, -0.03230176, 0.007924498, 0.01045622, -0.0023190305, 0.0024116545, 0.009324149, -0.019883284, 0.024205739, -0.015643165, -0.019608844, 0.008541991, -0.014325845, 0.03910791, 0.014847284, -0.042373765, -0.0046209083, 0.0077323886, 0.03718682, 0.028706579, 0.02044589, 0.05922447, 0.025276061, -0.007993108, 0.03024345, -0.011828428, 0.0062092384, -0.02427435, -0.052747652, -0.035128508, 0.01932068, 0.04360875, -0.03578717, -0.0082263835, 0.020583112, -0.013783824, -0.009070291, 0.025495613, -0.026222883, 0.041522995, -0.0020565959, 0.03592439, 0.014174903, -0.01739959, -0.012727223, -0.043197088, -0.0017821543, 0.021145716, 0.019800954, 0.0024493902, -0.0023378984, 0.007231533, 0.015313835, 0.0008353313, -0.02586611, -0.023121694, 0.00018063824, -0.007602029, 0.046600163, 0.016384156, -0.009166345, 0.000625212, 0.056480058, 0.011691207, -0.016233213, -0.056480058, -0.00079287856, 0.0040342896, -0.013866155, -0.006270988, 0.014984504, -0.01672721, 0.037351485, 0.019169737, -0.04440463, 0.006092601, 0.0042435513, 0.008246967, 0.020061672, -0.04380086, -0.0033670538, 0.014600286, -0.022490479, -0.014463066, -0.012644892, -0.025015341, 0.012994804, -0.03897069, 0.025262339, -0.003512851, -0.019087404, 0.013022249, 0.031066775, -0.021077106, 0.015711775, 0.015519666, -0.011972509, -0.0030463005, -0.014353289, -0.003327603, 0.0431422, -0.01158143, 0.012926194, -0.025056507, 0.011485375, 0.031258885, -0.0023121694, -0.023272637, 0.024370404, -0.023066806, 0.009269261, -0.040836893, 0.011560847, -0.0020222906, 0.013447632, 0.021872986, 0.03320742, 0.009557424, 0.028075363, 0.00786961, -0.012672335, 0.044816293, 0.058840252, -0.016466489, -0.0011938204, 0.021118272, 0.0073550316, 0.01726237, -0.024246905, -0.004922794, 0.020061672, -0.013173191, 0.016384156, 0.009948503, 0.021200605, -0.00026114823, -0.038147364, 0.0044905487, 0.012692918, -0.0007036851, -0.015341279, 0.04253843, -0.012953638, 0.021310382, -0.01323494, -0.01481984, -0.01343391, -0.002303593, 0.0040994696, -0.017742641, 0.0074785305, -0.0034648236, -0.007492252, 0.046078723, -0.0019176599, -0.019814676, 0.023643134, -0.045145623, -0.0009288129, 0.008391048, 0.006885051, -0.006590026, 0.008946792, -0.03216454, 0.0009785554, -0.00023391849, 0.021118272, 0.029228017, -0.011430488, -0.010229806, -0.005279568, -0.04797237, -0.008590018, 0.026236605, -0.014517955, 0.018387578, -0.0112383785, -0.01713887, -0.024727177, 0.046874605, -0.006181794, 0.017523088, -0.00013861438, -0.017591698, -0.019210903, 0.0026689435, 0.0016860998, 0.01819547, 0.0391628, -0.015108003, 0.015821552, 0.015204058, 0.0056088977, 0.01885413, -0.03180777, 0.012885028, -0.0027272622, -0.010435637, -0.0033996438, 0.029694568, -9.374964e-05, 0.017879862, 0.01098452, 0.0144219, -0.047725372, -0.008514547, 0.0053070122, -3.5511228e-05, -0.0126311695, -0.02408224, 0.0026689435, 0.021667155, 0.029996453, 0.027032485, -0.0030188563, -0.026963875, 0.013063414, -0.0037392653, 0.018565966, 0.024027351, -0.010195501, -0.027361816, -0.017495645, 0.026881542, -0.012802695, 0.014463066, -0.01164318, -0.00661747, 0.011156046, 0.019059962, 0.0052486933, -0.024260627, 0.015917607, -0.006322446, -0.0034288033, -0.006782135, 0.02773231, -0.0022315523, -0.02792442, 0.028089086, -0.011087436, 0.022325814, 0.0014150889, -0.0012469934, -0.023272637, -0.0018696326, 0.015080559, 0.018222915, 0.00813719, -0.002891927, -0.011540264, -0.005982824, -0.00522811, 0.030380672, -0.01965001, 0.027938142, 0.02281981, -0.012850722, -0.018730631, -0.0091114575, 0.0011955356, -0.049866017, 0.0010454506, -0.02129666, 0.029475015, 0.0065488596, 0.027073652, 0.052720208, -0.02361569, 0.020871274, 0.01912857, -0.0075540016, -0.011451071, 0.0034476712, 0.009152623, -0.039382353, 0.014120014, -0.033042755, -0.021461323, -0.048082147, -0.016891873, -0.009632896, 0.015094281, -0.017852418, -5.697877e-05, -0.013372161, 0.00020132856, 0.00880271, 0.015821552, 0.005042862, 0.015355001, 0.09149879, 0.00588677, -0.022408146, 0.017372146, 0.0133858835, 0.0069468, -0.028349806, 0.010888466, -0.018607132, -0.009578007, -0.0073138652, 0.020102838, 0.012397894, -0.022792365, -0.0048027257, 0.01296736, -0.012651752, -0.031450994, -0.010682635, 0.005022279, 0.0030308631, -0.009234956, -0.0023567663, 0.02441157, -0.02269631, -0.013763241, -0.03180777, 0.008658629, 0.02640127, 0.032329205, -0.013159469, 0.030188562, -0.010950215, 0.030600226, 0.0076569174, 0.03254876, -0.042181656, 0.029667124, 0.0041303444, -0.016439045, 0.013674047, -0.008109746, 0.0016354996, -0.0041063307, -0.01753681, -0.027334372, -0.0063498896, -0.036034167, 0.004857614, -0.01045622, -0.008315577, 0.036555607, -0.03479918, 0.026277771, 0.040864337, -0.03559506, -0.00064536626, 0.0057255356, 0.042620763, -0.01892274, 0.012699779, 0.032246873, -0.035622504, 0.035759725, 0.008288133, -0.015300113, -0.02837725, 0.008068579, 0.0027547064, 0.034305185, 0.009214372, 0.023711745, -0.009433926, -0.043498974, 0.0015445909, 0.021927875, 0.022408146, -0.005900492, -0.03268598, 0.02845958, -0.025097674, 0.0034065049, 0.021077106, 0.04207188, -0.002156081, -0.0071697836, 0.008157773, -0.023080528, 0.0010017114, 0.042099323, -0.021612266, -0.009324149, 0.021090828, -0.010044558, -0.00061792217, 0.007910776, -0.0084802415, -0.037433818, 0.037927814, 0.016768374, 0.007890193, 0.011629458, -0.03381119, 0.031176552, 0.008061718, -0.007231533, -0.07838049, 0.010565997, 0.016123436, 0.014065126, 0.013543687, -0.021282937, 0.020926163, 0.008562574, -0.006092601, 0.016686043, -0.020610556, 0.0041852323, 0.0014022244, -0.043828305, 0.0033721996, 0.006538568, 0.019553956, 0.008061718, 0.027210873, -0.02652477, 0.01329669, 0.01224009, 0.0026535061, -0.015121725, 0.04215421, -4.9018894e-05, 0.010483664, -0.00023649137, 0.041111335, 0.009296705, 0.008212661, -0.02712854, -0.010689496, 0.025975887, 0.0400959, 0.009090874, 0.014051404, -0.008322438, 0.029282905, -0.013783824, 0.008898765, 0.004089178, -0.052253656, 0.005042862, -6.351819e-05, -0.04300498, -0.017962195, -0.019663732, 0.03356419, -0.007828443, 0.016548822, 0.024397848, 0.0035368646, -0.035677392, -0.0010634607, -0.0071766446, -0.022847254, 0.023848964, 0.005286429, -0.0010754676, 0.01627438, -0.018044528, -0.008493964, 0.0035437257, 0.009996531, 0.0009133755, 0.01573922, 0.0011475084, -0.012699779, -0.04207188, 0.012926194, 0.024740899, -0.04366364, -0.006339598, 0.019361846, -0.026895264, -0.028267473, 0.003612336, -0.0004463962, 0.023999907, 0.020171449, -0.019636288, -0.0069845356, -0.018538522, 0.0097358115, -0.019608844, -0.014874728, -0.015066837, -0.01925207, -0.0045214235, -0.04638061, -0.024192017, -0.0066003175, 0.0132555235, -0.0023207457, 0.0027306927, 0.02678549, -0.013090858, 0.019526511, 0.04182488, -0.013228079, 0.0031920974, -0.021584822, -0.031533327, 0.059169583, -0.010161196, -0.0010145758, 0.008747823, 0.049866017, -0.0036500716, 0.00912518, -0.014435622, -0.003413366, -0.004216107, 0.00039022148, 0.0040720254, 0.024260627, -0.031203996, -0.03499129, 0.0022864405, 0.028596802, -0.023670578, 0.0058318814, 0.016329268, -0.035101064, -0.040452674, -0.0398489, 0.006384195, -0.020349836, -0.0014313839, -6.828876e-05, 0.0370496, 0.018579688, 0.00992792, -0.03466196, -0.0075402795, 0.033289753, 0.045749396, -0.018277803, -0.012459643, -0.010758106, 0.005663786, -0.018552244, -0.019183459, 0.007773555, 0.029749457, -0.005663786, -0.023972463, -0.0340033, -0.004209246, -0.0007405632, 0.0048541836, -0.015409889, -0.03954702, -0.03661049, 0.012487087, 0.04703927, 0.00016144877, 0.03394841, -0.03446985, 0.015986215, 0.011986231, -0.024164572, 0.027032485, 0.01521778, -0.023711745, -0.016192047, 0.0010900473, -0.014408178, 0.030764889, 0.0033996438, -0.013577992, -0.009694645, -0.04360875, 0.017509367, -0.007924498, -0.011128602, 0.0034974136, -2.6733924e-05, 0.00119039, 0.008164634, 0.0054751076, -0.001759856, -0.0088575985, 0.020981051, -0.021420157, -0.02818514, 0.003380776, -0.01270664, 0.0054819686, -0.0067787045, 0.0030874666, -0.04599639, -0.004312162, -0.038339473, 0.034360074, 0.03427774, -0.028953576, 0.0023704884, 0.02176321, -0.00833616, 0.03737893, -0.0023773494, 0.046408053, -0.03921769, 0.012939916, 0.0048919194, -0.0020788943, 0.012493948, 0.018305246, -0.028624246, 0.03559506, -0.017660309, 0.041852325, -0.016713487, 0.0068953424, -0.0056843692, 0.028020475, 0.007238394, -0.031862658, 0.03446985, -0.011293267, 0.019677455, 0.03381119, 0.014668897, -0.014655175, 0.0010068571, 0.011526542, 0.040919226, -0.0059519494, -0.01713887, 0.013900461, 0.023190305, -0.049399465, -0.027746033, 0.034414962, -0.006922786, 0.009612313, 0.019910729, 0.0038353198, -0.0068507455, -0.010970798, -0.0022195454, -0.030655114, -0.014367011, 0.01977351, 0.047999814, -0.033234864, -0.016987927, 0.010065141, 0.026854098, -0.004524854, 0.0053035817, -0.029529903, -0.013440771, -0.017893584, 0.030133674, -0.025427002, -0.002044589, -0.0093584545, -0.014174903, -0.008775266, 0.00800683, -0.04399297, -0.02917313, 0.02614055, 0.028130252, -0.005516274, 0.0025454448, -0.03672027, -0.05724849, -0.01561572, -0.007238394, -0.05458641, 0.008466519, -0.001535157, -0.030051341, 0.0050394316, 0.017207481, 0.026812933, 0.010764967, -0.011306989, -0.004133775 ] } ], "model": "text-embedding-3-small", "usage": { "prompt_tokens": 6, "total_tokens": 6 } } headers: CF-RAY: - 939c181e8dcae38c-NRT Connection: - keep-alive Content-Type: - application/json Date: - Sat, 03 May 2025 01:48:07 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-allow-origin: - '*' access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '33302' openai-model: - text-embedding-3-small openai-organization: test_openai_org_id openai-processing-ms: - '70' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload via: - envoy-router-8985dd5b9-nwnh2 x-envoy-upstream-service-time: - '38' x-ratelimit-limit-requests: - '5000' x-ratelimit-limit-tokens: - '5000000' x-ratelimit-remaining-requests: - '4999' x-ratelimit-remaining-tokens: - '4999993' x-ratelimit-reset-requests: - 12ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_9d3ac02ffd35831e5cb85c4a96414d5c status: code: 200 message: OK version: 1 test_embeddings_with_dimensions.yaml000066400000000000000000000423421527070072300464210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "This is a test for embeddings with dimensions", "model": "text-embedding-3-small", "dimensions": 512 } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '112' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - Linux x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.9.2 method: POST uri: https://api.openai.com/v1/embeddings response: body: string: |- { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": [ 0.009219426, -0.06619997, 0.10735908, -0.06785497, -0.023044067, -0.05842867, 0.028836563, 0.020453632, -0.026479987, 0.045620415, 0.019949937, -0.04285009, -0.052600194, -0.03561846, -0.02048961, 0.0022486404, -0.011153256, 0.00040644174, 0.026695859, 0.055478454, 0.0013019628, -0.06281801, 0.03466504, 0.025778413, 0.00886414, 0.008463882, 0.029628085, 0.03363966, 0.12095886, 0.00993899, -0.0019608145, -0.03283015, -0.056701712, -0.07433105, -0.023026077, 0.00888213, -0.05925617, 0.097932786, -0.06360954, 0.041303024, 0.006008367, 0.009336354, -0.07670562, 0.018088063, -0.027613303, 0.07238823, -0.1020343, -0.02194673, -0.046447914, 0.080015615, -0.090953, 0.03831683, -0.037129547, 0.023673685, -0.043173894, 0.045188677, -0.0196801, -0.021353088, -0.011863826, 0.005823978, 0.03243439, -0.027631292, -0.033981454, -0.03624808, 0.02194673, -7.420513e-05, -0.046591826, 0.05310389, 0.0037979535, -0.030509552, 0.046411935, 0.016496025, -0.040511504, 0.02378162, 0.06983377, 0.03954009, 0.022270534, 0.0070742224, -0.057457257, -0.03871259, -0.03871259, 0.07908018, -0.031444985, 0.0007465486, -0.03590629, -0.010541625, -0.06299791, -0.018960536, -0.036949657, -0.06933008, -0.03795705, -0.077641055, -0.07605801, -0.01731453, -0.038208894, 0.012340538, 0.04835476, -0.07972779, -0.05968791, 0.022702273, -0.0067009483, 0.03849672, -0.018088063, 0.027289499, -0.048246827, -0.04572835, 0.031660855, 0.0027051142, -0.06468888, 0.018285943, -0.04245433, -0.028350856, 0.028656673, 0.04198661, 0.077209316, -0.0017258314, -0.036733788, -0.046016175, -0.015119857, -0.026174173, -0.04839074, -0.030563518, 0.0676391, -0.046879653, -0.03914433, -0.043713566, -0.018672708, -0.061450843, -0.0623503, 0.022108631, 0.009363338, 0.015344721, 0.054902803, 0.07483475, -0.086131915, -0.03669781, -0.09750105, 0.047023565, -0.021658903, 0.040151723, -0.0035663436, 0.014508227, 0.021694882, -0.028045041, -0.015209803, 0.045368567, -0.071164966, 0.095702134, 0.059364103, -0.0047536255, -0.020939339, -0.03813694, -0.028152976, -0.02829689, -0.07886431, -0.052384324, 0.011764886, 0.025454609, 0.06130693, -0.02826091, -0.038640637, -0.00405205, 0.031444985, 0.00036512298, -0.0018360148, 0.013671733, -0.007402524, -0.03914433, -0.054435086, -0.09325561, 0.016361106, -0.039468136, -0.015398689, -0.03583433, -0.018456839, -0.009408311, -0.013590782, -0.08239018, 0.017062683, -0.073755406, -0.051592804, 0.040259656, -0.030707432, -0.0026084227, -0.08332562, 0.09347148, 0.01772828, 0.022090642, 0.03957607, 0.023547761, -0.025724445, 0.04781509, 0.063501604, 0.031301074, 0.0039666016, -0.03095928, 0.062710084, -0.006395133, 0.036176126, -0.031319063, -0.019949937, 0.061198995, -0.042490307, 0.031301074, 0.007807279, -0.05000976, -0.013275973, -0.042958025, -0.052096497, -0.02072347, 0.03094129, -0.031624876, 0.037849113, -0.013222005, -0.057241388, 0.051772695, -0.04982987, -0.043713566, 0.011216218, -0.024501184, -0.045620415, 0.061163016, 0.07102106, 0.015830427, -0.11405104, -0.012394506, 0.0016268913, -0.05691758, 0.05058541, 0.026911728, -0.005765514, -0.02624613, -0.035150744, 0.016244177, 0.027199553, -0.030095803, 0.034593083, -0.0066739647, -0.06609204, -0.0045647398, -0.039504115, -0.011656951, 0.05371552, 0.029196346, 0.0097501045, 0.0029704538, 0.03524069, -0.049218237, -0.003806948, -0.011522033, -0.014463254, 0.017134639, -0.029592106, -0.046088133, -0.0350608, 0.022648305, -0.06371747, 0.06569628, 0.019536188, 0.025922326, 0.007501464, 0.01045168, -0.0573853, 0.017053688, 0.020273741, -0.03220053, 0.031534933, 0.016837819, 0.0138336355, -0.09181648, 0.0041712276, -0.021461023, 0.0024285316, -0.028656673, -0.06371747, 0.0537515, -0.042886067, -0.043029983, 0.019338306, -0.025274716, 0.022054665, -0.034359224, 0.019158415, 0.10095496, -0.04759922, -0.0014987187, 0.00030693942, -0.022612328, -0.045404546, -0.035798352, -0.0023903046, -0.068394646, 0.048966393, 0.02790113, -0.051161066, -0.0026713847, 0.008405417, 0.015425673, 0.042130526, -0.06889834, 0.05101715, 0.00563959, 0.05346367, 0.076561704, -0.05555041, 0.08023149, -0.033891506, 0.08447692, 0.065408446, -0.023943523, -0.021245154, -0.0110813, -0.037669223, 0.07476279, -0.09174453, -0.013455864, 0.055190627, -0.028764607, 0.0074474965, -0.01628915, -0.034719005, -0.07195649, 0.015146841, -0.07785692, 0.013590782, -0.027163574, -0.027163574, 0.034934875, -0.029124388, -0.029088411, -0.023313902, 0.05080128, 0.022450425, 0.023205968, -0.0357264, -0.0573853, -0.055118673, 0.0025994282, 0.0075284475, 0.011504044, -0.07015758, 0.011620973, 0.02192874, 0.015470645, -0.022702273, 0.032506343, -0.007496967, 0.024663087, 0.10275387, -0.040187698, 0.015020917, -0.024699066, -0.017422466, 0.006525554, 0.062782034, -0.055622365, -0.018097058, 0.005509169, -0.013473853, -0.048894435, 0.002963708, -0.022648305, -0.024681076, 0.053247802, 0.012160647, -0.052420303, -0.0006239977, 0.048678566, 0.03383754, 0.0033797063, -0.013590782, 0.011513039, -0.021712871, 0.01632513, -0.051340956, -0.00074542424, -0.031696834, 0.08181453, 0.054794867, 0.040763352, 0.006970785, -0.008675254, -0.04857063, -0.059543997, -0.026012272, -0.011755891, 0.06191856, 0.005770011, -0.018564774, -0.0004266795, 0.015785456, -0.01496695, 0.0067549157, 0.0075104586, 0.045764327, -0.009291382, 0.056881607, -0.03648194, 0.004758123, 0.0017753015, -0.074187145, -0.11354734, 0.02703765, -0.043569654, 0.007501464, 0.031067215, 0.07505062, 0.03667982, 0.03831683, -0.00451527, -0.042886067, -0.021425044, 0.029088411, -0.045980196, -0.029789986, 0.05000976, 0.006633489, -0.049434107, 0.019014502, 0.12124669, -0.06472486, 0.01423839, -0.07274801, 0.016972737, 0.0042544273, -0.036553897, 0.11405104, 0.022936132, -0.008257007, -0.03358569, -0.05983182, 0.020741459, 0.080015615, 0.033999443, -0.06947399, -0.0598678, 0.060803235, 0.031409007, -0.014796053, -0.02705564, 0.007483475, 0.0360502, -0.054219216, -0.06324975, -0.004740134, -0.0598678, -0.0031683343, 0.045872264, -0.023817599, -0.048714545, 0.011683935, -0.092320174, -0.03565444, -0.073431596, 0.003550603, -0.051916607, -0.015884396, 0.020633524, 0.094263, -0.01946423, 0.040871285, -0.07220834, -0.064509, 0.06594812, 0.0931117, -0.0027613302, 0.0025184772, -0.034503136, 0.04770715, -0.019410264, -0.025544554, 0.05058541, 0.039863896, 0.01147706, 0.024789011, -0.042130526, -0.017008714, -0.051880628, -0.027991075, -0.05367954, 0.013482847, -0.03910835, -0.034161344, -0.021802817, 0.017089667, 0.18838209, -0.040691394, -0.026983684, 0.06587616, -0.036985636, -0.06623595, 0.0040992713, -0.07433105, -0.018375888, 0.025274716, -0.018960536, 0.021604937, -0.039252266, 0.06296193, -0.020075861, 0.039216287, 0.03563645, 0.01627116, 0.024483196, 0.044361178, -0.009264398, -0.057241388, -0.0061297934, -0.0537515, 0.03910835, 0.0044208267, 0.04123107 ] } ], "model": "text-embedding-3-small", "usage": { "prompt_tokens": 8, "total_tokens": 8 } } headers: CF-RAY: - 939c182bfc0e6d5f-NRT Connection: - keep-alive Content-Type: - application/json Date: - Sat, 03 May 2025 01:48:09 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-allow-origin: - '*' access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '11121' openai-model: - text-embedding-3-small openai-organization: test_openai_org_id openai-processing-ms: - '90' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload via: - envoy-router-687976cc6-wtrv5 x-envoy-upstream-service-time: - '43' x-ratelimit-limit-requests: - '5000' x-ratelimit-limit-tokens: - '5000000' x-ratelimit-remaining-requests: - '4999' x-ratelimit-remaining-tokens: - '4999988' x-ratelimit-reset-requests: - 12ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_a3d50a6faea5e691c607f0a4902c56ab status: code: 200 message: OK version: 1 test_embeddings_with_encoding_format.yaml000066400000000000000000000251271527070072300474110ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "This is a test for embeddings with encoding format", "model": "text-embedding-3-small", "encoding_format": "base64" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '127' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.26.0 x-stainless-arch: - arm64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - Linux x-stainless-package-version: - 1.26.0 x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.9.2 method: POST uri: https://api.openai.com/v1/embeddings response: body: string: |- { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": "DK6fO8qEirxr7OE8SQSPvXpWIry659e8mJ5BPUKZiTxZdqM76WvmPDaVLT0xwge86TEXvU+lObzFhka8XkUfvNgvd7yb1d08OC0NuzMvyTshVB07MPgsvc7umDzYWhU9SjeBPBQdz7wB2Yu8gYuCPbZ9ST1078a8c4rZvDwyrrxnsRs8BRj8vNv2nrz3bA88HFZZvMXnCT2xSeC8Pp/vOuVimzyN/1K8kc5OvXAk9bvH6AA8UgsePWdMrrxvgQ69+Z8BvEE0HD2w4Ei9FbEEPUY9Zzyd0qq7ZbROvA6Azjx0Ho+8vbKpPITGyLsC3bW7mAMvPTXHKL3QIYu6mNRmvNYnozx078a6l295vQyuHz1frra6bYhrvP/bPj1O17Q8KCSQvJKcUzzXjBA9m9XduwBFVrwJSDs7RTk9vSS6gb2m2Tm8UXfoPL+6/bxyggW9mnDwOxBS/Tx6u4+94JMfvIIqv7xvgQ69/d7xvH1XGb08lxu9H7y9vBvtwTtOdnG8G1IvvJU4Xb06ZCm7+NUmPJg5VLzP8kI8y4i0u35fbbta2xC6arKSvGUVkjwU56m7Y0eNvT7ON7xc3Ae9C39Xu5drT7w4yJ88kmKEPVtEKLwLf1c86ZYEvdP3Y7ujDug7jf/SvHuJFLxWED89LyqovB2FIb1i6fy8IbkKulupFb0O5bu8UXdovOqaLrzeXIO8XNwHPRAYLj36o6u9gYsCvQjfo72zRi083S07vbmCaj1KbaY6oWsBvfaeCjz9Q1+8PgTdvKtHcjwVUMG9qqQLPQhEkTwW7308dLkhvAJ88jtoUFi9v+UbPGIUG73BHDi9H/LivEI4xrypEFY9MWHEu2N9MjmyQgM9NQF4PDsuhDrC6jy7kwFBvBfoID0tLds6o9QYvRVMF7x3VSu8PcqNvPM4przCu/S5kmIEvfVvwjqrqLW892yPvZJiBDvwAQq9mDnUvBAYLj3/2z68HBwKPCMmzLyLMU49mNRmPCGKwjycaRM9wuo8uwJCo7ypENY8r9yePQbiVjvgyUQ8cOolvXIhQj0B2Qu9CRIWPbkd/bzvCGe8tBAIPYTGyLxgsuA8Bd6sPFrbEL1BNBy9RzYKvdPBvrxkSze8e172PKPUmLzAGA48OMgfO3ZRgTv3ByI9fiUevYWQI7xS3FU8izHOvIbDlbyxSeA8nAhQPZg51DyuDpq9KvIUvAt/17zHuTi93sx3PUc2iryjOQa77GzdvNHvjztJo0s8HByKPE3TCrzGVEs7xFf+uyW+q7zT9+M8c+scvVDUAbqHYlI9nAhQu2AXTrx+X+08aRqzO/k+Pr31CtU8dO9GOuEy3DyS/ZY8W6kVvP48Ar1MP9W8c1AKve5pqrymqnE9YnmIPO02uDc6ms66l9C8PAKnkLya0bO6ptWPPFUMlbxMBYY8z1ewO0ptJrqD+EO9c8B+O0nZ8LzellI8L4+VPFWnJ70zL0k9oaGmvCpXgrz0axi7KvKUPK6pLL0xwoe9DuW7O43JLT3C5pK89j3HN052cbxbDgO8saojvfY5HTuoRnu7UXdovQJCIz3uaao8JLoBvbkdfTwTtDc80/fjO7GqIz3rZIm8XHcaPUQ1kzvLUg89FB1PPSj1R71rh/Q8ljGAvSW+Kz1XeVY9GVXiujsDZryx5HI8n2oKu51x5zw4Z1y9MPgsvDD4rDyrR3K8njtCPNBb2rpu7Vi8AnzyOeH8Nj1SQUO9kv2WPH28Bj0Ae/u8XqqMPPrdertGA5g8cyXsvPXULz20Slc9RTm9u6Q9sLwyKx+8RDUTvQwTjTyvd7E7qj+evOkG+bwQs8C8ogo+PWvs4TzIhz29TnJHPeaVjTvb9h49rq3WPBVMl7x7iZS7OfsRvGrot7xPQMw8/QmQPW+BDr0PFIQ8sniovEmjy7yqpAu9OAJvPKaq8bu24ja8Q6HdPIstpDx2wfW8DK4fvf1DXzw3/sQ8tXkfvI4yxbzChc872L+CvJ0MejsNfKS8eIgdOkOdMzmOkwg9IbkKPbewOz1WEL88M5Q2vYbDFb371h29BagHPA4b4Twbt5w8AQ8xPKcIgrxWdSw5h5h3PIpjSbq85KS86ALPvEai1DwvYE09SaPLPIJZB72xD5G6ua2IvGJOarxREnu9jpOIPN3IzbyhBhS7Qc8uPWVPYT2V/g086WvmPBGBRbxmfqm8Hx2BvO1lAD3S87m862SJPAp7LTveMWW56WvmvJloHDyuSOk8V3lWvFupFb3q/5u9WrDyO7lIm7x6VqK8CBlzPQsaarzP8kI7gYuCvRkbE71+igu8aOtqu1cU6TsVTJe8EeYyvfOdEz1iTuo8OMgfvDyXm7vTIgI8wksAPcK79LyhPDm9nXFnvJPLG72SnNO7xecJPQB7+zr3QfG8o3NVvKmraL0A4Oi8fI0+vYKPrDzwAQq9ecJsvLmCajy3FSk9KvKUvC33tTuhBpS91CYsvY4yxTzFvGs9e4mUvOJhJL02lS28EFL9PIHBJ7xhSsC8V3lWPSSPY7uP/B88b4GOPDP5o7yyQoM8oDgPvJdrz7zT9+O87mmqu+T5A73tZQC9gfv2uuZq77xzUAo+VnWsvE48Ir0yKx89HriTu6M5Br3vMwW8H1dQvWCyYLwVTBc8x1j1vJPLm7yBi4K8ZuMWPZ0M+ryMmmW8+zuLPFfewzxOoY8842n4PFQ+kDxAZhe9MV2avAcVybyY1OY7jJrlPIdi0rwB2Qu9iv5bPD83zzt/xNo7yoQKvSosZD1g4Si9DhvhvMa1jjy5fsC8zb9QvFZ1rLxFOb07ExmlPNVZnrzA6cW9CH7gvJeal7wYUbi8jf9SvGgWCT3YlGQ7aIZ9u6zbJ7zZ+dG7wYGlPDJlbjyOMkW8ukzFPO8IZ7wCfPI8MC5SvCAhq7t1vcs84saRPB3qjrnWwrW8CnutPPY5nTpjfbK8elaiPPgLTL3QvJ08FB3PPK2lAj1GPWe8O8kWvePKOztiTmo8c1CKPGKzVz3dkig9wOlFvTpkKT2mD986TAkwvIrINj0W7/28eyQnvYT87brnmTc8mnBwO4GLAr371h08KixkvKVwojyMmuW8A+HfO9sw7rtBNBw8xFd+PDb6GjxREvu86WvmOwbi1jw+BF28tn1JvHBPkzlOcsc80/djPNSLmbtmGTy9brezOw+vFjw+BN08CneDvNhaFb3mlQ09NGK7PDf+xDyKme68HFbZPLUUMjw3YzI9mDnUvC33NT2oRvs8O574uiGO7Lsjhw+8723UvPaeirsCeMg801zRuxyM/jvdkig7EkugPBbv/bugct47D6+WvGJO6rsGER88C39XO/pC6Dybnzg7VUK6O/M4pjwlIxk8CH5gvCGKwjsKdwM9SQQPvf1D37yH/eS7toHzvJ4FHbysrN88KcNMvSMmzLv3QfG42fnRPLQQiDxInyG7+AvMvBKwjTtyIUK8pabHu+DJRLyCj6w88qTwvOT5AzyF9ZC8bekuPHZRAb16u4+8wksAvKBy3rjHgxO9TAWGvKOp+jmuSGk6Ov+7vO02uDu5SJs8l5qXvZxpEzzqNcG8CnstPKM5hrwMSTK7zb9QPKHXS71GotQ8jJY7vexs3bxDZ447wksAvJX+Dby5HX28JPAmPEZohbwX6KC8RZqAPKaq8Tv/QCw9nZyFvC/FOruLLaS70e8Pvd3IzbwI36M8+t36uzZfiDzEHS89RAbLPPMCATymD187ak0lOyYnQzt3HwY9gSYVu64OGr3ezPe5PQAzvdkoGrzezPc8YrPXPH6KC7yWMYA7850TvP+lmTySYoQ8ZRWSPOtos7zn/qQ8Rj1nu7mtCLxm4xY8fI2+vFGiBrzO7pi776N5OmgWibyV02+8BRh8unwoUTv3ojS9st2VvFILHjydnIU8lzWqu1faGby651c8Oy4EPNJYJ7wO5Tu76wPGvC8qKLylcKK6o9hCO/nZUDwRHFg83vcVvaejFD0GER89JIs5vHmMRzwXg7O8gWBkvIBYEDzkM1O8tErXu4JZh7y3sLu8PzMlPePKO7y85KQ6H431PBNT9DuEJww9KFq1OfM8UDxfrja7c4rZO0TQpbwFfem7H/JiPKYP3zxxU707qEb7u1d51rt8KFE8FlRrPaBy3rzvo/m8gFiQPLewO73k+QM9K1ssPFytPzxQ1AE8wrv0vNLzObyRzk48NstSvBdNjjyUNDO8vB50vc2FAb01x6g79wciPe0Ak7vVWR48CnstvCos5LwJrai99weivIuSkTxcTPy80YoiPMlRmDy0gPy7Ca0oPNVZnrxasPI89NCFO1JBw7w7A+a4ntbUvHhdfzzRiqK8d1UrPbdLzrxUE/K8rq1WPMGBJbyNLhs85ceIPSfxnTye1lS9axuquVES+7uuc4e8QTQcvGNHDTuzq5q8F02OvCG5irwD4V88hl6oPOH8trycaRO8kv2WO4L0GT1/xFq6QwKhPGJO6rwJ4828GfB0vFN0NT01LJa7c8D+O4zFA7wCp5A83CkRvW2IazrkM1M5o6n6uI1kwLy5gmo83S07Pdn5UTy5Hf27WRE2vGsXAD3kmMC88qTwPA9KqTtUPpC7MmVuPA6287sl9FC8/UPfPBO0t7x+igu9mnBwPKULNbvuaSo9q0fyO2fnQL2ZzYm8p6MUPWnkDT3Zw6w6Z+dAPbBFNj0ovyI9rHa6uhYanDz3pl68x4OTO5X+jbxJo0u9v0oJvdkomrtAO3k8MWHEvIQnjLwuXKM8H411uBmAgLxOdnE8njtCveGXybo1kYO8yh8dPSOHjzz93nG8OGfcvKgMLL1n50C8RmgFPC2SyDzellK6dFS0OhJPyjsZVWI8MWHEO5pwcLxmfim9/QkQvEo3AbzEghw9G+1BPHcfBrtDPHC8BNqCPdbCtTxREvu88aBGvXAkdTqYA687v+UbOuGXyby8SZI8URJ7vAEPMT3szSC89p6KvQPh3zyWZ6U8uHqWu9z6SDy05Wm9IvPZu/UKVTzdyM28pnTMvHMl7LvH6IC8zfV1PLOrGr2/H+s8sniou5X+Db25fkC8dYemPGIUm7wTtDe8LfOLPAkSlrzeXIO7v0oJPOtoszwtksg80r0UOgp7LTyabMa87NFKvAsa6jxBoGa8IIaYvB8dAT1aFeC8tID8uxhROL2pq2g8wuq8O1gNDLtOdvE8jPuoPBSCvDuSYoQ8elaiPF9N8zrAsyA9qavoPEbYebqcCFA825XbORu3HD2eoC87xYZGvYb5Oru7Gso8J5DavG5SxjyBJpU78m5LPIn6sTuBxdG8bIAXu/emXjya0TM7oaGmvPemXj3S87k7GbrPO95cg7wtksi8tRQyu+zNoDzTIoI8Ov+7vInEjDxnTC482cMsPD6f7zwghpg7v0oJvf3ecTyPNm+9QaBmvEBmlzwB2Ys8WA2MuhtSrzyxSeC8KPXHu1Z1LDykoh08LyooPTJlbrzxzw47nQx6O/vWHb1I1UY7MsaxPEo3Ab1LOys8PcoNPGJO6rzcxCO9VD4QPQtFiLxX3kM87NFKOwh+YDySN+a8A+FfuiqRUbz52VC7KFo1PfRrGDz1b8I8+m0GPGuH9LvgLrK7LyoovSfGfzzGtQ48IIaYvGYZPLy6sTI9rkhpO0w/1TplsKQ8kpzTPAbiVr0Ae3s7aBYJPEo3gbzZ+dG8VnGCvE48IjxeSck88zxQPXOKWTuD+EO71vhau0KZCTuuSGm8a+xhPDqaTj0ZVWI7txWpvNb4Wrx5J9o8WrDyulfewzxjRw297ZulvH+ONTypq2g8QGYXO/pthryvQYy7fMNjvJrRs7xPpTm6WtsQPci94ruE/O28xeuzPKYP3ztiTuo8Qc8uvCS6gTwLGmq8cIU4vBVMlzzoY5I8Pp/vvOH4DL2AWBC8PzOluTWRAzzvo/k8ybYFvKM5Bj3eMeU76v8bOyS6gbxxUz27786Xu6mr6LwE2gK8SzsrvD2bxTzZw6w6nGmTPFLcVT0TU3S8EbfqPIRh2zxmGTw88aBGvJ9qCrsyKx+8j2ENvbDgSDqbOku8IiIivMKFT722gXO8bSP+u/ShvTypdcO8Sm0mO4X1kLyv3B48l295O/TQBT0vKig8YxjFOzbLUj0TfpK8HSA0vKgMLDzlxwg8c4pZu70Xl7zraLM8ptk5vbQQiLyxD5G5hZAjPd4xZbvellK8kpzTPHdVqzw2X4i8rECVvFtEKDxwJHU6zSCUO3G4KrvrZIm6bYhrPD83T7xVQrq7eL7CvEABqjxfE6Q8EFL9PNz6yLzblds8ntZUOQtFCDxUPpA83jFlPP7XFL34OhQ9OfsRu4qZbrytpYK8RZqAvFeve7sST0q8ST7evGexG7yacHC7F4Mzvd0tuzxkSzc8+qfVOgu1/DzI7Cq9CH7gO1gNDD3qmi69723Uuo4yRTyxqiM9qRBWvNP34zwZgAA9mWicvB/y4jyHYlK81pPturJCg7veMWU8wBiOPDPKWz36p9U8UXfoPKbVj7ydcee8iv7bN80glDyeBZ08lgI4vMAYDr37Ows9BhGfvGqDSjpqTaU72JRkPZnNCbxi6fw7SzsrPNEltbxmfqk86pouPaQHC7uY/4S8X01zO8jsqjrvbdS7+HA5vB0gtDuAXLq8ciFCPTRekTtg4ag8LyooPGGrA72VOF09imPJO6DTIbwhVJ29hGFbO4HF0Tv1CtU8CxrqO4X1ELxs5YQ8cLSAunwo0bw0XhE9+DqUvGCy4Drb9p485JjAvDpkqbsvYE08TW4dPWKvrbwHerY88QW0vBm2Jby1FDI7/jwCO9BbWrxV3Uw9XbFpvI3/Uru35uA7cbgqPRvtQTr2PUc8CRIWvRxW2Tsqx/Y8nQz6OwYRnzxk5sk8ISl/POEyXD0PSqm63yqIu/RrmLjTXFG9h2JSO3UiObyhawG9hfWQvJLS+DxJPt48v0qJPFF3aDzqNUE8jcmtPFtEqLzWXci7Y+Ifu9wpkbzun888MPisux5TJjwCfPI8gcGnvJbMEjuhBpQ8P5iSPEBml7sZ8PQ8e7+5PIz7qLwBdB69ee0KPOlr5jzOJD69oHLeuydWizxaek28VHjfvJc1qjuuqay79aXnPGtRzzzVvou8t7C7OrnjLTlLcVA8blLGu5EzvLwARda7blLGvKIKvrt9W0O9THV6vH76f7zyCV48yL3iu2kas7ttiOs8aLVFu1FzPj3raLM8coKFvJGYqTuM+6i8dx+GOgh+YD296M68ST5eOgWoB7sSSyA9mAMvu52cBTwIRJG8oaEmvMjsqryvEsQ738WaPGpNpTxygoW8dLmhvHiIHTzQkX88iV+fvP3e8Tq7ew08nZyFvCcrbb3YvwK97TY4PIb5ujvQW1q83sz3u+/OFz1dsWk89aVnvIGLAr3sMg68fMPjPIFgZDzwAQq7WaxIuoGLgrkBdB48xlChvOH4jLyBJhU8pqrxPIfHv7v63fq8KSg6vdtbjLsahKq8C7X8OjD4rLx3Vau8axcAvUGgZruMxQM976N5uzP5Izx6uw+9qRBWu3QeDzyXNSq81PAGPW1OHDx/xNq8HeoOvW1OnLzrZIm8kpzTPCpXgjwhVJ28NZGDO3bwPb2s2ye7Hu44vBZU67yz4T+7hyiDPGLp/LuYOdQ7paZHPF17RLyMmmW8Hx0BO3nCbLzmMCC9w0+qvIBcurxpf6A8e+6BvHRUtDwG4ta8fVtDvPY9x7zn/iQ9U3ALPYD3TDvTknY6F02OuiIiorxbDgM9HPFrPP1D3zwn8R296jVBPKaq8TuurVY7KPXHPLOrmjx1vUu8eYxHPH/EWjwwLtI8kC8SvEx1ejw0XhE8yh+dujto0zwR4gi94JMfPT7ON7ymqvE8XbHpPE528Ty546282JRkPByMfjw0/U09xFd+vJrRM7tFOb080CGLPKl1Q70kKva86GOSPOc0SjulcCK8RgMYPDvJFrrkzuW8Ps63vCgkkDwWGhy9mJ5BO1qwcjwvZPc8DRe3vIz7qLyKme48/g26PBjsSjq5rQg82PWnuofHP7ww9AK6EFJ9PPg6lLyaNiG8uR39vBlV4rwEdZW7ogq+PCqR0bw2lS2962gzPZ2cBT2abMa8brezNy3zi7xfeJG9J/EdvBeDMzuAXDq9/d5xPN9grTshVB29ylnsui0tW7o1kYM81fQwPMDpxby54y08" } ], "model": "text-embedding-3-small", "usage": { "prompt_tokens": 9, "total_tokens": 9 } } headers: CF-RAY: - 939c1836de294a26-NRT Connection: - keep-alive Content-Type: - application/json Date: - Sat, 03 May 2025 01:48:10 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-allow-origin: - '*' access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '8414' openai-model: - text-embedding-3-small openai-organization: test_openai_org_id openai-processing-ms: - '82' openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload via: - envoy-router-698696864c-zvnz4 x-envoy-upstream-service-time: - '52' x-ratelimit-limit-requests: - '5000' x-ratelimit-limit-tokens: - '5000000' x-ratelimit-remaining-requests: - '4999' x-ratelimit-remaining-tokens: - '4999987' x-ratelimit-reset-requests: - 12ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_f9b3fad2c1ee8d52c777edfa84b4cfff status: code: 200 message: OK version: 1 test_embeddings_with_not_given_values.yaml000066400000000000000000000254271527070072300476250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "This is a test for embeddings with encoding format", "model": "text-embedding-3-small", "encoding_format": "base64" } headers: accept: - application/json accept-encoding: - gzip, deflate authorization: - Bearer test_openai_api_key connection: - keep-alive content-length: - '127' content-type: - application/json host: - api.openai.com user-agent: - OpenAI/Python 1.109.1 x-stainless-arch: - x64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - Linux x-stainless-package-version: - 1.109.1 x-stainless-read-timeout: - '600' x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - 3.10.12 method: POST uri: https://api.openai.com/v1/embeddings response: body: string: |- { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": "DK6fO8qEirxr7OE8SQSPvXpWIry659e8mJ5BPUKZiTxZdqM76WvmPDaVLT0xwge86TEXvU+lObzFhka8XkUfvNgvd7yb1d08OC0NuzMvyTshVB07MPgsvc7umDzYWhU9SjeBPBQdz7wB2Yu8gYuCPbZ9ST1078a8c4rZvDwyrrxnsRs8BRj8vNv2nrz3bA88HFZZvMXnCT2xSeC8Pp/vOuVimzyN/1K8kc5OvXAk9bvH6AA8UgsePWdMrrxvgQ69+Z8BvEE0HD2w4Ei9FbEEPUY9Zzyd0qq7ZbROvA6Azjx0Ho+8vbKpPITGyLsC3bW7mAMvPTXHKL3QIYu6mNRmvNYnozx078a6l295vQyuHz1frra6bYhrvP/bPj1O17Q8KCSQvJKcUzzXjBA9m9XduwBFVrwJSDs7RTk9vSS6gb2m2Tm8UXfoPL+6/bxyggW9mnDwOxBS/Tx6u4+94JMfvIIqv7xvgQ69/d7xvH1XGb08lxu9H7y9vBvtwTtOdnG8G1IvvJU4Xb06ZCm7+NUmPJg5VLzP8kI8y4i0u35fbbta2xC6arKSvGUVkjwU56m7Y0eNvT7ON7xc3Ae9C39Xu5drT7w4yJ88kmKEPVtEKLwLf1c86ZYEvdP3Y7ujDug7jf/SvHuJFLxWED89LyqovB2FIb1i6fy8IbkKulupFb0O5bu8UXdovOqaLrzeXIO8XNwHPRAYLj36o6u9gYsCvQjfo72zRi083S07vbmCaj1KbaY6oWsBvfaeCjz9Q1+8PgTdvKtHcjwVUMG9qqQLPQhEkTwW7308dLkhvAJ88jtoUFi9v+UbPGIUG73BHDi9H/LivEI4xrypEFY9MWHEu2N9MjmyQgM9NQF4PDsuhDrC6jy7kwFBvBfoID0tLds6o9QYvRVMF7x3VSu8PcqNvPM4przCu/S5kmIEvfVvwjqrqLW892yPvZJiBDvwAQq9mDnUvBAYLj3/2z68HBwKPCMmzLyLMU49mNRmPCGKwjycaRM9wuo8uwJCo7ypENY8r9yePQbiVjvgyUQ8cOolvXIhQj0B2Qu9CRIWPbkd/bzvCGe8tBAIPYTGyLxgsuA8Bd6sPFrbEL1BNBy9RzYKvdPBvrxkSze8e172PKPUmLzAGA48OMgfO3ZRgTv3ByI9fiUevYWQI7xS3FU8izHOvIbDlbyxSeA8nAhQPZg51DyuDpq9KvIUvAt/17zHuTi93sx3PUc2iryjOQa77GzdvNHvjztJo0s8HByKPE3TCrzGVEs7xFf+uyW+q7zT9+M8c+scvVDUAbqHYlI9nAhQu2AXTrx+X+08aRqzO/k+Pr31CtU8dO9GOuEy3DyS/ZY8W6kVvP48Ar1MP9W8c1AKve5pqrymqnE9YnmIPO02uDc6ms66l9C8PAKnkLya0bO6ptWPPFUMlbxMBYY8z1ewO0ptJrqD+EO9c8B+O0nZ8LzellI8L4+VPFWnJ70zL0k9oaGmvCpXgrz0axi7KvKUPK6pLL0xwoe9DuW7O43JLT3C5pK89j3HN052cbxbDgO8saojvfY5HTuoRnu7UXdovQJCIz3uaao8JLoBvbkdfTwTtDc80/fjO7GqIz3rZIm8XHcaPUQ1kzvLUg89FB1PPSj1R71rh/Q8ljGAvSW+Kz1XeVY9GVXiujsDZryx5HI8n2oKu51x5zw4Z1y9MPgsvDD4rDyrR3K8njtCPNBb2rpu7Vi8AnzyOeH8Nj1SQUO9kv2WPH28Bj0Ae/u8XqqMPPrdertGA5g8cyXsvPXULz20Slc9RTm9u6Q9sLwyKx+8RDUTvQwTjTyvd7E7qj+evOkG+bwQs8C8ogo+PWvs4TzIhz29TnJHPeaVjTvb9h49rq3WPBVMl7x7iZS7OfsRvGrot7xPQMw8/QmQPW+BDr0PFIQ8sniovEmjy7yqpAu9OAJvPKaq8bu24ja8Q6HdPIstpDx2wfW8DK4fvf1DXzw3/sQ8tXkfvI4yxbzChc872L+CvJ0MejsNfKS8eIgdOkOdMzmOkwg9IbkKPbewOz1WEL88M5Q2vYbDFb371h29BagHPA4b4Twbt5w8AQ8xPKcIgrxWdSw5h5h3PIpjSbq85KS86ALPvEai1DwvYE09SaPLPIJZB72xD5G6ua2IvGJOarxREnu9jpOIPN3IzbyhBhS7Qc8uPWVPYT2V/g086WvmPBGBRbxmfqm8Hx2BvO1lAD3S87m862SJPAp7LTveMWW56WvmvJloHDyuSOk8V3lWvFupFb3q/5u9WrDyO7lIm7x6VqK8CBlzPQsaarzP8kI7gYuCvRkbE71+igu8aOtqu1cU6TsVTJe8EeYyvfOdEz1iTuo8OMgfvDyXm7vTIgI8wksAPcK79LyhPDm9nXFnvJPLG72SnNO7xecJPQB7+zr3QfG8o3NVvKmraL0A4Oi8fI0+vYKPrDzwAQq9ecJsvLmCajy3FSk9KvKUvC33tTuhBpS91CYsvY4yxTzFvGs9e4mUvOJhJL02lS28EFL9PIHBJ7xhSsC8V3lWPSSPY7uP/B88b4GOPDP5o7yyQoM8oDgPvJdrz7zT9+O87mmqu+T5A73tZQC9gfv2uuZq77xzUAo+VnWsvE48Ir0yKx89HriTu6M5Br3vMwW8H1dQvWCyYLwVTBc8x1j1vJPLm7yBi4K8ZuMWPZ0M+ryMmmW8+zuLPFfewzxOoY8842n4PFQ+kDxAZhe9MV2avAcVybyY1OY7jJrlPIdi0rwB2Qu9iv5bPD83zzt/xNo7yoQKvSosZD1g4Si9DhvhvMa1jjy5fsC8zb9QvFZ1rLxFOb07ExmlPNVZnrzA6cW9CH7gvJeal7wYUbi8jf9SvGgWCT3YlGQ7aIZ9u6zbJ7zZ+dG7wYGlPDJlbjyOMkW8ukzFPO8IZ7wCfPI8MC5SvCAhq7t1vcs84saRPB3qjrnWwrW8CnutPPY5nTpjfbK8elaiPPgLTL3QvJ08FB3PPK2lAj1GPWe8O8kWvePKOztiTmo8c1CKPGKzVz3dkig9wOlFvTpkKT2mD986TAkwvIrINj0W7/28eyQnvYT87brnmTc8mnBwO4GLAr371h08KixkvKVwojyMmuW8A+HfO9sw7rtBNBw8xFd+PDb6GjxREvu86WvmOwbi1jw+BF28tn1JvHBPkzlOcsc80/djPNSLmbtmGTy9brezOw+vFjw+BN08CneDvNhaFb3mlQ09NGK7PDf+xDyKme68HFbZPLUUMjw3YzI9mDnUvC33NT2oRvs8O574uiGO7Lsjhw+8723UvPaeirsCeMg801zRuxyM/jvdkig7EkugPBbv/bugct47D6+WvGJO6rsGER88C39XO/pC6Dybnzg7VUK6O/M4pjwlIxk8CH5gvCGKwjsKdwM9SQQPvf1D37yH/eS7toHzvJ4FHbysrN88KcNMvSMmzLv3QfG42fnRPLQQiDxInyG7+AvMvBKwjTtyIUK8pabHu+DJRLyCj6w88qTwvOT5AzyF9ZC8bekuPHZRAb16u4+8wksAvKBy3rjHgxO9TAWGvKOp+jmuSGk6Ov+7vO02uDu5SJs8l5qXvZxpEzzqNcG8CnstPKM5hrwMSTK7zb9QPKHXS71GotQ8jJY7vexs3bxDZ447wksAvJX+Dby5HX28JPAmPEZohbwX6KC8RZqAPKaq8Tv/QCw9nZyFvC/FOruLLaS70e8Pvd3IzbwI36M8+t36uzZfiDzEHS89RAbLPPMCATymD187ak0lOyYnQzt3HwY9gSYVu64OGr3ezPe5PQAzvdkoGrzezPc8YrPXPH6KC7yWMYA7850TvP+lmTySYoQ8ZRWSPOtos7zn/qQ8Rj1nu7mtCLxm4xY8fI2+vFGiBrzO7pi776N5OmgWibyV02+8BRh8unwoUTv3ojS9st2VvFILHjydnIU8lzWqu1faGby651c8Oy4EPNJYJ7wO5Tu76wPGvC8qKLylcKK6o9hCO/nZUDwRHFg83vcVvaejFD0GER89JIs5vHmMRzwXg7O8gWBkvIBYEDzkM1O8tErXu4JZh7y3sLu8PzMlPePKO7y85KQ6H431PBNT9DuEJww9KFq1OfM8UDxfrja7c4rZO0TQpbwFfem7H/JiPKYP3zxxU707qEb7u1d51rt8KFE8FlRrPaBy3rzvo/m8gFiQPLewO73k+QM9K1ssPFytPzxQ1AE8wrv0vNLzObyRzk48NstSvBdNjjyUNDO8vB50vc2FAb01x6g79wciPe0Ak7vVWR48CnstvCos5LwJrai99weivIuSkTxcTPy80YoiPMlRmDy0gPy7Ca0oPNVZnrxasPI89NCFO1JBw7w7A+a4ntbUvHhdfzzRiqK8d1UrPbdLzrxUE/K8rq1WPMGBJbyNLhs85ceIPSfxnTye1lS9axuquVES+7uuc4e8QTQcvGNHDTuzq5q8F02OvCG5irwD4V88hl6oPOH8trycaRO8kv2WO4L0GT1/xFq6QwKhPGJO6rwJ4828GfB0vFN0NT01LJa7c8D+O4zFA7wCp5A83CkRvW2IazrkM1M5o6n6uI1kwLy5gmo83S07Pdn5UTy5Hf27WRE2vGsXAD3kmMC88qTwPA9KqTtUPpC7MmVuPA6287sl9FC8/UPfPBO0t7x+igu9mnBwPKULNbvuaSo9q0fyO2fnQL2ZzYm8p6MUPWnkDT3Zw6w6Z+dAPbBFNj0ovyI9rHa6uhYanDz3pl68x4OTO5X+jbxJo0u9v0oJvdkomrtAO3k8MWHEvIQnjLwuXKM8H411uBmAgLxOdnE8njtCveGXybo1kYO8yh8dPSOHjzz93nG8OGfcvKgMLL1n50C8RmgFPC2SyDzellK6dFS0OhJPyjsZVWI8MWHEO5pwcLxmfim9/QkQvEo3AbzEghw9G+1BPHcfBrtDPHC8BNqCPdbCtTxREvu88aBGvXAkdTqYA687v+UbOuGXyby8SZI8URJ7vAEPMT3szSC89p6KvQPh3zyWZ6U8uHqWu9z6SDy05Wm9IvPZu/UKVTzdyM28pnTMvHMl7LvH6IC8zfV1PLOrGr2/H+s8sniou5X+Db25fkC8dYemPGIUm7wTtDe8LfOLPAkSlrzeXIO7v0oJPOtoszwtksg80r0UOgp7LTyabMa87NFKvAsa6jxBoGa8IIaYvB8dAT1aFeC8tID8uxhROL2pq2g8wuq8O1gNDLtOdvE8jPuoPBSCvDuSYoQ8elaiPF9N8zrAsyA9qavoPEbYebqcCFA825XbORu3HD2eoC87xYZGvYb5Oru7Gso8J5DavG5SxjyBJpU78m5LPIn6sTuBxdG8bIAXu/emXjya0TM7oaGmvPemXj3S87k7GbrPO95cg7wtksi8tRQyu+zNoDzTIoI8Ov+7vInEjDxnTC482cMsPD6f7zwghpg7v0oJvf3ecTyPNm+9QaBmvEBmlzwB2Ys8WA2MuhtSrzyxSeC8KPXHu1Z1LDykoh08LyooPTJlbrzxzw47nQx6O/vWHb1I1UY7MsaxPEo3Ab1LOys8PcoNPGJO6rzcxCO9VD4QPQtFiLxX3kM87NFKOwh+YDySN+a8A+FfuiqRUbz52VC7KFo1PfRrGDz1b8I8+m0GPGuH9LvgLrK7LyoovSfGfzzGtQ48IIaYvGYZPLy6sTI9rkhpO0w/1TplsKQ8kpzTPAbiVr0Ae3s7aBYJPEo3gbzZ+dG8VnGCvE48IjxeSck88zxQPXOKWTuD+EO71vhau0KZCTuuSGm8a+xhPDqaTj0ZVWI7txWpvNb4Wrx5J9o8WrDyulfewzxjRw297ZulvH+ONTypq2g8QGYXO/pthryvQYy7fMNjvJrRs7xPpTm6WtsQPci94ruE/O28xeuzPKYP3ztiTuo8Qc8uvCS6gTwLGmq8cIU4vBVMlzzoY5I8Pp/vvOH4DL2AWBC8PzOluTWRAzzvo/k8ybYFvKM5Bj3eMeU76v8bOyS6gbxxUz27786Xu6mr6LwE2gK8SzsrvD2bxTzZw6w6nGmTPFLcVT0TU3S8EbfqPIRh2zxmGTw88aBGvJ9qCrsyKx+8j2ENvbDgSDqbOku8IiIivMKFT722gXO8bSP+u/ShvTypdcO8Sm0mO4X1kLyv3B48l295O/TQBT0vKig8YxjFOzbLUj0TfpK8HSA0vKgMLDzlxwg8c4pZu70Xl7zraLM8ptk5vbQQiLyxD5G5hZAjPd4xZbvellK8kpzTPHdVqzw2X4i8rECVvFtEKDxwJHU6zSCUO3G4KrvrZIm6bYhrPD83T7xVQrq7eL7CvEABqjxfE6Q8EFL9PNz6yLzblds8ntZUOQtFCDxUPpA83jFlPP7XFL34OhQ9OfsRu4qZbrytpYK8RZqAvFeve7sST0q8ST7evGexG7yacHC7F4Mzvd0tuzxkSzc8+qfVOgu1/DzI7Cq9CH7gO1gNDD3qmi69723Uuo4yRTyxqiM9qRBWvNP34zwZgAA9mWicvB/y4jyHYlK81pPturJCg7veMWU8wBiOPDPKWz36p9U8UXfoPKbVj7ydcee8iv7bN80glDyeBZ08lgI4vMAYDr37Ows9BhGfvGqDSjpqTaU72JRkPZnNCbxi6fw7SzsrPNEltbxmfqk86pouPaQHC7uY/4S8X01zO8jsqjrvbdS7+HA5vB0gtDuAXLq8ciFCPTRekTtg4ag8LyooPGGrA72VOF09imPJO6DTIbwhVJ29hGFbO4HF0Tv1CtU8CxrqO4X1ELxs5YQ8cLSAunwo0bw0XhE9+DqUvGCy4Drb9p485JjAvDpkqbsvYE08TW4dPWKvrbwHerY88QW0vBm2Jby1FDI7/jwCO9BbWrxV3Uw9XbFpvI3/Uru35uA7cbgqPRvtQTr2PUc8CRIWvRxW2Tsqx/Y8nQz6OwYRnzxk5sk8ISl/POEyXD0PSqm63yqIu/RrmLjTXFG9h2JSO3UiObyhawG9hfWQvJLS+DxJPt48v0qJPFF3aDzqNUE8jcmtPFtEqLzWXci7Y+Ifu9wpkbzun888MPisux5TJjwCfPI8gcGnvJbMEjuhBpQ8P5iSPEBml7sZ8PQ8e7+5PIz7qLwBdB69ee0KPOlr5jzOJD69oHLeuydWizxaek28VHjfvJc1qjuuqay79aXnPGtRzzzVvou8t7C7OrnjLTlLcVA8blLGu5EzvLwARda7blLGvKIKvrt9W0O9THV6vH76f7zyCV48yL3iu2kas7ttiOs8aLVFu1FzPj3raLM8coKFvJGYqTuM+6i8dx+GOgh+YD296M68ST5eOgWoB7sSSyA9mAMvu52cBTwIRJG8oaEmvMjsqryvEsQ738WaPGpNpTxygoW8dLmhvHiIHTzQkX88iV+fvP3e8Tq7ew08nZyFvCcrbb3YvwK97TY4PIb5ujvQW1q83sz3u+/OFz1dsWk89aVnvIGLAr3sMg68fMPjPIFgZDzwAQq7WaxIuoGLgrkBdB48xlChvOH4jLyBJhU8pqrxPIfHv7v63fq8KSg6vdtbjLsahKq8C7X8OjD4rLx3Vau8axcAvUGgZruMxQM976N5uzP5Izx6uw+9qRBWu3QeDzyXNSq81PAGPW1OHDx/xNq8HeoOvW1OnLzrZIm8kpzTPCpXgjwhVJ28NZGDO3bwPb2s2ye7Hu44vBZU67yz4T+7hyiDPGLp/LuYOdQ7paZHPF17RLyMmmW8Hx0BO3nCbLzmMCC9w0+qvIBcurxpf6A8e+6BvHRUtDwG4ta8fVtDvPY9x7zn/iQ9U3ALPYD3TDvTknY6F02OuiIiorxbDgM9HPFrPP1D3zwn8R296jVBPKaq8TuurVY7KPXHPLOrmjx1vUu8eYxHPH/EWjwwLtI8kC8SvEx1ejw0XhE8yh+dujto0zwR4gi94JMfPT7ON7ymqvE8XbHpPE528Ty546282JRkPByMfjw0/U09xFd+vJrRM7tFOb080CGLPKl1Q70kKva86GOSPOc0SjulcCK8RgMYPDvJFrrkzuW8Ps63vCgkkDwWGhy9mJ5BO1qwcjwvZPc8DRe3vIz7qLyKme48/g26PBjsSjq5rQg82PWnuofHP7ww9AK6EFJ9PPg6lLyaNiG8uR39vBlV4rwEdZW7ogq+PCqR0bw2lS2962gzPZ2cBT2abMa8brezNy3zi7xfeJG9J/EdvBeDMzuAXDq9/d5xPN9grTshVB29ylnsui0tW7o1kYM81fQwPMDpxby54y08" } ], "model": "text-embedding-3-small", "usage": { "prompt_tokens": 9, "total_tokens": 9 } } headers: CF-RAY: - 99ac1f2bcf0c5a01-MXP Connection: - keep-alive Content-Type: - application/json Date: - Fri, 07 Nov 2025 10:24:35 GMT Server: - cloudflare Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff access-control-allow-origin: - '*' access-control-expose-headers: - X-Request-ID alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '8414' openai-model: - text-embedding-3-small openai-organization: test_openai_org_id openai-processing-ms: - '90' openai-project: - proj_Pf1eM5R55Z35wBy4rt8PxAGq openai-version: - '2020-10-01' strict-transport-security: - max-age=31536000; includeSubDomains; preload via: - envoy-router-545c575f45-dndxq x-envoy-upstream-service-time: - '307' x-openai-proxy-wasm: - v0.1 x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '5000000' x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - '4999988' x-ratelimit-reset-requests: - 6ms x-ratelimit-reset-tokens: - 0s x-request-id: - req_8c7cc42f2a184e4cbf905a2f3830b7ca status: code: 200 message: OK version: 1 test_responses_create_aggregates_cache_tokens[content_mode0].yaml000066400000000000000000000110711527070072300542420ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "Say this is a test", "instructions": "You are a helpful assistant.", "model": "gpt-4o-mini" } headers: Accept: - application/json Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '103' Content-Type: - application/json Host: - api.openai.com User-Agent: - OpenAI/Python 1.109.1 X-Stainless-Arch: - arm64 X-Stainless-Async: - 'false' X-Stainless-Lang: - python X-Stainless-OS: - MacOS X-Stainless-Package-Version: - 1.109.1 X-Stainless-Runtime: - CPython X-Stainless-Runtime-Version: - 3.12.12 authorization: - Bearer test_openai_api_key x-stainless-read-timeout: - '600' x-stainless-retry-count: - '0' method: POST uri: https://api.openai.com/v1/responses response: body: string: |- { "id": "resp_0a8171a24e10956f0069e2f3f03a4c819da6eca7c5d387a3b8", "object": "response", "created_at": 1776481264, "status": "completed", "background": false, "billing": { "payer": "developer" }, "completed_at": 1776481265, "error": null, "frequency_penalty": 0.0, "incomplete_details": null, "instructions": "You are a helpful assistant.", "max_output_tokens": null, "max_tool_calls": null, "model": "gpt-4o-mini-2024-07-18", "output": [ { "id": "msg_0a8171a24e10956f0069e2f3f0f640819d9b572b43c703a798", "type": "message", "status": "completed", "content": [ { "type": "output_text", "annotations": [], "logprobs": [], "text": "This is a test." } ], "role": "assistant" } ], "parallel_tool_calls": true, "presence_penalty": 0.0, "previous_response_id": null, "prompt_cache_key": null, "prompt_cache_retention": "in_memory", "reasoning": { "effort": null, "summary": null }, "safety_identifier": null, "service_tier": "default", "store": true, "temperature": 1.0, "text": { "format": { "type": "text" }, "verbosity": "medium" }, "tool_choice": "auto", "tools": [], "top_logprobs": 0, "top_p": 1.0, "truncation": "disabled", "usage": { "input_tokens": 22, "input_tokens_details": { "cached_tokens": 0 }, "output_tokens": 6, "output_tokens_details": { "reasoning_tokens": 0 }, "total_tokens": 28 }, "user": null, "metadata": {} } headers: CF-RAY: - 9ee06c3d2ae3f2f9-EWR Connection: - keep-alive Content-Type: - application/json Date: - Sat, 18 Apr 2026 03:01:05 GMT Server: - cloudflare Set-Cookie: test_set_cookie Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '1575' openai-organization: test_openai_org_id openai-processing-ms: - '917' openai-project: - proj_s74VWObPgWXRchv2sHdrOTPY openai-version: - '2020-10-01' set-cookie: - __cf_bm=nawHaNe2Gcjnf3PnJWOe7eCMcuL4GfmHJjNVPemFYOE-1776481264.1903315-1.0.1.1-cmimAw8su.1UpRhI8dWFr6DhgPw2Z2sJi8cKNV_tHFMFijmHK3ND45DjgKsf_ur_EZexZIAN.O__1bqzwnC_acPUsfxzivCaUIWeL1ThD2hWwU2mRBTfDdUDbR04KBsW; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sat, 18 Apr 2026 03:31:05 GMT x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9996' x-ratelimit-remaining-tokens: - '199959' x-ratelimit-reset-requests: - 34.066s x-ratelimit-reset-tokens: - 12ms x-request-id: - req_07ac89c9040840a6be685694eacd3288 status: code: 200 message: OK version: 1 test_responses_create_api_error[content_mode0].yaml000066400000000000000000000045241527070072300514120ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "Hello", "model": "this-model-does-not-exist" } headers: Accept: - application/json Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '56' Content-Type: - application/json Host: - api.openai.com User-Agent: - OpenAI/Python 1.109.1 X-Stainless-Arch: - arm64 X-Stainless-Async: - 'false' X-Stainless-Lang: - python X-Stainless-OS: - MacOS X-Stainless-Package-Version: - 1.109.1 X-Stainless-Runtime: - CPython X-Stainless-Runtime-Version: - 3.12.12 authorization: - Bearer test_openai_api_key x-stainless-read-timeout: - '600' x-stainless-retry-count: - '0' method: POST uri: https://api.openai.com/v1/responses response: body: string: |- { "error": { "message": "The requested model 'this-model-does-not-exist' does not exist.", "type": "invalid_request_error", "param": "model", "code": "model_not_found" } } headers: CF-RAY: - 9ee06c562f5c0f7d-EWR Connection: - keep-alive Content-Length: - '191' Content-Type: - application/json Date: - Sat, 18 Apr 2026 03:01:08 GMT Server: - cloudflare Set-Cookie: test_set_cookie Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload X-Content-Type-Options: - nosniff alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC openai-organization: test_openai_org_id openai-processing-ms: - '107' openai-project: - proj_s74VWObPgWXRchv2sHdrOTPY openai-version: - '2020-10-01' set-cookie: - __cf_bm=sImUBrv0bwxemWtgUZK8rUGOArohh1idXm1s6ve.9PM-1776481268.1848395-1.0.1.1-wfALia92HJtSuU5STTxRZGW0Cd1tZtPVOpbKTSFoiJLjLOcwVrsTA4C4j2yaYyhAqXvYCG9Ns.EIIhy5ZQv_PNuB11_p2HjTGRNJGtclgs_deByUIWyOSuE2JuY9zxD.; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sat, 18 Apr 2026 03:31:08 GMT x-request-id: - req_48f6dc739d654972b34e069edb97d2dc status: code: 400 message: Bad Request version: 1 test_responses_create_basic[content_mode0].yaml000066400000000000000000000111201527070072300504770ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "Say this is a test", "instructions": "You are a helpful assistant.", "model": "gpt-4o-mini", "stream": false } headers: Accept: - application/json Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '120' Content-Type: - application/json Host: - api.openai.com User-Agent: - OpenAI/Python 1.109.1 X-Stainless-Arch: - arm64 X-Stainless-Async: - 'false' X-Stainless-Lang: - python X-Stainless-OS: - MacOS X-Stainless-Package-Version: - 1.109.1 X-Stainless-Runtime: - CPython X-Stainless-Runtime-Version: - 3.12.12 authorization: - Bearer test_openai_api_key x-stainless-read-timeout: - '600' x-stainless-retry-count: - '0' method: POST uri: https://api.openai.com/v1/responses response: body: string: |- { "id": "resp_0f4faba17dcd0f1e0069e2f3e4907881909179832ba1237025", "object": "response", "created_at": 1776481253, "status": "completed", "background": false, "billing": { "payer": "developer" }, "completed_at": 1776481257, "error": null, "frequency_penalty": 0.0, "incomplete_details": null, "instructions": "You are a helpful assistant.", "max_output_tokens": null, "max_tool_calls": null, "model": "gpt-4o-mini-2024-07-18", "output": [ { "id": "msg_0f4faba17dcd0f1e0069e2f3e7b2b88190bff23981628ac362", "type": "message", "status": "completed", "content": [ { "type": "output_text", "annotations": [], "logprobs": [], "text": "This is a test." } ], "role": "assistant" } ], "parallel_tool_calls": true, "presence_penalty": 0.0, "previous_response_id": null, "prompt_cache_key": null, "prompt_cache_retention": "in_memory", "reasoning": { "effort": null, "summary": null }, "safety_identifier": null, "service_tier": "default", "store": true, "temperature": 1.0, "text": { "format": { "type": "text" }, "verbosity": "medium" }, "tool_choice": "auto", "tools": [], "top_logprobs": 0, "top_p": 1.0, "truncation": "disabled", "usage": { "input_tokens": 22, "input_tokens_details": { "cached_tokens": 0 }, "output_tokens": 6, "output_tokens_details": { "reasoning_tokens": 0 }, "total_tokens": 28 }, "user": null, "metadata": {} } headers: CF-RAY: - 9ee06be8ab20d481-EWR Connection: - keep-alive Content-Type: - application/json Date: - Sat, 18 Apr 2026 03:00:58 GMT Server: - cloudflare Set-Cookie: test_set_cookie Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '1575' openai-organization: test_openai_org_id openai-processing-ms: - '7177' openai-project: - proj_s74VWObPgWXRchv2sHdrOTPY openai-version: - '2020-10-01' set-cookie: - __cf_bm=ckwOJyTtv0EpPspvwSIW3P1dXrhIU1GtdL9BNBJlNbA-1776481250.670554-1.0.1.1-7j2fYPA6kw.SMgF3tYMODcA6O.P9Xwu6N6jolKR9OG0AJczT7a1Jr5HlRnJ6T.tXJ5HYV_jtosjzUCBM1yHZTiRTwGTtu6Klyq1ldSG5hFV4MRK9StK_zmCKjmIbqO9d; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sat, 18 Apr 2026 03:30:58 GMT x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - '199958' x-ratelimit-reset-requests: - 8.64s x-ratelimit-reset-tokens: - 12ms x-request-id: - req_d6009916bd4447188ce5baaea187709b status: code: 200 message: OK version: 1 test_responses_create_captures_content[content_mode0].yaml000066400000000000000000000113141527070072300530030ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "Say this is a test", "instructions": "You are a helpful assistant.", "model": "gpt-4o-mini", "stream": false, "text": { "format": { "type": "text" } } } headers: Accept: - application/json Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '158' Content-Type: - application/json Host: - api.openai.com User-Agent: - OpenAI/Python 1.109.1 X-Stainless-Arch: - arm64 X-Stainless-Async: - 'false' X-Stainless-Lang: - python X-Stainless-OS: - MacOS X-Stainless-Package-Version: - 1.109.1 X-Stainless-Runtime: - CPython X-Stainless-Runtime-Version: - 3.12.12 authorization: - Bearer test_openai_api_key x-stainless-read-timeout: - '600' x-stainless-retry-count: - '0' method: POST uri: https://api.openai.com/v1/responses response: body: string: |- { "id": "resp_0d6d73520bd9405c0069e2f3eb1108819590a9e98dba6a746d", "object": "response", "created_at": 1776481259, "status": "completed", "background": false, "billing": { "payer": "developer" }, "completed_at": 1776481260, "error": null, "frequency_penalty": 0.0, "incomplete_details": null, "instructions": "You are a helpful assistant.", "max_output_tokens": null, "max_tool_calls": null, "model": "gpt-4o-mini-2024-07-18", "output": [ { "id": "msg_0d6d73520bd9405c0069e2f3eba7f081958368e178f0078326", "type": "message", "status": "completed", "content": [ { "type": "output_text", "annotations": [], "logprobs": [], "text": "This is a test. How can I assist you further?" } ], "role": "assistant" } ], "parallel_tool_calls": true, "presence_penalty": 0.0, "previous_response_id": null, "prompt_cache_key": null, "prompt_cache_retention": "in_memory", "reasoning": { "effort": null, "summary": null }, "safety_identifier": null, "service_tier": "default", "store": true, "temperature": 1.0, "text": { "format": { "type": "text" }, "verbosity": "medium" }, "tool_choice": "auto", "tools": [], "top_logprobs": 0, "top_p": 1.0, "truncation": "disabled", "usage": { "input_tokens": 22, "input_tokens_details": { "cached_tokens": 0 }, "output_tokens": 13, "output_tokens_details": { "reasoning_tokens": 0 }, "total_tokens": 35 }, "user": null, "metadata": {} } headers: CF-RAY: - 9ee06c1ccc4b0f93-EWR Connection: - keep-alive Content-Type: - application/json Date: - Sat, 18 Apr 2026 03:01:00 GMT Server: - cloudflare Set-Cookie: test_set_cookie Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '1606' openai-organization: test_openai_org_id openai-processing-ms: - '1097' openai-project: - proj_s74VWObPgWXRchv2sHdrOTPY openai-version: - '2020-10-01' set-cookie: - __cf_bm=eRjb5xrZMpFJHlBLeAzQEHihWjr.Mb5bNSgOGzGSnwA-1776481259.0045145-1.0.1.1-fAERlGPpEWHNuG0MiIcvmzII8t4Xj7yC_HgjLktqdKXPJmbHLtPtT_beeOd8sCcZVQYjGOH0QcF0w4Hz9S.t29trpZuNiezPTQNuyMSy51F2kKYkypy9Eromd2kQp9Cj; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sat, 18 Apr 2026 03:31:00 GMT x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9998' x-ratelimit-remaining-tokens: - '199958' x-ratelimit-reset-requests: - 13.065s x-ratelimit-reset-tokens: - 12ms x-request-id: - req_9191bbf984a54a3281f8f335b87e7aeb status: code: 200 message: OK version: 1 test_responses_create_captures_tool_call_content[content_mode0].yaml000066400000000000000000000135051527070072300550370ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "What's the weather in Seattle right now?", "model": "gpt-4o-mini", "tool_choice": { "type": "function", "name": "get_current_weather" }, "tools": [ { "type": "function", "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. Boston, MA" } }, "required": [ "location" ], "additionalProperties": false }, "strict": true } ] } headers: Accept: - application/json Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '480' Content-Type: - application/json Host: - api.openai.com User-Agent: - OpenAI/Python 1.109.1 X-Stainless-Arch: - arm64 X-Stainless-Async: - 'false' X-Stainless-Lang: - python X-Stainless-OS: - MacOS X-Stainless-Package-Version: - 1.109.1 X-Stainless-Runtime: - CPython X-Stainless-Runtime-Version: - 3.12.12 authorization: - Bearer test_openai_api_key x-stainless-read-timeout: - '600' x-stainless-retry-count: - '0' method: POST uri: https://api.openai.com/v1/responses response: body: string: |- { "id": "resp_0bedf6e1ffba28050069e2f401ae1c8196be360fd5993c96de", "object": "response", "created_at": 1776481281, "status": "completed", "background": false, "billing": { "payer": "developer" }, "completed_at": 1776481282, "error": null, "frequency_penalty": 0.0, "incomplete_details": null, "instructions": null, "max_output_tokens": null, "max_tool_calls": null, "model": "gpt-4o-mini-2024-07-18", "output": [ { "id": "fc_0bedf6e1ffba28050069e2f402203c8196b45065342d00ed17", "type": "function_call", "status": "completed", "arguments": "{\"location\":\"Seattle, WA\"}", "call_id": "call_90uO5LcGP5vTBTCrjyhYtWsA", "name": "get_current_weather" } ], "parallel_tool_calls": true, "presence_penalty": 0.0, "previous_response_id": null, "prompt_cache_key": null, "prompt_cache_retention": "in_memory", "reasoning": { "effort": null, "summary": null }, "safety_identifier": null, "service_tier": "default", "store": true, "temperature": 1.0, "text": { "format": { "type": "text" }, "verbosity": "medium" }, "tool_choice": { "type": "function", "name": "get_current_weather" }, "tools": [ { "type": "function", "description": "Get the current weather in a given location", "name": "get_current_weather", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. Boston, MA" } }, "required": [ "location" ], "additionalProperties": false }, "strict": true } ], "top_logprobs": 0, "top_p": 1.0, "truncation": "disabled", "usage": { "input_tokens": 72, "input_tokens_details": { "cached_tokens": 0 }, "output_tokens": 8, "output_tokens_details": { "reasoning_tokens": 0 }, "total_tokens": 80 }, "user": null, "metadata": {} } headers: CF-RAY: - 9ee06caa3d06c745-EWR Connection: - keep-alive Content-Type: - application/json Date: - Sat, 18 Apr 2026 03:01:22 GMT Server: - cloudflare Set-Cookie: test_set_cookie Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '2034' openai-organization: test_openai_org_id openai-processing-ms: - '713' openai-project: - proj_s74VWObPgWXRchv2sHdrOTPY openai-version: - '2020-10-01' set-cookie: - __cf_bm=77TblxQ40FwSWcmbiooIxTpBbJWtYa4Sa8xnnuOb8P4-1776481281.6342883-1.0.1.1-wYyCi1iDQ3bB4g5c2Jj1pldvzLmLS9NtJ1ryP99rJkxwR77VRG3qXvTHSKzUAQk9LBvwdBumDx4dWSKvmBqXsb7K2O8zcH3uxfGFCw4SLKIaQIsDy2SGcb9JuoVZMe4r; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sat, 18 Apr 2026 03:31:22 GMT x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9987' x-ratelimit-remaining-tokens: - '199711' x-ratelimit-reset-requests: - 1m51.595s x-ratelimit-reset-tokens: - 86ms x-request-id: - req_66f20a8d196540e7928ba09ef1baad00 status: code: 200 message: OK version: 1 test_responses_create_event_only_no_content_in_span.yaml000066400000000000000000000111221527070072300525700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "Say this is a test", "instructions": "You are a helpful assistant.", "model": "gpt-4o-mini", "stream": false } headers: Accept: - application/json Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '120' Content-Type: - application/json Host: - api.openai.com User-Agent: - OpenAI/Python 1.109.1 X-Stainless-Arch: - arm64 X-Stainless-Async: - 'false' X-Stainless-Lang: - python X-Stainless-OS: - MacOS X-Stainless-Package-Version: - 1.109.1 X-Stainless-Runtime: - CPython X-Stainless-Runtime-Version: - 3.12.12 authorization: - Bearer test_openai_api_key x-stainless-read-timeout: - '600' x-stainless-retry-count: - '0' method: POST uri: https://api.openai.com/v1/responses response: body: string: |- { "id": "resp_0da562f2b64fdb7e0069e2f40b28e081909487104de4d4c8c0", "object": "response", "created_at": 1776481291, "status": "completed", "background": false, "billing": { "payer": "developer" }, "completed_at": 1776481291, "error": null, "frequency_penalty": 0.0, "incomplete_details": null, "instructions": "You are a helpful assistant.", "max_output_tokens": null, "max_tool_calls": null, "model": "gpt-4o-mini-2024-07-18", "output": [ { "id": "msg_0da562f2b64fdb7e0069e2f40ba8bc8190a803f1b565d49c7c", "type": "message", "status": "completed", "content": [ { "type": "output_text", "annotations": [], "logprobs": [], "text": "This is a test." } ], "role": "assistant" } ], "parallel_tool_calls": true, "presence_penalty": 0.0, "previous_response_id": null, "prompt_cache_key": null, "prompt_cache_retention": "in_memory", "reasoning": { "effort": null, "summary": null }, "safety_identifier": null, "service_tier": "default", "store": true, "temperature": 1.0, "text": { "format": { "type": "text" }, "verbosity": "medium" }, "tool_choice": "auto", "tools": [], "top_logprobs": 0, "top_p": 1.0, "truncation": "disabled", "usage": { "input_tokens": 22, "input_tokens_details": { "cached_tokens": 0 }, "output_tokens": 6, "output_tokens_details": { "reasoning_tokens": 0 }, "total_tokens": 28 }, "user": null, "metadata": {} } headers: CF-RAY: - 9ee06ce2aead41b2-EWR Connection: - keep-alive Content-Type: - application/json Date: - Sat, 18 Apr 2026 03:01:31 GMT Server: - cloudflare Set-Cookie: test_set_cookie Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC content-length: - '1575' openai-organization: test_openai_org_id openai-processing-ms: - '698' openai-project: - proj_s74VWObPgWXRchv2sHdrOTPY openai-version: - '2020-10-01' set-cookie: - __cf_bm=5Y3wm6qm03P0G7_SigGuGob8rfE0X7eMfHOJDLR_E5o-1776481290.6632602-1.0.1.1-2QRz.JRmnCza_Xtkyqwgj1IPTwrSSA5LHyD9chhvKiCoHNE9_.I5_0qopgAV60mriRXdX99CCxozoio7VqvW.wnvklHzb6gwZDEsQCAlBqZGcIHrBfFk4jkvspj4sMO0; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sat, 18 Apr 2026 03:31:31 GMT x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - '9985' x-ratelimit-remaining-tokens: - '199959' x-ratelimit-reset-requests: - 2m7.93s x-ratelimit-reset-tokens: - 12ms x-request-id: - req_76d5ef520a6b4d74975dae1ade822de9 status: code: 200 message: OK version: 1 test_responses_create_instrumentation_error_swallowed[content_mode0].yaml000066400000000000000000000200021527070072300561520ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": "Say this is a test", "instructions": "You are a helpful assistant.", "model": "gpt-4o-mini", "stream": true } headers: Accept: - application/json Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '119' Content-Type: - application/json Host: - api.openai.com User-Agent: - OpenAI/Python 1.109.1 X-Stainless-Arch: - arm64 X-Stainless-Async: - 'false' X-Stainless-Lang: - python X-Stainless-OS: - MacOS X-Stainless-Package-Version: - 1.109.1 X-Stainless-Runtime: - CPython X-Stainless-Runtime-Version: - 3.12.12 authorization: - Bearer test_openai_api_key x-stainless-read-timeout: - '600' x-stainless-retry-count: - '0' method: POST uri: https://api.openai.com/v1/responses response: body: string: |+ event: response.created data: {"type":"response.created","response":{"id":"resp_052003e5c7cd2fe10069e2f400498c819f943227bcc94a63a6","object":"response","created_at":1776481280,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} event: response.in_progress data: {"type":"response.in_progress","response":{"id":"resp_052003e5c7cd2fe10069e2f400498c819f943227bcc94a63a6","object":"response","created_at":1776481280,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} event: response.output_item.added data: {"type":"response.output_item.added","item":{"id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2} event: response.content_part.added data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3} event: response.output_text.delta data: {"type":"response.output_text.delta","content_index":0,"delta":"This","item_id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","logprobs":[],"obfuscation":"hmEwhe4L8nm4","output_index":0,"sequence_number":4} event: response.output_text.delta data: {"type":"response.output_text.delta","content_index":0,"delta":" is","item_id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","logprobs":[],"obfuscation":"u3Q5kHUIahzcI","output_index":0,"sequence_number":5} event: response.output_text.delta data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","logprobs":[],"obfuscation":"0ZnBkOCdgrlbIT","output_index":0,"sequence_number":6} event: response.output_text.delta data: {"type":"response.output_text.delta","content_index":0,"delta":" test","item_id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","logprobs":[],"obfuscation":"hYsQ5M0hWLs","output_index":0,"sequence_number":7} event: response.output_text.delta data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","logprobs":[],"obfuscation":"PyM68hgEwGGLkTN","output_index":0,"sequence_number":8} event: response.output_text.done data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","logprobs":[],"output_index":0,"sequence_number":9,"text":"This is a test."} event: response.content_part.done data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."},"sequence_number":10} event: response.output_item.done data: {"type":"response.output_item.done","item":{"id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."}],"role":"assistant"},"output_index":0,"sequence_number":11} event: response.completed data: {"type":"response.completed","response":{"id":"resp_052003e5c7cd2fe10069e2f400498c819f943227bcc94a63a6","object":"response","created_at":1776481280,"status":"completed","background":false,"completed_at":1776481280,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."}],"role":"assistant"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":22,"input_tokens_details":{"cached_tokens":0},"output_tokens":6,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":28},"user":null,"metadata":{}},"sequence_number":12} headers: CF-RAY: - 9ee06ca19ad7606a-EWR Connection: - keep-alive Content-Type: - text/event-stream; charset=utf-8 Date: - Sat, 18 Apr 2026 03:01:20 GMT Server: - cloudflare Set-Cookie: test_set_cookie Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload Transfer-Encoding: - chunked X-Content-Type-Options: - nosniff alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC openai-organization: test_openai_org_id openai-processing-ms: - '51' openai-project: - proj_s74VWObPgWXRchv2sHdrOTPY openai-version: - '2020-10-01' set-cookie: - __cf_bm=zeme85udcp85MpYWX94kpho5aA4pRXRhhWbljEzlyKg-1776481280.2544615-1.0.1.1-DxHmuQ9bjw1.C_t_MZ23Az8WRay9CC2Wh0GbHPNN41zA5npmFYATDR2lI7uN061efp8b0vaq0viZms.aHHjkxiiNYFhewlDgEVLyFCa4F1mVx1iKbXQcmUJWbaXjFOtV; HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sat, 18 Apr 2026 03:31:20 GMT x-request-id: - req_4e7549325e3f4abaa4ac13b74667292e status: code: 200 message: OK version: 1 test_responses_create_reports_reasoning_tokens[content_mode0].yaml000066400000000000000000000135551527070072300545620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettesinteractions: - request: body: |- { "input": [ { "role": "user", "content": "\nWrite a bash script that takes a matrix represented as a string with\nformat '[1,2],[3,4],[5,6]' and prints the transpose in the same format.\n" } ], "max_output_tokens": 300, "model": "gpt-5.4", "reasoning": { "effort": "low" } } headers: Accept: - application/json Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '267' Content-Type: - application/json Host: - api.openai.com User-Agent: - OpenAI/Python 1.109.1 X-Stainless-Arch: - arm64 X-Stainless-Async: - 'false' X-Stainless-Lang: - python X-Stainless-OS: - MacOS X-Stainless-Package-Version: - 1.109.1 X-Stainless-Runtime: - CPython X-Stainless-Runtime-Version: - 3.12.12 authorization: - Bearer test_openai_api_key x-stainless-read-timeout: - '30.0' x-stainless-retry-count: - '0' method: POST uri: https://api.openai.com/v1/responses response: body: string: |- { "id": "resp_05177a4994c7df3a0069e2f402f00881a1b9eda520cb779fef", "object": "response", "created_at": 1776481282, "status": "completed", "background": false, "billing": { "payer": "developer" }, "completed_at": 1776481288, "error": null, "frequency_penalty": 0.0, "incomplete_details": null, "instructions": null, "max_output_tokens": 300, "max_tool_calls": null, "model": "gpt-5.4-2026-03-05", "output": [ { "id": "rs_05177a4994c7df3a0069e2f403351c81a1ae31439661e8e976", "type": "reasoning", "summary": [] }, { "id": "msg_05177a4994c7df3a0069e2f403633481a1a824dd7ecef75116", "type": "message", "status": "completed", "content": [ { "type": "output_text", "annotations": [], "logprobs": [], "text": "```bash\n#!/usr/bin/env bash\n\n# Usage:\n# ./transpose.sh '[1,2],[3,4],[5,6]'\n\ninput=\"$1\"\n\n# Extract rows into an array\nIFS='|' read -r -a rows <<< \"$(echo \"$input\" | sed 's/],\\[/]|[/g')\"\n\n# Remove leading/trailing brackets and split first row to get column count\nfirst_row=\"${rows[0]#[}\"\nfirst_row=\"${first_row%]}\"\nIFS=',' read -r -a first_vals <<< \"$first_row\"\ncols=\"${#first_vals[@]}\"\n\n# Build transpose\nresult=\"\"\nfor ((c=0; c InMemoryLogRecordExporter rename try: from opentelemetry.sdk._logs.export import ( # pylint: disable=no-name-in-module InMemoryLogRecordExporter, SimpleLogRecordProcessor, ) except ImportError: # Fallback to old name for compatibility with older SDK versions from opentelemetry.sdk._logs.export import ( InMemoryLogExporter as InMemoryLogRecordExporter, ) from opentelemetry.sdk._logs.export import ( SimpleLogRecordProcessor, ) from opentelemetry.sdk.metrics import ( MeterProvider, ) from opentelemetry.sdk.metrics.export import ( InMemoryMetricReader, ) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) from opentelemetry.sdk.trace.sampling import ALWAYS_OFF @pytest.fixture(scope="function", name="span_exporter") def fixture_span_exporter(): exporter = InMemorySpanExporter() yield exporter @pytest.fixture(scope="function", name="log_exporter") def fixture_log_exporter(): exporter = InMemoryLogRecordExporter() yield exporter @pytest.fixture(scope="function", name="metric_reader") def fixture_metric_reader(): exporter = InMemoryMetricReader() yield exporter @pytest.fixture(scope="function", name="tracer_provider") def fixture_tracer_provider(span_exporter): provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(span_exporter)) return provider @pytest.fixture(scope="function", name="logger_provider") def fixture_logger_provider(log_exporter): provider = LoggerProvider() provider.add_log_record_processor(SimpleLogRecordProcessor(log_exporter)) return provider @pytest.fixture(scope="function", name="meter_provider") def fixture_meter_provider(metric_reader): meter_provider = MeterProvider( metric_readers=[metric_reader], ) return meter_provider @pytest.fixture(autouse=True) def environment(): if not os.getenv("OPENAI_API_KEY"): os.environ["OPENAI_API_KEY"] = "test_openai_api_key" @pytest.fixture def openai_client(): return OpenAI() @pytest.fixture def async_openai_client(): return AsyncOpenAI() @pytest.fixture(scope="module") def vcr_config(): return { "filter_headers": [ ("cookie", "test_cookie"), ("authorization", "Bearer test_openai_api_key"), ("openai-organization", "test_openai_org_id"), ("openai-project", "test_openai_project_id"), ], "decode_compressed_response": True, "before_record_response": scrub_response_headers, } @pytest.fixture( scope="function", params=[(True, "span_only"), (False, "True")], name="content_mode", ) def fixture_content_mode(request): # returns tuple: (latest_experimental_enabled: bool, content_mode: str) # we don't test (True, "event_only"), (True, "span_and_event") because it's util's # responsibility return request.param @pytest.fixture(scope="function") def instrument_no_content( tracer_provider, logger_provider, meter_provider, content_mode, ): _OpenTelemetrySemanticConventionStability._initialized = False latest_experimental_enabled, _ = content_mode os.environ.update( {OTEL_SEMCONV_STABILITY_OPT_IN: "gen_ai_latest_experimental" if latest_experimental_enabled else ""} ) instrumentor = OpenAIInstrumentor() instrumentor.instrument( tracer_provider=tracer_provider, logger_provider=logger_provider, meter_provider=meter_provider, ) yield instrumentor os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None) os.environ.pop(OTEL_SEMCONV_STABILITY_OPT_IN, None) instrumentor.uninstrument() @pytest.fixture(scope="function") def instrument_with_content(tracer_provider, logger_provider, meter_provider, content_mode): _OpenTelemetrySemanticConventionStability._initialized = False latest_experimental_enabled, content_mode_value = content_mode os.environ.update({OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: content_mode_value}) os.environ.update( {OTEL_SEMCONV_STABILITY_OPT_IN: "gen_ai_latest_experimental" if latest_experimental_enabled else ""} ) instrumentor = OpenAIInstrumentor() instrumentor.instrument( tracer_provider=tracer_provider, logger_provider=logger_provider, meter_provider=meter_provider, ) yield instrumentor os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None) os.environ.pop(OTEL_SEMCONV_STABILITY_OPT_IN, None) instrumentor.uninstrument() @pytest.fixture(scope="function") def instrument_with_content_unsampled(span_exporter, logger_provider, meter_provider, content_mode): _OpenTelemetrySemanticConventionStability._initialized = False latest_experimental_enabled, content_mode_value = content_mode os.environ.update({OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: content_mode_value}) os.environ.update( {OTEL_SEMCONV_STABILITY_OPT_IN: "gen_ai_latest_experimental" if latest_experimental_enabled else ""} ) tracer_provider = TracerProvider(sampler=ALWAYS_OFF) tracer_provider.add_span_processor(SimpleSpanProcessor(span_exporter)) instrumentor = OpenAIInstrumentor() instrumentor.instrument( tracer_provider=tracer_provider, logger_provider=logger_provider, meter_provider=meter_provider, ) yield instrumentor os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None) os.environ.pop(OTEL_SEMCONV_STABILITY_OPT_IN, None) instrumentor.uninstrument() @pytest.fixture(scope="function") def instrument_event_only(tracer_provider, logger_provider, meter_provider): _OpenTelemetrySemanticConventionStability._initialized = False os.environ.update( { OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "event_only", OTEL_SEMCONV_STABILITY_OPT_IN: "gen_ai_latest_experimental", } ) instrumentor = OpenAIInstrumentor() instrumentor.instrument( tracer_provider=tracer_provider, logger_provider=logger_provider, meter_provider=meter_provider, ) yield instrumentor os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None) os.environ.pop(OTEL_SEMCONV_STABILITY_OPT_IN, None) instrumentor.uninstrument() class LiteralBlockScalar(str): """Formats the string as a literal block scalar, preserving whitespace and without interpreting escape characters""" def literal_block_scalar_presenter(dumper, data): """Represents a scalar string as a literal block, via '|' syntax""" return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|") yaml.add_representer(LiteralBlockScalar, literal_block_scalar_presenter) def process_string_value(string_value): """Pretty-prints JSON or returns long strings as a LiteralBlockScalar""" try: json_data = json.loads(string_value) return LiteralBlockScalar(json.dumps(json_data, indent=2)) except (ValueError, TypeError): if len(string_value) > 80: return LiteralBlockScalar(string_value) return string_value def convert_body_to_literal(data): """Searches the data for body strings, attempting to pretty-print JSON""" if isinstance(data, dict): for key, value in data.items(): # Handle response body case (e.g., response.body.string) if key == "body" and isinstance(value, dict) and "string" in value: value["string"] = process_string_value(value["string"]) # Handle request body case (e.g., request.body) elif key == "body" and isinstance(value, str): data[key] = process_string_value(value) else: convert_body_to_literal(value) elif isinstance(data, list): for idx, choice in enumerate(data): data[idx] = convert_body_to_literal(choice) return data class PrettyPrintJSONBody: """This makes request and response body recordings more readable.""" @staticmethod def serialize(cassette_dict): cassette_dict = convert_body_to_literal(cassette_dict) return yaml.dump(cassette_dict, default_flow_style=False, allow_unicode=True) @staticmethod def deserialize(cassette_string): return yaml.load(cassette_string, Loader=yaml.Loader) @pytest.fixture(scope="module", autouse=True) def fixture_vcr(vcr): vcr.register_serializer("yaml", PrettyPrintJSONBody) return vcr def scrub_response_headers(response): """ This scrubs sensitive response headers. Note they are case-sensitive! """ response["headers"]["openai-organization"] = "test_openai_org_id" response["headers"]["Set-Cookie"] = "test_set_cookie" return response requirements.latest.txt000066400000000000000000000033651527070072300416750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests# Copyright The OpenTelemetry Authors # # 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. # ******************************** # WARNING: NOT HERMETIC !!!!!!!!!! # ******************************** # # This "requirements.txt" is installed in conjunction # with multiple other dependencies in the top-level "tox.ini" # file. In particular, please see: # # openai-latest: {[testenv]test_deps} # openai-latest: -r {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/requirements.latest.txt # # This provides additional dependencies, namely: # # opentelemetry-api # opentelemetry-sdk # opentelemetry-semantic-conventions # # ... with a "dev" version based on the latest distribution. # This variant of the requirements aims to test the system using # the newest supported version of external dependencies. openai==2.26.0 pydantic==2.12.5 httpx==0.27.2 # older jiter is required for PyPy < 3.11 jiter==0.11.1 Deprecated==1.3.1 importlib-metadata==6.11.0 packaging==24.0 pytest==7.4.4 pytest-vcr==1.0.2 pytest-asyncio==0.21.0 wrapt==2.1.2 # test with the latest version of opentelemetry-api, sdk, and semantic conventions -e opentelemetry-instrumentation -e instrumentation-genai/opentelemetry-instrumentation-openai-v2 -e util/opentelemetry-util-genairequirements.oldest.txt000066400000000000000000000025471527070072300416740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests# Copyright The OpenTelemetry Authors # # 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. # This variant of the requirements aims to test the system using # the oldest supported version of external dependencies. openai==1.26.0 numpy==2.3.1 ; python_version >= "3.11" numpy==2.1.0 ; python_version == "3.10" numpy==2.0.0 ; python_version == "3.9" pydantic==2.12.5 httpx==0.27.2 Deprecated==1.2.14 importlib-metadata==6.11.0 packaging==24.0 pytest==7.4.4 pytest-vcr==1.0.2 pytest-asyncio==0.21.0 wrapt==1.16.0 opentelemetry-exporter-otlp-proto-http~=1.30 opentelemetry-api==1.40 # when updating, also update in pyproject.toml opentelemetry-sdk==1.40 # when updating, also update in pyproject.toml opentelemetry-semantic-conventions==0.61b0 # when updating, also update in pyproject.toml -e util/opentelemetry-util-genai -e instrumentation-genai/opentelemetry-instrumentation-openai-v2 test_async_chat_completions.py000066400000000000000000001257501527070072300432620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-locals,too-many-lines from unittest.mock import AsyncMock import pytest from openai import APIConnectionError, AsyncOpenAI, NotFoundError from opentelemetry.instrumentation.openai_v2.patch import ( AsyncLegacyChatStreamWrapper, ) from opentelemetry.semconv._incubating.attributes import ( error_attributes as ErrorAttributes, ) from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, ) from opentelemetry.semconv._incubating.attributes import ( openai_attributes as OpenAIAttributes, ) from opentelemetry.semconv._incubating.attributes import ( server_attributes as ServerAttributes, ) from opentelemetry.util.genai.utils import is_experimental_mode from .test_utils import ( DEFAULT_MODEL, EXPECTED_TOOL_DEFINITIONS, USER_ONLY_EXPECTED_INPUT_MESSAGES, USER_ONLY_PROMPT, WEATHER_TOOL_EXPECTED_INPUT_MESSAGES, WEATHER_TOOL_PROMPT, assert_all_attributes, assert_message_in_logs, assert_messages_attribute, format_simple_expected_output_message, get_current_weather_tool_definition, ) @pytest.mark.asyncio() async def test_async_chat_completion_with_content( span_exporter, log_exporter, async_openai_client, instrument_with_content, vcr, ): latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_async_chat_completion_with_content.yaml"): response = await async_openai_client.chat.completions.create( messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, stream=False ) spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response.id, response.model, response.usage.prompt_tokens, response.usage.completion_tokens, ) if latest_experimental_enabled: assert_messages_attribute( spans[0].attributes["gen_ai.input.messages"], USER_ONLY_EXPECTED_INPUT_MESSAGES, ) assert_messages_attribute( spans[0].attributes["gen_ai.output.messages"], format_simple_expected_output_message(response.choices[0].message.content), ) else: logs = log_exporter.get_finished_logs() assert len(logs) == 2 user_message = {"content": USER_ONLY_PROMPT[0]["content"]} assert_message_in_logs(logs[0], "gen_ai.user.message", user_message, spans[0]) choice_event = { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": response.choices[0].message.content, }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, spans[0]) @pytest.mark.asyncio() async def test_async_chat_completion_no_content( span_exporter, log_exporter, async_openai_client, instrument_no_content, vcr, ): latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_async_chat_completion_no_content.yaml"): response = await async_openai_client.chat.completions.create( messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, stream=False ) spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response.id, response.model, response.usage.prompt_tokens, response.usage.completion_tokens, ) logs = log_exporter.get_finished_logs() if latest_experimental_enabled: assert len(logs) == 0 assert "gen_ai.input.messages" not in spans[0].attributes assert "gen_ai.output.messages" not in spans[0].attributes else: assert len(logs) == 2 assert_message_in_logs(logs[0], "gen_ai.user.message", None, spans[0]) choice_event = { "index": 0, "finish_reason": "stop", "message": {"role": "assistant"}, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, spans[0]) @pytest.mark.asyncio() async def test_async_chat_completion_bad_endpoint(span_exporter, instrument_no_content): latest_experimental_enabled = is_experimental_mode() client = AsyncOpenAI(base_url="http://localhost:4242") with pytest.raises(APIConnectionError): await client.chat.completions.create( messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, timeout=0.1, ) spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, server_address="localhost", ) assert 4242 == spans[0].attributes[ServerAttributes.SERVER_PORT] assert "APIConnectionError" == spans[0].attributes[ErrorAttributes.ERROR_TYPE] @pytest.mark.asyncio() async def test_async_chat_completion_404(span_exporter, async_openai_client, instrument_no_content, vcr): latest_experimental_enabled = is_experimental_mode() llm_model_value = "this-model-does-not-exist" with vcr.use_cassette("test_async_chat_completion_404.yaml"): with pytest.raises(NotFoundError): await async_openai_client.chat.completions.create( messages=USER_ONLY_PROMPT, model=llm_model_value, ) spans = span_exporter.get_finished_spans() assert_all_attributes(spans[0], llm_model_value, latest_experimental_enabled) assert "NotFoundError" == spans[0].attributes[ErrorAttributes.ERROR_TYPE] @pytest.mark.asyncio() async def test_async_chat_completion_api_exception_propagates(async_openai_client, instrument_no_content, vcr): with vcr.use_cassette("test_async_chat_completion_404.yaml"): with pytest.raises(NotFoundError): await async_openai_client.chat.completions.create( messages=USER_ONLY_PROMPT, model="this-model-does-not-exist", ) @pytest.mark.asyncio() async def test_async_chat_completion_extra_params(span_exporter, async_openai_client, instrument_no_content, vcr): latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_async_chat_completion_extra_params.yaml"): response = await async_openai_client.chat.completions.create( messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, seed=42, temperature=0.5, max_tokens=50, stream=False, extra_body={"service_tier": "default"}, response_format={"type": "text"}, ) spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response.id, response.model, response.usage.prompt_tokens, response.usage.completion_tokens, request_service_tier="default", response_service_tier=getattr(response, "service_tier", None), ) assert spans[0].attributes[GenAIAttributes.GEN_AI_REQUEST_SEED] == 42 assert spans[0].attributes[GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE] == 0.5 assert spans[0].attributes[GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS] == 50 request_service_tier_attr_key = ( OpenAIAttributes.OPENAI_REQUEST_SERVICE_TIER if latest_experimental_enabled else GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER ) assert spans[0].attributes[request_service_tier_attr_key] == "default" output_type_attr_key = ( GenAIAttributes.GEN_AI_OUTPUT_TYPE if latest_experimental_enabled else GenAIAttributes.GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT ) assert spans[0].attributes[output_type_attr_key] == "text" @pytest.mark.asyncio() async def test_async_chat_completion_multiple_choices( span_exporter, log_exporter, async_openai_client, instrument_with_content, vcr, ): latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_async_chat_completion_multiple_choices.yaml"): response = await async_openai_client.chat.completions.create( messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, n=2, stream=False ) spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response.id, response.model, response.usage.prompt_tokens, response.usage.completion_tokens, ) logs = log_exporter.get_finished_logs() if latest_experimental_enabled: expected_output_messages = [ { "role": "assistant", "parts": [ { "type": "text", "content": response.choices[0].message.content, } ], "finish_reason": "stop", }, { "role": "assistant", "parts": [ { "type": "text", "content": response.choices[1].message.content, } ], "finish_reason": "stop", }, ] assert_messages_attribute( spans[0].attributes["gen_ai.output.messages"], expected_output_messages, ) else: assert len(logs) == 3 # 1 user message + 2 choice messages user_message = {"content": USER_ONLY_PROMPT[0]["content"]} assert_message_in_logs(logs[0], "gen_ai.user.message", user_message, spans[0]) choice_event_0 = { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": response.choices[0].message.content, }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_event_0, spans[0]) choice_event_1 = { "index": 1, "finish_reason": "stop", "message": { "role": "assistant", "content": response.choices[1].message.content, }, } assert_message_in_logs(logs[2], "gen_ai.choice", choice_event_1, spans[0]) @pytest.mark.asyncio() async def test_async_chat_completion_with_raw_response( span_exporter, log_exporter, async_openai_client, instrument_with_content, vcr, ): latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_async_chat_completion_with_raw_response.yaml"): response = await async_openai_client.chat.completions.with_raw_response.create( messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, ) response = response.parse() spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response.id, response.model, response.usage.prompt_tokens, response.usage.completion_tokens, ) logs = log_exporter.get_finished_logs() if latest_experimental_enabled: assert_messages_attribute( spans[0].attributes["gen_ai.input.messages"], USER_ONLY_EXPECTED_INPUT_MESSAGES, ) assert_messages_attribute( spans[0].attributes["gen_ai.output.messages"], format_simple_expected_output_message(response.choices[0].message.content), ) else: assert len(logs) == 2 user_message = {"content": USER_ONLY_PROMPT[0]["content"]} assert_message_in_logs(logs[0], "gen_ai.user.message", user_message, spans[0]) choice_event = { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": response.choices[0].message.content, }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, spans[0]) @pytest.mark.asyncio() async def test_chat_completion_with_raw_response_streaming( span_exporter, log_exporter, async_openai_client, instrument_with_content, vcr, ): latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_chat_completion_with_raw_response_streaming.yaml"): raw_response = await async_openai_client.chat.completions.with_raw_response.create( messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, stream=True, stream_options={"include_usage": True}, ) response = raw_response.parse() message_content = "" response_stream_usage = None response_stream_model = None response_stream_id = None async for chunk in response: if chunk.choices: message_content += chunk.choices[0].delta.content or "" # get the last chunk if getattr(chunk, "usage", None): response_stream_usage = chunk.usage response_stream_model = chunk.model response_stream_id = chunk.id spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response_stream_id, response_stream_model, response_stream_usage.prompt_tokens, response_stream_usage.completion_tokens, response_service_tier="default", ) logs = log_exporter.get_finished_logs() if latest_experimental_enabled: assert len(logs) == 0 assert_messages_attribute( spans[0].attributes["gen_ai.input.messages"], USER_ONLY_EXPECTED_INPUT_MESSAGES, ) assert_messages_attribute( spans[0].attributes["gen_ai.output.messages"], format_simple_expected_output_message(message_content), ) else: assert len(logs) == 2 user_message = {"content": USER_ONLY_PROMPT[0]["content"]} assert_message_in_logs(logs[0], "gen_ai.user.message", user_message, spans[0]) choice_event = { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": message_content, }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, spans[0]) @pytest.mark.asyncio() async def test_async_chat_completion_tool_calls_with_content( span_exporter, log_exporter, async_openai_client, instrument_with_content, vcr, ): with vcr.use_cassette("test_async_chat_completion_tool_calls_with_content.yaml"): await chat_completion_tool_call( span_exporter, log_exporter, async_openai_client, True, is_experimental_mode(), ) @pytest.mark.asyncio() async def test_async_chat_completion_tool_calls_no_content( span_exporter, log_exporter, async_openai_client, instrument_no_content, vcr, ): with vcr.use_cassette("test_async_chat_completion_tool_calls_no_content.yaml"): await chat_completion_tool_call( span_exporter, log_exporter, async_openai_client, False, is_experimental_mode(), ) async def chat_completion_tool_call( span_exporter, log_exporter, async_openai_client, expect_content, latest_experimental_enabled, ): # pylint: disable=too-many-statements messages_value = WEATHER_TOOL_PROMPT.copy() response_0 = await async_openai_client.chat.completions.create( messages=messages_value, model=DEFAULT_MODEL, tool_choice="auto", tools=[get_current_weather_tool_definition()], ) # sanity check assert "tool_calls" in response_0.choices[0].finish_reason # final request messages_value.append( { "role": "assistant", "tool_calls": response_0.choices[0].message.to_dict()["tool_calls"], } ) tool_call_result_0 = { "role": "tool", "content": "50 degrees and raining", "tool_call_id": response_0.choices[0].message.tool_calls[0].id, } tool_call_result_1 = { "role": "tool", "content": "70 degrees and sunny", "tool_call_id": response_0.choices[0].message.tool_calls[1].id, } messages_value.append(tool_call_result_0) messages_value.append(tool_call_result_1) response_1 = await async_openai_client.chat.completions.create(messages=messages_value, model=DEFAULT_MODEL) # sanity check assert "stop" in response_1.choices[0].finish_reason # validate both calls spans = span_exporter.get_finished_spans() assert len(spans) == 2 assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response_0.id, response_0.model, response_0.usage.prompt_tokens, response_0.usage.completion_tokens, ) assert_all_attributes( spans[1], DEFAULT_MODEL, latest_experimental_enabled, response_1.id, response_1.model, response_1.usage.prompt_tokens, response_1.usage.completion_tokens, ) logs = log_exporter.get_finished_logs() if latest_experimental_enabled: if not expect_content: pass else: # first call assert_messages_attribute( spans[0].attributes["gen_ai.input.messages"], WEATHER_TOOL_EXPECTED_INPUT_MESSAGES, ) first_output = [ { "role": "assistant", "parts": [ { "type": "tool_call", "id": response_0.choices[0].message.tool_calls[0].id, "name": "get_current_weather", "arguments": {"location": "Seattle, WA"}, }, { "type": "tool_call", "id": response_0.choices[0].message.tool_calls[1].id, "name": "get_current_weather", "arguments": {"location": "San Francisco, CA"}, }, ], "finish_reason": "tool_calls", } ] assert_messages_attribute(spans[0].attributes["gen_ai.output.messages"], first_output) assert_messages_attribute( spans[0].attributes["gen_ai.tool.definitions"], EXPECTED_TOOL_DEFINITIONS, ) assert "gen_ai.tool.definitions" not in spans[1].attributes # second call del first_output[0]["finish_reason"] second_input = [] second_input += WEATHER_TOOL_EXPECTED_INPUT_MESSAGES.copy() second_input += first_output second_input += [ { "role": "tool", "parts": [ { "type": "tool_call_response", "id": response_0.choices[0].message.tool_calls[0].id, "response": tool_call_result_0["content"], } ], }, { "role": "tool", "parts": [ { "type": "tool_call_response", "id": response_0.choices[0].message.tool_calls[1].id, "response": tool_call_result_1["content"], } ], }, ] assert_messages_attribute(spans[1].attributes["gen_ai.input.messages"], second_input) assert_messages_attribute( spans[1].attributes["gen_ai.output.messages"], [ { "role": "assistant", "parts": [ { "type": "text", "content": response_1.choices[0].message.content, }, ], "finish_reason": "stop", } ], ) else: assert len(logs) == 9 # 3 logs for first completion, 6 for second # call one system_message = {"content": messages_value[0]["content"]} if expect_content else None assert_message_in_logs(logs[0], "gen_ai.system.message", system_message, spans[0]) user_message = {"content": messages_value[1]["content"]} if expect_content else None assert_message_in_logs(logs[1], "gen_ai.user.message", user_message, spans[0]) function_call_0 = {"name": "get_current_weather"} function_call_1 = {"name": "get_current_weather"} if expect_content: function_call_0["arguments"] = ( response_0.choices[0].message.tool_calls[0].function.arguments.replace("\n", "") ) function_call_1["arguments"] = ( response_0.choices[0].message.tool_calls[1].function.arguments.replace("\n", "") ) choice_event = { "index": 0, "finish_reason": "tool_calls", "message": { "role": "assistant", "tool_calls": [ { "id": response_0.choices[0].message.tool_calls[0].id, "type": "function", "function": function_call_0, }, { "id": response_0.choices[0].message.tool_calls[1].id, "type": "function", "function": function_call_1, }, ], }, } assert_message_in_logs(logs[2], "gen_ai.choice", choice_event, spans[0]) # call two system_message = {"content": messages_value[0]["content"]} if expect_content else None assert_message_in_logs(logs[3], "gen_ai.system.message", system_message, spans[1]) user_message = {"content": messages_value[1]["content"]} if expect_content else None assert_message_in_logs(logs[4], "gen_ai.user.message", user_message, spans[1]) assistant_tool_call = {"tool_calls": messages_value[2]["tool_calls"]} if not expect_content: assistant_tool_call["tool_calls"][0]["function"]["arguments"] = None assistant_tool_call["tool_calls"][1]["function"]["arguments"] = None assert_message_in_logs(logs[5], "gen_ai.assistant.message", assistant_tool_call, spans[1]) tool_message_0 = { "id": tool_call_result_0["tool_call_id"], "content": tool_call_result_0["content"] if expect_content else None, } assert_message_in_logs(logs[6], "gen_ai.tool.message", tool_message_0, spans[1]) tool_message_1 = { "id": tool_call_result_1["tool_call_id"], "content": tool_call_result_1["content"] if expect_content else None, } assert_message_in_logs(logs[7], "gen_ai.tool.message", tool_message_1, spans[1]) message = { "role": "assistant", "content": response_1.choices[0].message.content if expect_content else None, } choice = { "index": 0, "finish_reason": "stop", "message": message, } assert_message_in_logs(logs[8], "gen_ai.choice", choice, spans[1]) @pytest.mark.asyncio() async def test_async_chat_completion_streaming( span_exporter, log_exporter, async_openai_client, instrument_with_content, vcr, ): latest_experimental_enabled = is_experimental_mode() llm_model_value = "gpt-4" kwargs = { "model": llm_model_value, "messages": USER_ONLY_PROMPT, "stream": True, "stream_options": {"include_usage": True}, } response_stream_usage = None response_stream_model = None response_stream_id = None response_stream_result = "" with vcr.use_cassette("test_async_chat_completion_streaming.yaml"): response = await async_openai_client.chat.completions.create(**kwargs) async for chunk in response: if chunk.choices: response_stream_result += chunk.choices[0].delta.content or "" # get the last chunk if getattr(chunk, "usage", None): response_stream_usage = chunk.usage response_stream_model = chunk.model response_stream_id = chunk.id spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], llm_model_value, latest_experimental_enabled, response_stream_id, response_stream_model, response_stream_usage.prompt_tokens, response_stream_usage.completion_tokens, ) logs = log_exporter.get_finished_logs() if latest_experimental_enabled: assert_messages_attribute( spans[0].attributes["gen_ai.input.messages"], USER_ONLY_EXPECTED_INPUT_MESSAGES, ) assert_messages_attribute( spans[0].attributes["gen_ai.output.messages"], format_simple_expected_output_message(response_stream_result), ) else: assert len(logs) == 2 user_message = {"content": "Say this is a test"} assert_message_in_logs(logs[0], "gen_ai.user.message", user_message, spans[0]) choice_event = { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": response_stream_result, }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, spans[0]) @pytest.mark.asyncio() async def test_async_chat_completion_streaming_user_exception_propagates( span_exporter, async_openai_client, instrument_with_content, vcr, ): latest_experimental_enabled = is_experimental_mode() llm_model_value = "gpt-4" kwargs = { "model": llm_model_value, "messages": USER_ONLY_PROMPT, "stream": True, "stream_options": {"include_usage": True}, } response_stream_model = None response_stream_id = None with vcr.use_cassette("test_async_chat_completion_streaming.yaml"): response = await async_openai_client.chat.completions.create(**kwargs) with pytest.raises(RuntimeError, match="user failure"): async with response: async for chunk in response: response_stream_model = chunk.model response_stream_id = chunk.id raise RuntimeError("user failure") spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], llm_model_value, latest_experimental_enabled, response_stream_id, response_stream_model, ) assert "RuntimeError" == spans[0].attributes[ErrorAttributes.ERROR_TYPE] @pytest.mark.asyncio() async def test_async_chat_completion_streaming_not_complete( span_exporter, log_exporter, async_openai_client, instrument_with_content, vcr, ): latest_experimental_enabled = is_experimental_mode() kwargs = { "model": DEFAULT_MODEL, "messages": USER_ONLY_PROMPT, "stream": True, } response_stream_model = None response_stream_id = None response_stream_result = "" with vcr.use_cassette("test_async_chat_completion_streaming_not_complete.yaml"): response = await async_openai_client.chat.completions.create(**kwargs) idx = 0 async for chunk in response: if chunk.choices: response_stream_result += chunk.choices[0].delta.content or "" if idx == 1: # fake a stop break if chunk.model: response_stream_model = chunk.model if chunk.id: response_stream_id = chunk.id idx += 1 await response.close() spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response_stream_id, response_stream_model, ) logs = log_exporter.get_finished_logs() if latest_experimental_enabled: assert_messages_attribute( spans[0].attributes["gen_ai.input.messages"], USER_ONLY_EXPECTED_INPUT_MESSAGES, ) assert_messages_attribute( spans[0].attributes["gen_ai.output.messages"], format_simple_expected_output_message(response_stream_result, finish_reason="error"), ) else: assert len(logs) == 2 user_message = {"content": "Say this is a test"} assert_message_in_logs(logs[0], "gen_ai.user.message", user_message, spans[0]) choice_event = { "index": 0, "finish_reason": "error", "message": { "role": "assistant", "content": response_stream_result, }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, spans[0]) @pytest.mark.asyncio() async def test_async_chat_completion_streaming_close_awaits_underlying_stream( span_exporter, async_openai_client, instrument_with_content, vcr, ): # Closing the legacy async stream wrapper must await the underlying # AsyncStream.close so the httpx response/connection is released instead of # leaking. # https://github.com/open-telemetry/opentelemetry-python-contrib/issues/4710 if is_experimental_mode(): pytest.skip("legacy stream wrapper only") kwargs = { "model": DEFAULT_MODEL, "messages": USER_ONLY_PROMPT, "stream": True, } with vcr.use_cassette("test_async_chat_completion_streaming_not_complete.yaml"): response = await async_openai_client.chat.completions.create(**kwargs) assert isinstance(response, AsyncLegacyChatStreamWrapper) async for _ in response: # consume one chunk, then exit early (mirrors cancellation) break # Wrap the real close so the underlying connection is still released # while asserting it was awaited. underlying_close = AsyncMock(wraps=response.stream.close) response.stream.close = underlying_close # close() must be awaitable and must await the underlying stream close. await response.close() underlying_close.assert_awaited_once() spans = span_exporter.get_finished_spans() assert len(spans) == 1 @pytest.mark.asyncio() async def test_async_chat_completion_streaming_context_manager_awaits_close( span_exporter, async_openai_client, instrument_with_content, vcr, ): # Exiting the legacy async stream wrapper as an async context manager must # await the underlying AsyncStream.close so the httpx response/connection is # released instead of leaking. # https://github.com/open-telemetry/opentelemetry-python-contrib/issues/4710 if is_experimental_mode(): pytest.skip("legacy stream wrapper only") kwargs = { "model": DEFAULT_MODEL, "messages": USER_ONLY_PROMPT, "stream": True, } with vcr.use_cassette("test_async_chat_completion_streaming_not_complete.yaml"): response = await async_openai_client.chat.completions.create(**kwargs) assert isinstance(response, AsyncLegacyChatStreamWrapper) # Wrap the real close so the underlying connection is still released # while asserting it was awaited. underlying_close = AsyncMock(wraps=response.stream.close) response.stream.close = underlying_close async with response: async for _ in response: # consume one chunk, then exit early break underlying_close.assert_awaited_once() spans = span_exporter.get_finished_spans() assert len(spans) == 1 @pytest.mark.asyncio() async def test_async_chat_completion_multiple_choices_streaming( span_exporter, log_exporter, async_openai_client, instrument_with_content, vcr, ): latest_experimental_enabled = is_experimental_mode() messages_value = WEATHER_TOOL_PROMPT.copy() with vcr.use_cassette("test_async_chat_completion_multiple_choices_streaming.yaml"): response_0 = await async_openai_client.chat.completions.create( messages=messages_value, model=DEFAULT_MODEL, n=2, stream=True, stream_options={"include_usage": True}, ) response_stream_usage = None response_stream_model = None response_stream_id = None # two strings for each choice response_stream_result = ["", ""] finish_reasons = ["", ""] async for chunk in response_0: if chunk.choices: for choice in chunk.choices: response_stream_result[choice.index] += choice.delta.content or "" if choice.finish_reason: finish_reasons[choice.index] = choice.finish_reason # get the last chunk if getattr(chunk, "usage", None): response_stream_usage = chunk.usage response_stream_model = chunk.model response_stream_id = chunk.id # sanity check assert "stop" == finish_reasons[0] spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response_stream_id, response_stream_model, response_stream_usage.prompt_tokens, response_stream_usage.completion_tokens, ) logs = log_exporter.get_finished_logs() if latest_experimental_enabled: expected_output_messages = [ { "role": "assistant", "parts": [ { "type": "text", "content": "".join(response_stream_result[0]), } ], "finish_reason": "stop", }, { "role": "assistant", "parts": [ { "type": "text", "content": "".join(response_stream_result[1]), } ], "finish_reason": "stop", }, ] assert_messages_attribute( spans[0].attributes["gen_ai.input.messages"], WEATHER_TOOL_EXPECTED_INPUT_MESSAGES, ) assert_messages_attribute( spans[0].attributes["gen_ai.output.messages"], expected_output_messages, ) else: assert len(logs) == 4 system_message = {"content": messages_value[0]["content"]} assert_message_in_logs(logs[0], "gen_ai.system.message", system_message, spans[0]) user_message = {"content": "What's the weather in Seattle and San Francisco today?"} assert_message_in_logs(logs[1], "gen_ai.user.message", user_message, spans[0]) choice_event_0 = { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": "".join(response_stream_result[0]), }, } assert_message_in_logs(logs[2], "gen_ai.choice", choice_event_0, spans[0]) choice_event_1 = { "index": 1, "finish_reason": "stop", "message": { "role": "assistant", "content": "".join(response_stream_result[1]), }, } assert_message_in_logs(logs[3], "gen_ai.choice", choice_event_1, spans[0]) @pytest.mark.asyncio() async def test_async_chat_completion_multiple_tools_streaming_with_content( span_exporter, log_exporter, async_openai_client, instrument_with_content, vcr, ): with vcr.use_cassette("test_async_chat_completion_multiple_tools_streaming_with_content.yaml"): await async_chat_completion_multiple_tools_streaming( span_exporter, log_exporter, async_openai_client, True, is_experimental_mode(), ) @pytest.mark.asyncio() async def test_async_chat_completion_multiple_tools_streaming_no_content( span_exporter, log_exporter, async_openai_client, instrument_no_content, vcr, ): with vcr.use_cassette("test_async_chat_completion_multiple_tools_streaming_no_content.yaml"): await async_chat_completion_multiple_tools_streaming( span_exporter, log_exporter, async_openai_client, False, is_experimental_mode(), ) @pytest.mark.asyncio() async def test_async_chat_completion_streaming_unsampled( span_exporter, log_exporter, async_openai_client, instrument_with_content_unsampled, vcr, ): latest_experimental_enabled = is_experimental_mode() kwargs = { "model": DEFAULT_MODEL, "messages": USER_ONLY_PROMPT, "stream": True, "stream_options": {"include_usage": True}, } response_stream_result = "" with vcr.use_cassette("test_async_chat_completion_streaming_unsampled.yaml"): response = await async_openai_client.chat.completions.create(**kwargs) async for chunk in response: if chunk.choices: response_stream_result += chunk.choices[0].delta.content or "" spans = span_exporter.get_finished_spans() assert len(spans) == 0 logs = log_exporter.get_finished_logs() if not latest_experimental_enabled: assert len(logs) == 2 user_message = {"content": "Say this is a test"} assert_message_in_logs(logs[0], "gen_ai.user.message", user_message, None) choice_event = { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": response_stream_result, }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, None) assert logs[0].log_record.trace_id is not None assert logs[0].log_record.span_id is not None assert not logs[0].log_record.trace_flags.sampled assert logs[0].log_record.trace_id == logs[1].log_record.trace_id assert logs[0].log_record.span_id == logs[1].log_record.span_id assert logs[0].log_record.trace_flags == logs[1].log_record.trace_flags async def async_chat_completion_multiple_tools_streaming( span_exporter, log_exporter, async_openai_client, expect_content, latest_experimental_enabled, ): messages_value = WEATHER_TOOL_PROMPT.copy() response = await async_openai_client.chat.completions.create( messages=messages_value, model=DEFAULT_MODEL, tool_choice="auto", tools=[get_current_weather_tool_definition()], stream=True, stream_options={"include_usage": True}, ) finish_reason = None response_stream_usage = None response_stream_model = None response_stream_id = None # two tools tool_names = ["", ""] tool_call_ids = ["", ""] tool_args = ["", ""] async for chunk in response: if chunk.choices: if chunk.choices[0].finish_reason: finish_reason = chunk.choices[0].finish_reason for tool_call in chunk.choices[0].delta.tool_calls or []: t_idx = tool_call.index if tool_call.id: tool_call_ids[t_idx] = tool_call.id if tool_call.function: if tool_call.function.arguments: tool_args[t_idx] += tool_call.function.arguments if tool_call.function.name: tool_names[t_idx] = tool_call.function.name # get the last chunk if getattr(chunk, "usage", None): response_stream_usage = chunk.usage response_stream_model = chunk.model response_stream_id = chunk.id # sanity check assert "tool_calls" == finish_reason spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response_stream_id, response_stream_model, response_stream_usage.prompt_tokens, response_stream_usage.completion_tokens, ) logs = log_exporter.get_finished_logs() if latest_experimental_enabled: if expect_content: # first call assert_messages_attribute( spans[0].attributes["gen_ai.input.messages"], WEATHER_TOOL_EXPECTED_INPUT_MESSAGES, ) first_output = [ { "role": "assistant", "parts": [ { "type": "tool_call", "id": tool_call_ids[0], "name": "get_current_weather", "arguments": {"location": "Seattle, WA"}, }, { "type": "tool_call", "id": tool_call_ids[1], "name": "get_current_weather", "arguments": {"location": "San Francisco, CA"}, }, ], "finish_reason": "tool_calls", } ] assert_messages_attribute(spans[0].attributes["gen_ai.output.messages"], first_output) assert_messages_attribute( spans[0].attributes["gen_ai.tool.definitions"], EXPECTED_TOOL_DEFINITIONS, ) else: assert len(logs) == 3 system_message = {"content": messages_value[0]["content"]} if expect_content else None assert_message_in_logs(logs[0], "gen_ai.system.message", system_message, spans[0]) user_message = {"content": "What's the weather in Seattle and San Francisco today?"} if expect_content else None assert_message_in_logs(logs[1], "gen_ai.user.message", user_message, spans[0]) choice_event = { "index": 0, "finish_reason": "tool_calls", "message": { "role": "assistant", "tool_calls": [ { "id": tool_call_ids[0], "type": "function", "function": { "name": tool_names[0], "arguments": (tool_args[0].replace("\n", "") if expect_content else None), }, }, { "id": tool_call_ids[1], "type": "function", "function": { "name": tool_names[1], "arguments": (tool_args[1].replace("\n", "") if expect_content else None), }, }, ], }, } assert_message_in_logs(logs[2], "gen_ai.choice", choice_event, spans[0]) test_async_embeddings.py000066400000000000000000000167221527070072300420260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Unit tests for OpenAI Async Embeddings API instrumentation.""" import pytest from openai import AsyncOpenAI, NotFoundError from opentelemetry.semconv._incubating.attributes import ( error_attributes as ErrorAttributes, ) from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, ) from opentelemetry.semconv._incubating.metrics import gen_ai_metrics from opentelemetry.util.genai.utils import is_experimental_mode from .test_utils import ( DEFAULT_EMBEDDING_MODEL, assert_all_attributes, assert_embedding_attributes, ) @pytest.mark.asyncio async def test_async_embeddings_no_content( span_exporter, log_exporter, async_openai_client, instrument_no_content, vcr, ): """Test creating embeddings asynchronously with content capture disabled""" latest_experimental_enabled = is_experimental_mode() input_text = "This is a test for async embeddings" with vcr.use_cassette("test_async_embeddings_no_content.yaml"): response = await async_openai_client.embeddings.create( model=DEFAULT_EMBEDDING_MODEL, input=input_text, ) # Verify spans spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert_embedding_attributes( spans[0], DEFAULT_EMBEDDING_MODEL, latest_experimental_enabled, response, ) # No logs should be emitted when content capture is disabled logs = log_exporter.get_finished_logs() assert len(logs) == 0 @pytest.mark.asyncio async def test_async_embeddings_with_dimensions( span_exporter, metric_reader, async_openai_client, instrument_no_content, vcr, ): """Test creating embeddings asynchronously with custom dimensions""" latest_experimental_enabled = is_experimental_mode() input_text = "This is a test for async embeddings with dimensions" dimensions = 512 # Using a smaller dimension than default with vcr.use_cassette("test_async_embeddings_with_dimensions.yaml"): response = await async_openai_client.embeddings.create( model=DEFAULT_EMBEDDING_MODEL, input=input_text, dimensions=dimensions, ) # Verify spans spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert_embedding_attributes( spans[0], DEFAULT_EMBEDDING_MODEL, latest_experimental_enabled, response, ) # Verify dimensions attribute is set correctly assert spans[0].attributes["gen_ai.embeddings.dimension.count"] == dimensions # Verify actual embedding dimensions match the requested dimensions assert len(response.data[0].embedding) == dimensions @pytest.mark.asyncio async def test_async_embeddings_with_batch_input( span_exporter, metric_reader, async_openai_client, instrument_with_content, vcr, ): """Test creating embeddings asynchronously with batch input""" latest_experimental_enabled = is_experimental_mode() input_texts = [ "This is the first test string for async embeddings", "This is the second test string for async embeddings", "This is the third test string for async embeddings", ] with vcr.use_cassette("test_async_embeddings_with_batch_input.yaml"): response = await async_openai_client.embeddings.create( model=DEFAULT_EMBEDDING_MODEL, input=input_texts, ) # Verify spans spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert_embedding_attributes( spans[0], DEFAULT_EMBEDDING_MODEL, latest_experimental_enabled, response, ) # Verify results contain the same number of embeddings as input texts assert len(response.data) == len(input_texts) @pytest.mark.asyncio async def test_async_embeddings_error_handling(span_exporter, metric_reader, instrument_no_content, vcr): """Test async embeddings error handling""" latest_experimental_enabled = is_experimental_mode() model_name = "non-existent-embedding-model" input_text = "This is a test for async embeddings with error" client = AsyncOpenAI() with vcr.use_cassette("test_async_embeddings_error_handling.yaml"): with pytest.raises(NotFoundError): await client.embeddings.create( model=model_name, input=input_text, ) # Verify spans spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert_all_attributes( spans[0], model_name, latest_experimental_enabled, operation_name="embeddings", ) assert "NotFoundError" == spans[0].attributes[ErrorAttributes.ERROR_TYPE] @pytest.mark.asyncio @pytest.mark.vcr() async def test_async_embeddings_token_metrics( span_exporter, metric_reader, async_openai_client, instrument_no_content, vcr, ): """Test that token usage metrics are correctly recorded for async embeddings""" latest_experimental_enabled = is_experimental_mode() input_text = "This is a test for async embeddings token metrics" with vcr.use_cassette("test_async_embeddings_token_metrics.yaml"): response = await async_openai_client.embeddings.create( model=DEFAULT_EMBEDDING_MODEL, input=input_text, ) # Verify spans spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert_embedding_attributes( spans[0], DEFAULT_EMBEDDING_MODEL, latest_experimental_enabled, response, ) # Verify metrics metrics = metric_reader.get_metrics_data().resource_metrics assert len(metrics) == 1 metric_data = metrics[0].scope_metrics[0].metrics # Verify token usage metric token_metric = next( (m for m in metric_data if m.name == gen_ai_metrics.GEN_AI_CLIENT_TOKEN_USAGE), None, ) assert token_metric is not None # Find the input token data point input_token_point = None for point in token_metric.data.data_points: if point.attributes[GenAIAttributes.GEN_AI_TOKEN_TYPE] == GenAIAttributes.GenAiTokenTypeValues.INPUT.value: input_token_point = point break assert input_token_point is not None, "Input token metric not found" # Verify the token counts match what was reported in the response assert input_token_point.sum == response.usage.prompt_tokens @pytest.mark.asyncio async def test_async_embeddings_with_encoding_format( span_exporter, metric_reader, async_openai_client, instrument_no_content, vcr, ): """Test creating embeddings with different encoding format""" latest_experimental_enabled = is_experimental_mode() input_text = "This is a test for embeddings with encoding format" encoding_format = "base64" with vcr.use_cassette("test_async_embeddings_with_encoding_format.yaml"): response = await async_openai_client.embeddings.create( model=DEFAULT_EMBEDDING_MODEL, input=input_text, encoding_format=encoding_format, ) # Verify spans spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert_embedding_attributes( spans[0], DEFAULT_EMBEDDING_MODEL, latest_experimental_enabled, response, ) # Verify encoding_format attribute is set correctly assert spans[0].attributes["gen_ai.request.encoding_formats"] == (encoding_format,) test_chat_completions.py000066400000000000000000001371571527070072300420710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-locals,too-many-lines import logging import os import pytest from openai import ( NOT_GIVEN, APIConnectionError, NotFoundError, OpenAI, ) try: from openai import not_given # pylint: disable=no-name-in-module except ImportError: not_given = NOT_GIVEN from opentelemetry.semconv._incubating.attributes import ( error_attributes as ErrorAttributes, ) from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, ) from opentelemetry.semconv._incubating.attributes import ( openai_attributes as OpenAIAttributes, ) from opentelemetry.semconv._incubating.attributes import ( server_attributes as ServerAttributes, ) from opentelemetry.semconv._incubating.metrics import gen_ai_metrics from opentelemetry.util.genai.utils import is_experimental_mode from .test_utils import ( DEFAULT_MODEL, EXPECTED_TOOL_DEFINITIONS, USER_ONLY_EXPECTED_INPUT_MESSAGES, USER_ONLY_PROMPT, WEATHER_TOOL_EXPECTED_INPUT_MESSAGES, WEATHER_TOOL_PROMPT, assert_all_attributes, assert_message_in_logs, assert_messages_attribute, format_simple_expected_output_message, get_current_weather_tool_definition, ) def test_chat_completion_with_content(span_exporter, log_exporter, openai_client, instrument_with_content, vcr): latest_experimental_enabled = is_experimental_mode() print( f"latest_experimental_enabled={latest_experimental_enabled}, ENV_VAR={os.getenv('OTEL_SEMCONV_STABILITY_OPT_IN')}" ) with vcr.use_cassette("test_chat_completion_with_content.yaml"): response = openai_client.chat.completions.create( messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, stream=False, ) spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response.id, response.model, response.usage.prompt_tokens, response.usage.completion_tokens, ) if latest_experimental_enabled: assert_messages_attribute( spans[0].attributes["gen_ai.input.messages"], USER_ONLY_EXPECTED_INPUT_MESSAGES, ) assert_messages_attribute( spans[0].attributes["gen_ai.output.messages"], format_simple_expected_output_message(response.choices[0].message.content), ) else: logs = log_exporter.get_finished_logs() assert len(logs) == 2 user_message = {"content": USER_ONLY_PROMPT[0]["content"]} assert_message_in_logs(logs[0], "gen_ai.user.message", user_message, spans[0]) choice_event = { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": response.choices[0].message.content, }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, spans[0]) def test_chat_completion_handles_not_given( span_exporter, log_exporter, openai_client, instrument_no_content, vcr, caplog, ): caplog.set_level(logging.WARNING) latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_chat_completion_handles_not_given.yaml"): response = openai_client.chat.completions.create( messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, stream=False, top_p=NOT_GIVEN, max_tokens=not_given, ) (span,) = span_exporter.get_finished_spans() assert_all_attributes( span, DEFAULT_MODEL, latest_experimental_enabled, response.id, response.model, response.usage.prompt_tokens, response.usage.completion_tokens, response_service_tier="default", ) assert GenAIAttributes.GEN_AI_REQUEST_TOP_P not in span.attributes assert GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS not in span.attributes assert_no_invalid_type_warning(caplog) def test_chat_completion_no_content(span_exporter, log_exporter, openai_client, instrument_no_content, vcr): latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_chat_completion_no_content.yaml"): response = openai_client.chat.completions.create(messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, stream=False) spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response.id, response.model, response.usage.prompt_tokens, response.usage.completion_tokens, ) logs = log_exporter.get_finished_logs() if latest_experimental_enabled: assert len(logs) == 0 assert "gen_ai.input.messages" not in spans[0].attributes assert "gen_ai.output.messages" not in spans[0].attributes else: assert len(logs) == 2 assert_message_in_logs(logs[0], "gen_ai.user.message", None, spans[0]) choice_event = { "index": 0, "finish_reason": "stop", "message": {"role": "assistant"}, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, spans[0]) def test_chat_completion_bad_endpoint(span_exporter, metric_reader, instrument_no_content, vcr): latest_experimental_enabled = is_experimental_mode() client = OpenAI(base_url="http://localhost:4242") with vcr.use_cassette("test_chat_completion_bad_endpoint.yaml"): with pytest.raises(APIConnectionError): client.chat.completions.create( messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, timeout=0.1, ) spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, server_address="localhost", ) assert 4242 == spans[0].attributes[ServerAttributes.SERVER_PORT] assert "APIConnectionError" == spans[0].attributes[ErrorAttributes.ERROR_TYPE] metrics = metric_reader.get_metrics_data().resource_metrics assert len(metrics) == 1 metric_data = metrics[0].scope_metrics[0].metrics duration_metric = next( (m for m in metric_data if m.name == gen_ai_metrics.GEN_AI_CLIENT_OPERATION_DURATION), None, ) assert duration_metric is not None assert duration_metric.data.data_points[0].sum > 0 assert duration_metric.data.data_points[0].attributes[ErrorAttributes.ERROR_TYPE] == "APIConnectionError" def test_chat_completion_404(span_exporter, openai_client, metric_reader, instrument_no_content, vcr): latest_experimental_enabled = is_experimental_mode() llm_model_value = "this-model-does-not-exist" with vcr.use_cassette("test_chat_completion_404.yaml"): with pytest.raises(NotFoundError): openai_client.chat.completions.create( messages=USER_ONLY_PROMPT, model=llm_model_value, ) spans = span_exporter.get_finished_spans() assert_all_attributes(spans[0], llm_model_value, latest_experimental_enabled) assert "NotFoundError" == spans[0].attributes[ErrorAttributes.ERROR_TYPE] metrics = metric_reader.get_metrics_data().resource_metrics assert len(metrics) == 1 metric_data = metrics[0].scope_metrics[0].metrics duration_metric = next( (m for m in metric_data if m.name == gen_ai_metrics.GEN_AI_CLIENT_OPERATION_DURATION), None, ) assert duration_metric is not None assert duration_metric.data.data_points[0].sum > 0 assert duration_metric.data.data_points[0].attributes[ErrorAttributes.ERROR_TYPE] == "NotFoundError" def test_chat_completion_api_exception_propagates(openai_client, instrument_no_content, vcr): with vcr.use_cassette("test_chat_completion_404.yaml"): with pytest.raises(NotFoundError): openai_client.chat.completions.create( messages=USER_ONLY_PROMPT, model="this-model-does-not-exist", ) def test_chat_completion_extra_params(span_exporter, openai_client, instrument_no_content, vcr): latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_chat_completion_extra_params.yaml"): response = openai_client.chat.completions.create( n=2, messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, seed=42, temperature=0.5, max_tokens=50, stream=False, extra_body={"service_tier": "default"}, response_format={"type": "text"}, stop=["full", "stop"], ) spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response.id, response.model, response.usage.prompt_tokens, response.usage.completion_tokens, request_service_tier="default", response_service_tier=getattr(response, "service_tier", None), ) assert spans[0].attributes[GenAIAttributes.GEN_AI_REQUEST_SEED] == 42 assert spans[0].attributes[GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE] == 0.5 assert spans[0].attributes[GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS] == 50 request_service_tier_attr_key = ( OpenAIAttributes.OPENAI_REQUEST_SERVICE_TIER if latest_experimental_enabled else GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER ) assert spans[0].attributes[request_service_tier_attr_key] == "default" output_type_attr_key = ( GenAIAttributes.GEN_AI_OUTPUT_TYPE if latest_experimental_enabled else GenAIAttributes.GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT ) assert spans[0].attributes[output_type_attr_key] == "text" assert spans[0].attributes[GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES] == ("full", "stop") assert spans[0].attributes[GenAIAttributes.GEN_AI_REQUEST_CHOICE_COUNT] == 2 def test_chat_completion_n_1_is_not_reported(span_exporter, openai_client, instrument_no_content, vcr): latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_chat_completion_n_1_is_not_reported.yaml"): response = openai_client.chat.completions.create( n=1, messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, ) spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response.id, response.model, response.usage.prompt_tokens, response.usage.completion_tokens, response_service_tier=getattr(response, "service_tier", None), ) assert GenAIAttributes.GEN_AI_REQUEST_CHOICE_COUNT not in spans[0].attributes def test_chat_completion_handle_stop_sequences_as_string(span_exporter, openai_client, instrument_no_content, vcr): latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_chat_completion_handle_stop_sequences_as_string.yaml"): response = openai_client.chat.completions.create( messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, stop="stop", ) spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response.id, response.model, response.usage.prompt_tokens, response.usage.completion_tokens, response_service_tier=getattr(response, "service_tier", None), ) assert spans[0].attributes[GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES] == ("stop",) def test_chat_completion_multiple_choices(span_exporter, log_exporter, openai_client, instrument_with_content, vcr): latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_chat_completion_multiple_choices.yaml"): response = openai_client.chat.completions.create( messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, n=2, stream=False ) spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response.id, response.model, response.usage.prompt_tokens, response.usage.completion_tokens, ) assert spans[0].attributes[GenAIAttributes.GEN_AI_REQUEST_CHOICE_COUNT] == 2 logs = log_exporter.get_finished_logs() if latest_experimental_enabled: expected_output_messages = [ { "role": "assistant", "parts": [ { "type": "text", "content": response.choices[0].message.content, } ], "finish_reason": "stop", }, { "role": "assistant", "parts": [ { "type": "text", "content": response.choices[1].message.content, } ], "finish_reason": "stop", }, ] assert_messages_attribute( spans[0].attributes["gen_ai.input.messages"], USER_ONLY_EXPECTED_INPUT_MESSAGES, ) assert_messages_attribute( spans[0].attributes["gen_ai.output.messages"], expected_output_messages, ) else: assert len(logs) == 3 # 1 user message + 2 choice messages user_message = {"content": USER_ONLY_PROMPT[0]["content"]} assert_message_in_logs(logs[0], "gen_ai.user.message", user_message, spans[0]) choice_event_0 = { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": response.choices[0].message.content, }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_event_0, spans[0]) choice_event_1 = { "index": 1, "finish_reason": "stop", "message": { "role": "assistant", "content": response.choices[1].message.content, }, } assert_message_in_logs(logs[2], "gen_ai.choice", choice_event_1, spans[0]) def test_chat_completion_with_raw_response(span_exporter, log_exporter, openai_client, instrument_with_content, vcr): latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_chat_completion_with_raw_response.yaml"): response = openai_client.chat.completions.with_raw_response.create( messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, ) response = response.parse() spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response.id, response.model, response.usage.prompt_tokens, response.usage.completion_tokens, ) if latest_experimental_enabled: assert_messages_attribute( spans[0].attributes["gen_ai.input.messages"], USER_ONLY_EXPECTED_INPUT_MESSAGES, ) assert_messages_attribute( spans[0].attributes["gen_ai.output.messages"], format_simple_expected_output_message(response.choices[0].message.content), ) else: logs = log_exporter.get_finished_logs() assert len(logs) == 2 user_message = {"content": USER_ONLY_PROMPT[0]["content"]} assert_message_in_logs(logs[0], "gen_ai.user.message", user_message, spans[0]) choice_event = { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": response.choices[0].message.content, }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, spans[0]) def test_chat_completion_with_raw_response_streaming( span_exporter, log_exporter, openai_client, instrument_with_content, vcr ): latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_chat_completion_with_raw_response_streaming.yaml"): raw_response = openai_client.chat.completions.with_raw_response.create( messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, stream=True, stream_options={"include_usage": True}, ) response = raw_response.parse() message_content = "" response_stream_usage = None response_stream_model = None response_stream_id = None for chunk in response: if chunk.choices: message_content += chunk.choices[0].delta.content or "" # get the last chunk if getattr(chunk, "usage", None): response_stream_usage = chunk.usage response_stream_model = chunk.model response_stream_id = chunk.id spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response_stream_id, response_stream_model, response_stream_usage.prompt_tokens, response_stream_usage.completion_tokens, response_service_tier="default", ) logs = log_exporter.get_finished_logs() if latest_experimental_enabled: assert len(logs) == 0 assert_messages_attribute( spans[0].attributes["gen_ai.input.messages"], USER_ONLY_EXPECTED_INPUT_MESSAGES, ) assert_messages_attribute( spans[0].attributes["gen_ai.output.messages"], format_simple_expected_output_message(message_content), ) else: assert len(logs) == 2 user_message = {"content": USER_ONLY_PROMPT[0]["content"]} assert_message_in_logs(logs[0], "gen_ai.user.message", user_message, spans[0]) choice_event = { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": message_content, }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, spans[0]) def test_chat_completion_tool_calls_with_content( span_exporter, log_exporter, openai_client, instrument_with_content, vcr ): with vcr.use_cassette("test_chat_completion_tool_calls_with_content.yaml"): chat_completion_tool_call( span_exporter, log_exporter, openai_client, True, is_experimental_mode(), ) def test_chat_completion_tool_calls_no_content(span_exporter, log_exporter, openai_client, instrument_no_content, vcr): with vcr.use_cassette("test_chat_completion_tool_calls_no_content.yaml"): chat_completion_tool_call( span_exporter, log_exporter, openai_client, False, is_experimental_mode(), ) def chat_completion_tool_call( span_exporter, log_exporter, openai_client, expect_content, latest_experimental_enabled, ): # pylint: disable=too-many-statements messages_value = WEATHER_TOOL_PROMPT.copy() response_0 = openai_client.chat.completions.create( messages=messages_value, model=DEFAULT_MODEL, tool_choice="auto", tools=[get_current_weather_tool_definition()], ) # sanity check assert "tool_calls" in response_0.choices[0].finish_reason # final request messages_value.append( { "role": "assistant", "tool_calls": response_0.choices[0].message.to_dict()["tool_calls"], } ) tool_call_result_0 = { "role": "tool", "content": "50 degrees and raining", "tool_call_id": response_0.choices[0].message.tool_calls[0].id, } tool_call_result_1 = { "role": "tool", "content": "70 degrees and sunny", "tool_call_id": response_0.choices[0].message.tool_calls[1].id, } messages_value.append(tool_call_result_0) messages_value.append(tool_call_result_1) response_1 = openai_client.chat.completions.create(messages=messages_value, model=DEFAULT_MODEL) # sanity check assert "stop" in response_1.choices[0].finish_reason # validate both calls spans = span_exporter.get_finished_spans() assert len(spans) == 2 assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response_0.id, response_0.model, response_0.usage.prompt_tokens, response_0.usage.completion_tokens, ) assert_all_attributes( spans[1], DEFAULT_MODEL, latest_experimental_enabled, response_1.id, response_1.model, response_1.usage.prompt_tokens, response_1.usage.completion_tokens, ) logs = log_exporter.get_finished_logs() if latest_experimental_enabled: if not expect_content: pass else: # first call assert_messages_attribute( spans[0].attributes["gen_ai.input.messages"], WEATHER_TOOL_EXPECTED_INPUT_MESSAGES, ) first_output = [ { "role": "assistant", "parts": [ { "type": "tool_call", "id": response_0.choices[0].message.tool_calls[0].id, "name": "get_current_weather", "arguments": {"location": "Seattle, WA"}, }, { "type": "tool_call", "id": response_0.choices[0].message.tool_calls[1].id, "name": "get_current_weather", "arguments": {"location": "San Francisco, CA"}, }, ], "finish_reason": "tool_calls", } ] assert_messages_attribute(spans[0].attributes["gen_ai.output.messages"], first_output) assert_messages_attribute( spans[0].attributes["gen_ai.tool.definitions"], EXPECTED_TOOL_DEFINITIONS, ) assert "gen_ai.tool.definitions" not in spans[1].attributes # second call del first_output[0]["finish_reason"] second_input = [] second_input += WEATHER_TOOL_EXPECTED_INPUT_MESSAGES.copy() second_input += first_output second_input += [ { "role": "tool", "parts": [ { "type": "tool_call_response", "id": response_0.choices[0].message.tool_calls[0].id, "response": tool_call_result_0["content"], } ], }, { "role": "tool", "parts": [ { "type": "tool_call_response", "id": response_0.choices[0].message.tool_calls[1].id, "response": tool_call_result_1["content"], } ], }, ] assert_messages_attribute(spans[1].attributes["gen_ai.input.messages"], second_input) assert_messages_attribute( spans[1].attributes["gen_ai.output.messages"], format_simple_expected_output_message(response_1.choices[0].message.content), ) else: assert len(logs) == 9 # 3 logs for first completion, 6 for second # call one system_message = {"content": messages_value[0]["content"]} if expect_content else None assert_message_in_logs(logs[0], "gen_ai.system.message", system_message, spans[0]) user_message = {"content": messages_value[1]["content"]} if expect_content else None assert_message_in_logs(logs[1], "gen_ai.user.message", user_message, spans[0]) function_call_0 = {"name": "get_current_weather"} function_call_1 = {"name": "get_current_weather"} if expect_content: function_call_0["arguments"] = ( response_0.choices[0].message.tool_calls[0].function.arguments.replace("\n", "") ) function_call_1["arguments"] = ( response_0.choices[0].message.tool_calls[1].function.arguments.replace("\n", "") ) choice_event = { "index": 0, "finish_reason": "tool_calls", "message": { "role": "assistant", "tool_calls": [ { "id": response_0.choices[0].message.tool_calls[0].id, "type": "function", "function": function_call_0, }, { "id": response_0.choices[0].message.tool_calls[1].id, "type": "function", "function": function_call_1, }, ], }, } assert_message_in_logs(logs[2], "gen_ai.choice", choice_event, spans[0]) # call two system_message = {"content": messages_value[0]["content"]} if expect_content else None assert_message_in_logs(logs[3], "gen_ai.system.message", system_message, spans[1]) user_message = {"content": messages_value[1]["content"]} if expect_content else None assert_message_in_logs(logs[4], "gen_ai.user.message", user_message, spans[1]) assistant_tool_call = {"tool_calls": messages_value[2]["tool_calls"]} if not expect_content: assistant_tool_call["tool_calls"][0]["function"]["arguments"] = None assistant_tool_call["tool_calls"][1]["function"]["arguments"] = None assert_message_in_logs(logs[5], "gen_ai.assistant.message", assistant_tool_call, spans[1]) tool_message_0 = { "id": tool_call_result_0["tool_call_id"], "content": tool_call_result_0["content"] if expect_content else None, } assert_message_in_logs(logs[6], "gen_ai.tool.message", tool_message_0, spans[1]) tool_message_1 = { "id": tool_call_result_1["tool_call_id"], "content": tool_call_result_1["content"] if expect_content else None, } assert_message_in_logs(logs[7], "gen_ai.tool.message", tool_message_1, spans[1]) message = { "role": "assistant", "content": response_1.choices[0].message.content if expect_content else None, } choice = { "index": 0, "finish_reason": "stop", "message": message, } assert_message_in_logs(logs[8], "gen_ai.choice", choice, spans[1]) def test_chat_completion_streaming(span_exporter, log_exporter, openai_client, instrument_with_content, vcr): latest_experimental_enabled = is_experimental_mode() kwargs = { "model": DEFAULT_MODEL, "messages": USER_ONLY_PROMPT, "stream": True, "stream_options": {"include_usage": True}, } response_stream_usage = None response_stream_model = None response_stream_id = None response_stream_result = "" with vcr.use_cassette("test_chat_completion_streaming.yaml"): response = openai_client.chat.completions.create(**kwargs) for chunk in response: if chunk.choices: response_stream_result += chunk.choices[0].delta.content or "" # get the last chunk if getattr(chunk, "usage", None): response_stream_usage = chunk.usage response_stream_model = chunk.model response_stream_id = chunk.id spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response_stream_id, response_stream_model, response_stream_usage.prompt_tokens, response_stream_usage.completion_tokens, ) logs = log_exporter.get_finished_logs() if latest_experimental_enabled: assert_messages_attribute( spans[0].attributes["gen_ai.input.messages"], USER_ONLY_EXPECTED_INPUT_MESSAGES, ) assert_messages_attribute( spans[0].attributes["gen_ai.output.messages"], format_simple_expected_output_message(response_stream_result), ) else: assert len(logs) == 2 user_message = {"content": "Say this is a test"} assert_message_in_logs(logs[0], "gen_ai.user.message", user_message, spans[0]) choice_event = { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": response_stream_result, }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, spans[0]) def test_chat_completion_streaming_user_exception_propagates( span_exporter, openai_client, instrument_with_content, vcr ): latest_experimental_enabled = is_experimental_mode() kwargs = { "model": DEFAULT_MODEL, "messages": USER_ONLY_PROMPT, "stream": True, "stream_options": {"include_usage": True}, } response_stream_model = None response_stream_id = None with vcr.use_cassette("test_chat_completion_streaming.yaml"): response = openai_client.chat.completions.create(**kwargs) with pytest.raises(RuntimeError, match="user failure"): with response: for chunk in response: response_stream_model = chunk.model response_stream_id = chunk.id raise RuntimeError("user failure") spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response_stream_id, response_stream_model, ) assert "RuntimeError" == spans[0].attributes[ErrorAttributes.ERROR_TYPE] def test_chat_completion_streaming_user_exception_wins_over_close_exception( span_exporter, openai_client, instrument_with_content, vcr, monkeypatch ): if not is_experimental_mode(): pytest.skip("new stream wrapper only") kwargs = { "model": DEFAULT_MODEL, "messages": USER_ONLY_PROMPT, "stream": True, "stream_options": {"include_usage": True}, } with vcr.use_cassette("test_chat_completion_streaming.yaml"): response = openai_client.chat.completions.create(**kwargs) original_close = response.__wrapped__.close def close_raises(): original_close() raise RuntimeError("close failure") monkeypatch.setattr(response.__wrapped__, "close", close_raises) with pytest.raises(RuntimeError, match="user failure"): with response: raise RuntimeError("user failure") spans = span_exporter.get_finished_spans() assert "RuntimeError" == spans[0].attributes[ErrorAttributes.ERROR_TYPE] def test_chat_completion_streaming_close_exception_propagates_when_first( span_exporter, openai_client, instrument_with_content, vcr, monkeypatch ): if not is_experimental_mode(): pytest.skip("new stream wrapper only") kwargs = { "model": DEFAULT_MODEL, "messages": USER_ONLY_PROMPT, "stream": True, "stream_options": {"include_usage": True}, } with vcr.use_cassette("test_chat_completion_streaming.yaml"): response = openai_client.chat.completions.create(**kwargs) original_close = response.__wrapped__.close def close_raises(): original_close() raise RuntimeError("close failure") monkeypatch.setattr(response.__wrapped__, "close", close_raises) with pytest.raises(RuntimeError, match="close failure"): response.close() spans = span_exporter.get_finished_spans() assert "RuntimeError" == spans[0].attributes[ErrorAttributes.ERROR_TYPE] def test_chat_completion_streaming_instrumentation_finalize_errors_swallowed( span_exporter, openai_client, instrument_with_content, vcr, monkeypatch ): if not is_experimental_mode(): pytest.skip("new stream wrapper only") kwargs = { "model": DEFAULT_MODEL, "messages": USER_ONLY_PROMPT, "stream": True, "stream_options": {"include_usage": True}, } with vcr.use_cassette("test_chat_completion_streaming.yaml"): response = openai_client.chat.completions.create(**kwargs) def stop_raises(): raise RuntimeError("instrumentation failure") monkeypatch.setattr(response, "_on_stream_end", stop_raises) response.close() assert span_exporter.get_finished_spans() == () def test_chat_completion_streaming_not_complete( span_exporter, log_exporter, openai_client, instrument_with_content, vcr ): latest_experimental_enabled = is_experimental_mode() kwargs = { "model": DEFAULT_MODEL, "messages": USER_ONLY_PROMPT, "stream": True, } response_stream_model = None response_stream_id = None response_stream_result = "" with vcr.use_cassette("test_chat_completion_streaming_not_complete.yaml"): response = openai_client.chat.completions.create(**kwargs) for idx, chunk in enumerate(response): if chunk.choices: response_stream_result += chunk.choices[0].delta.content or "" if idx == 1: # fake a stop break if chunk.model: response_stream_model = chunk.model if chunk.id: response_stream_id = chunk.id response.close() spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response_stream_id, response_stream_model, ) logs = log_exporter.get_finished_logs() if latest_experimental_enabled: assert_messages_attribute( spans[0].attributes["gen_ai.input.messages"], USER_ONLY_EXPECTED_INPUT_MESSAGES, ) assert_messages_attribute( spans[0].attributes["gen_ai.output.messages"], format_simple_expected_output_message(response_stream_result, finish_reason="error"), ) else: assert len(logs) == 2 user_message = {"content": "Say this is a test"} assert_message_in_logs(logs[0], "gen_ai.user.message", user_message, spans[0]) choice_event = { "index": 0, "finish_reason": "error", "message": { "role": "assistant", "content": response_stream_result, }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, spans[0]) def test_chat_completion_multiple_choices_streaming( span_exporter, log_exporter, openai_client, instrument_with_content, vcr ): latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_chat_completion_multiple_choices_streaming.yaml"): response_0 = openai_client.chat.completions.create( messages=WEATHER_TOOL_PROMPT, model=DEFAULT_MODEL, n=2, stream=True, stream_options={"include_usage": True}, ) # two strings for each choice response_stream_result = ["", ""] finish_reasons = ["", ""] response_stream_usage = None response_stream_model = None response_stream_id = None for chunk in response_0: if chunk.choices: for choice in chunk.choices: response_stream_result[choice.index] += choice.delta.content or "" if choice.finish_reason: finish_reasons[choice.index] = choice.finish_reason # get the last chunk if getattr(chunk, "usage", None): response_stream_usage = chunk.usage response_stream_model = chunk.model response_stream_id = chunk.id # sanity check assert "stop" == finish_reasons[0] spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response_stream_id, response_stream_model, response_stream_usage.prompt_tokens, response_stream_usage.completion_tokens, ) logs = log_exporter.get_finished_logs() if latest_experimental_enabled: expected_output_messages = [ { "role": "assistant", "parts": [ { "type": "text", "content": "".join(response_stream_result[0]), } ], "finish_reason": "stop", }, { "role": "assistant", "parts": [ { "type": "text", "content": "".join(response_stream_result[1]), } ], "finish_reason": "stop", }, ] assert_messages_attribute( spans[0].attributes["gen_ai.input.messages"], WEATHER_TOOL_EXPECTED_INPUT_MESSAGES, ) assert_messages_attribute( spans[0].attributes["gen_ai.output.messages"], expected_output_messages, ) else: assert len(logs) == 4 system_message = {"content": WEATHER_TOOL_PROMPT[0]["content"]} assert_message_in_logs(logs[0], "gen_ai.system.message", system_message, spans[0]) user_message = {"content": "What's the weather in Seattle and San Francisco today?"} assert_message_in_logs(logs[1], "gen_ai.user.message", user_message, spans[0]) choice_event_0 = { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": "".join(response_stream_result[0]), }, } assert_message_in_logs(logs[2], "gen_ai.choice", choice_event_0, spans[0]) choice_event_1 = { "index": 1, "finish_reason": "stop", "message": { "role": "assistant", "content": "".join(response_stream_result[1]), }, } assert_message_in_logs(logs[3], "gen_ai.choice", choice_event_1, spans[0]) def test_chat_completion_multiple_tools_streaming_with_content( span_exporter, log_exporter, openai_client, instrument_with_content, vcr ): with vcr.use_cassette("test_chat_completion_multiple_tools_streaming_with_content.yaml"): chat_completion_multiple_tools_streaming( span_exporter, log_exporter, openai_client, True, is_experimental_mode(), ) def test_chat_completion_multiple_tools_streaming_no_content( span_exporter, log_exporter, openai_client, instrument_no_content, vcr ): with vcr.use_cassette("test_chat_completion_multiple_tools_streaming_no_content.yaml"): chat_completion_multiple_tools_streaming( span_exporter, log_exporter, openai_client, False, is_experimental_mode(), ) def test_chat_completion_with_content_span_unsampled( span_exporter, log_exporter, openai_client, instrument_with_content_unsampled, vcr, ): latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_chat_completion_with_content_span_unsampled.yaml"): response = openai_client.chat.completions.create(messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, stream=False) spans = span_exporter.get_finished_spans() assert len(spans) == 0 logs = log_exporter.get_finished_logs() if not latest_experimental_enabled: assert len(logs) == 2 user_message = {"content": USER_ONLY_PROMPT[0]["content"]} assert_message_in_logs(logs[0], "gen_ai.user.message", user_message, None) choice_event = { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": response.choices[0].message.content, }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, None) assert logs[0].log_record.trace_id is not None assert logs[0].log_record.span_id is not None assert not logs[0].log_record.trace_flags.sampled assert logs[0].log_record.trace_id == logs[1].log_record.trace_id assert logs[0].log_record.span_id == logs[1].log_record.span_id assert logs[0].log_record.trace_flags == logs[1].log_record.trace_flags def test_chat_completion_with_context_manager_streaming( span_exporter, log_exporter, openai_client, instrument_with_content, vcr ): latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_chat_completion_with_context_manager_streaming.yaml"): with openai_client.chat.completions.create( messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, stream=True, stream_options={"include_usage": True}, ) as response: message_content = "" response_stream_usage = None response_stream_model = None response_stream_id = None for chunk in response: if chunk.choices: message_content += chunk.choices[0].delta.content or "" # get the last chunk if getattr(chunk, "usage", None): response_stream_usage = chunk.usage response_stream_model = chunk.model response_stream_id = chunk.id spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response_stream_id, response_stream_model, response_stream_usage.prompt_tokens, response_stream_usage.completion_tokens, response_service_tier="default", ) logs = log_exporter.get_finished_logs() if latest_experimental_enabled: assert len(logs) == 0 assert_messages_attribute( spans[0].attributes["gen_ai.input.messages"], USER_ONLY_EXPECTED_INPUT_MESSAGES, ) assert_messages_attribute( spans[0].attributes["gen_ai.output.messages"], format_simple_expected_output_message(message_content), ) else: assert len(logs) == 2 user_message = {"content": USER_ONLY_PROMPT[0]["content"]} assert_message_in_logs(logs[0], "gen_ai.user.message", user_message, spans[0]) choice_event = { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": message_content, }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, spans[0]) def chat_completion_multiple_tools_streaming( span_exporter, log_exporter, openai_client, expect_content, latest_experimental_enabled, ): response = openai_client.chat.completions.create( messages=WEATHER_TOOL_PROMPT, model=DEFAULT_MODEL, tool_choice="auto", tools=[get_current_weather_tool_definition()], stream=True, stream_options={"include_usage": True}, ) finish_reason = None response_stream_usage = None response_stream_model = None response_stream_id = None # two tools tool_names = ["", ""] tool_call_ids = ["", ""] tool_args = ["", ""] for chunk in response: if chunk.choices: if chunk.choices[0].finish_reason: finish_reason = chunk.choices[0].finish_reason for tool_call in chunk.choices[0].delta.tool_calls or []: t_idx = tool_call.index if tool_call.id: tool_call_ids[t_idx] = tool_call.id if tool_call.function: if tool_call.function.arguments: tool_args[t_idx] += tool_call.function.arguments if tool_call.function.name: tool_names[t_idx] = tool_call.function.name # get the last chunk if getattr(chunk, "usage", None): response_stream_usage = chunk.usage response_stream_model = chunk.model response_stream_id = chunk.id # sanity check assert "tool_calls" == finish_reason spans = span_exporter.get_finished_spans() assert_all_attributes( spans[0], DEFAULT_MODEL, latest_experimental_enabled, response_stream_id, response_stream_model, response_stream_usage.prompt_tokens, response_stream_usage.completion_tokens, ) logs = log_exporter.get_finished_logs() if latest_experimental_enabled: if expect_content: # first call assert_messages_attribute( spans[0].attributes["gen_ai.input.messages"], WEATHER_TOOL_EXPECTED_INPUT_MESSAGES, ) first_output = [ { "role": "assistant", "parts": [ { "type": "tool_call", "id": tool_call_ids[0], "name": "get_current_weather", "arguments": {"location": "Seattle, WA"}, }, { "type": "tool_call", "id": tool_call_ids[1], "name": "get_current_weather", "arguments": {"location": "San Francisco, CA"}, }, ], "finish_reason": "tool_calls", } ] assert_messages_attribute(spans[0].attributes["gen_ai.output.messages"], first_output) assert_messages_attribute( spans[0].attributes["gen_ai.tool.definitions"], EXPECTED_TOOL_DEFINITIONS, ) else: assert len(logs) == 3 system_message = {"content": WEATHER_TOOL_PROMPT[0]["content"]} if expect_content else None assert_message_in_logs(logs[0], "gen_ai.system.message", system_message, spans[0]) user_message = {"content": "What's the weather in Seattle and San Francisco today?"} if expect_content else None assert_message_in_logs(logs[1], "gen_ai.user.message", user_message, spans[0]) choice_event = { "index": 0, "finish_reason": "tool_calls", "message": { "role": "assistant", "tool_calls": [ { "id": tool_call_ids[0], "type": "function", "function": { "name": tool_names[0], "arguments": tool_args[0].replace("\n", "") if expect_content else None, }, }, { "id": tool_call_ids[1], "type": "function", "function": { "name": tool_names[1], "arguments": tool_args[1].replace("\n", "") if expect_content else None, }, }, ], }, } assert_message_in_logs(logs[2], "gen_ai.choice", choice_event, spans[0]) def assert_no_invalid_type_warning(caplog): assert "Invalid type" not in caplog.text test_chat_metrics.py000066400000000000000000000157031527070072300411730ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import pytest from tests.test_utils import DEFAULT_MODEL, USER_ONLY_PROMPT from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, ) from opentelemetry.semconv._incubating.attributes import ( openai_attributes as OpenAIAttributes, ) from opentelemetry.semconv._incubating.attributes import ( server_attributes as ServerAttributes, ) from opentelemetry.semconv._incubating.metrics import gen_ai_metrics from opentelemetry.util.genai.utils import is_experimental_mode _DURATION_BUCKETS = ( 0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.56, 5.12, 10.24, 20.48, 40.96, 81.92, ) _TOKEN_USAGE_BUCKETS = ( 1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864, ) def assert_all_metric_attributes(data_point, latest_experimental_enabled): assert GenAIAttributes.GEN_AI_OPERATION_NAME in data_point.attributes assert ( data_point.attributes[GenAIAttributes.GEN_AI_OPERATION_NAME] == GenAIAttributes.GenAiOperationNameValues.CHAT.value ) provider_name_attr_name = "gen_ai.provider.name" if latest_experimental_enabled else GenAIAttributes.GEN_AI_SYSTEM assert provider_name_attr_name in data_point.attributes assert data_point.attributes[provider_name_attr_name] == GenAIAttributes.GenAiSystemValues.OPENAI.value assert GenAIAttributes.GEN_AI_REQUEST_MODEL in data_point.attributes assert data_point.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == "gpt-4o-mini" assert GenAIAttributes.GEN_AI_RESPONSE_MODEL in data_point.attributes assert data_point.attributes[GenAIAttributes.GEN_AI_RESPONSE_MODEL] == "gpt-4o-mini-2024-07-18" system_fingerprint_attr_key = ( OpenAIAttributes.OPENAI_RESPONSE_SYSTEM_FINGERPRINT if latest_experimental_enabled else GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SYSTEM_FINGERPRINT ) response_service_tier_attr_key = ( OpenAIAttributes.OPENAI_RESPONSE_SERVICE_TIER if latest_experimental_enabled else GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER ) request_service_tier_attr_key = ( OpenAIAttributes.OPENAI_REQUEST_SERVICE_TIER if latest_experimental_enabled else GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER ) assert system_fingerprint_attr_key in data_point.attributes assert data_point.attributes[system_fingerprint_attr_key] == "fp_0ba0d124f1" assert request_service_tier_attr_key not in data_point.attributes assert response_service_tier_attr_key in data_point.attributes assert data_point.attributes[response_service_tier_attr_key] == "default" assert data_point.attributes[ServerAttributes.SERVER_ADDRESS] == "api.openai.com" def test_chat_completion_metrics(metric_reader, openai_client, instrument_with_content, vcr): latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_chat_completion_metrics.yaml"): openai_client.chat.completions.create(messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, stream=False) metrics = metric_reader.get_metrics_data().resource_metrics assert len(metrics) == 1 metric_data = metrics[0].scope_metrics[0].metrics assert len(metric_data) == 2 duration_metric = next( (m for m in metric_data if m.name == gen_ai_metrics.GEN_AI_CLIENT_OPERATION_DURATION), None, ) assert duration_metric is not None duration_point = duration_metric.data.data_points[0] assert duration_point.sum > 0 assert_all_metric_attributes(duration_point, latest_experimental_enabled) assert duration_point.explicit_bounds == _DURATION_BUCKETS token_usage_metric = next( (m for m in metric_data if m.name == gen_ai_metrics.GEN_AI_CLIENT_TOKEN_USAGE), None, ) assert token_usage_metric is not None input_token_usage = next( ( d for d in token_usage_metric.data.data_points if d.attributes[GenAIAttributes.GEN_AI_TOKEN_TYPE] == GenAIAttributes.GenAiTokenTypeValues.INPUT.value ), None, ) assert input_token_usage is not None assert input_token_usage.sum == 12 assert input_token_usage.explicit_bounds == _TOKEN_USAGE_BUCKETS assert input_token_usage.bucket_counts[2] == 1 assert_all_metric_attributes(input_token_usage, latest_experimental_enabled) output_token_usage = next( ( d for d in token_usage_metric.data.data_points if d.attributes[GenAIAttributes.GEN_AI_TOKEN_TYPE] == GenAIAttributes.GenAiTokenTypeValues.COMPLETION.value ), None, ) assert output_token_usage is not None assert output_token_usage.sum == 5 # assert against buckets [1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864] assert output_token_usage.bucket_counts[2] == 1 assert_all_metric_attributes(output_token_usage, latest_experimental_enabled) @pytest.mark.asyncio() async def test_async_chat_completion_metrics(metric_reader, async_openai_client, instrument_with_content, vcr): latest_experimental_enabled = is_experimental_mode() with vcr.use_cassette("test_async_chat_completion_metrics.yaml"): await async_openai_client.chat.completions.create(messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, stream=False) metrics = metric_reader.get_metrics_data().resource_metrics assert len(metrics) == 1 metric_data = metrics[0].scope_metrics[0].metrics assert len(metric_data) == 2 duration_metric = next( (m for m in metric_data if m.name == gen_ai_metrics.GEN_AI_CLIENT_OPERATION_DURATION), None, ) assert duration_metric is not None assert duration_metric.data.data_points[0].sum > 0 assert_all_metric_attributes(duration_metric.data.data_points[0], latest_experimental_enabled) token_usage_metric = next( (m for m in metric_data if m.name == gen_ai_metrics.GEN_AI_CLIENT_TOKEN_USAGE), None, ) assert token_usage_metric is not None input_token_usage = next( ( d for d in token_usage_metric.data.data_points if d.attributes[GenAIAttributes.GEN_AI_TOKEN_TYPE] == GenAIAttributes.GenAiTokenTypeValues.INPUT.value ), None, ) assert input_token_usage is not None assert input_token_usage.sum == 12 assert_all_metric_attributes(input_token_usage, latest_experimental_enabled) output_token_usage = next( ( d for d in token_usage_metric.data.data_points if d.attributes[GenAIAttributes.GEN_AI_TOKEN_TYPE] == GenAIAttributes.GenAiTokenTypeValues.COMPLETION.value ), None, ) assert output_token_usage is not None assert output_token_usage.sum == 12 assert_all_metric_attributes(output_token_usage, latest_experimental_enabled) test_choice_buffer.py000066400000000000000000000113741527070072300413110ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Unit tests for ChoiceBuffer and ToolCallBuffer classes.""" from openai.types.chat.chat_completion_chunk import ( ChoiceDeltaToolCall, ChoiceDeltaToolCallFunction, ) from opentelemetry.instrumentation.openai_v2.chat_buffers import ( ChoiceBuffer, ToolCallBuffer, ) def test_toolcallbuffer_append_arguments_with_string(): buf = ToolCallBuffer(0, "call_1", "get_weather") buf.append_arguments('{"city":') buf.append_arguments(' "NYC"}') assert "".join(buf.arguments) == '{"city": "NYC"}' def test_toolcallbuffer_append_arguments_with_none_is_skipped(): """Regression test for issue #4344. Some OpenAI-compatible providers (vLLM, TGI, etc.) send arguments=None on tool-call delta chunks instead of arguments="". This must not crash when joining the arguments list. """ buf = ToolCallBuffer(0, "call_1", "get_weather") buf.append_arguments(None) buf.append_arguments('{"city": "NYC"}') buf.append_arguments(None) assert "".join(buf.arguments) == '{"city": "NYC"}' def test_toolcallbuffer_append_arguments_all_none(): buf = ToolCallBuffer(0, "call_1", "get_weather") buf.append_arguments(None) buf.append_arguments(None) assert "".join(buf.arguments) == "" def test_toolcallbuffer_append_arguments_empty_string(): buf = ToolCallBuffer(0, "call_1", "get_weather") buf.append_arguments("") buf.append_arguments('{"city": "NYC"}') assert "".join(buf.arguments) == '{"city": "NYC"}' def test_choicebuffer_append_tool_call_with_none_arguments(): """End-to-end regression test for issue #4344. Simulates the exact scenario from the bug report where a provider sends arguments=None on the first tool-call delta chunk. """ buf = ChoiceBuffer(0) buf.append_tool_call( ChoiceDeltaToolCall( index=0, id="call_1", type="function", function=ChoiceDeltaToolCallFunction(name="get_weather", arguments=None), ) ) buf.append_tool_call( ChoiceDeltaToolCall( index=0, function=ChoiceDeltaToolCallFunction(arguments='{"city": "NYC"}'), ) ) # This must not raise TypeError result = "".join(buf.tool_calls_buffers[0].arguments) assert result == '{"city": "NYC"}' def test_choicebuffer_append_tool_call_normal_flow(): """Standard OpenAI flow where arguments="" on first delta.""" buf = ChoiceBuffer(0) buf.append_tool_call( ChoiceDeltaToolCall( index=0, id="call_1", type="function", function=ChoiceDeltaToolCallFunction(name="get_weather", arguments=""), ) ) buf.append_tool_call( ChoiceDeltaToolCall( index=0, function=ChoiceDeltaToolCallFunction(arguments='{"city": "NYC"}'), ) ) result = "".join(buf.tool_calls_buffers[0].arguments) assert result == '{"city": "NYC"}' def test_choicebuffer_append_multiple_tool_calls_with_none_arguments(): """Multiple tool calls where some have arguments=None.""" buf = ChoiceBuffer(0) # First tool call buf.append_tool_call( ChoiceDeltaToolCall( index=0, id="call_1", type="function", function=ChoiceDeltaToolCallFunction(name="get_weather", arguments=None), ) ) buf.append_tool_call( ChoiceDeltaToolCall( index=0, function=ChoiceDeltaToolCallFunction(arguments='{"city": "NYC"}'), ) ) # Second tool call buf.append_tool_call( ChoiceDeltaToolCall( index=1, id="call_2", type="function", function=ChoiceDeltaToolCallFunction(name="get_time", arguments=None), ) ) buf.append_tool_call( ChoiceDeltaToolCall( index=1, function=ChoiceDeltaToolCallFunction(arguments='{"tz": "EST"}'), ) ) assert "".join(buf.tool_calls_buffers[0].arguments) == '{"city": "NYC"}' assert "".join(buf.tool_calls_buffers[1].arguments) == '{"tz": "EST"}' def test_choicebuffer_append_tool_call_with_none_function(): """Handle delta chunks where function is None.""" buf = ChoiceBuffer(0) buf.append_tool_call( ChoiceDeltaToolCall( index=0, id="call_1", type="function", function=ChoiceDeltaToolCallFunction(name="get_weather", arguments='{"city": "NYC"}'), ) ) # Subsequent delta with function=None should not crash buf.append_tool_call( ChoiceDeltaToolCall( index=0, function=None, ) ) result = "".join(buf.tool_calls_buffers[0].arguments) assert result == '{"city": "NYC"}' test_completion_hook.py000066400000000000000000000070641527070072300417200ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os from unittest.mock import MagicMock, patch from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor from opentelemetry.util.genai.environment_variables import ( OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, ) from .test_utils import DEFAULT_MODEL, USER_ONLY_PROMPT @patch.dict( os.environ, {OTEL_SEMCONV_STABILITY_OPT_IN: "gen_ai_latest_experimental"}, ) def test_custom_hook_is_called( span_exporter, log_exporter, tracer_provider, logger_provider, meter_provider, openai_client, vcr, ): """A hook passed to instrument() is called after each chat completion.""" hook = MagicMock() instrumentor = OpenAIInstrumentor() _OpenTelemetrySemanticConventionStability._initialized = False instrumentor.instrument( tracer_provider=tracer_provider, logger_provider=logger_provider, meter_provider=meter_provider, completion_hook=hook, ) try: with vcr.use_cassette("test_chat_completion_with_content.yaml"): openai_client.chat.completions.create( messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, stream=False, ) finally: instrumentor.uninstrument() hook.on_completion.assert_called_once() kwargs = hook.on_completion.call_args.kwargs assert kwargs["inputs"] assert kwargs["outputs"] assert kwargs["span"] is not None # Content goes to the hook only — not to span attributes or log records spans = span_exporter.get_finished_spans() assert len(spans) == 1 span_attrs = spans[0].attributes or {} assert "gen_ai.input.messages" not in span_attrs assert "gen_ai.output.messages" not in span_attrs assert log_exporter.get_finished_logs() == () @patch.dict( os.environ, { OTEL_SEMCONV_STABILITY_OPT_IN: "gen_ai_latest_experimental", OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "span_only", }, ) def test_default_hook_loaded_from_env( span_exporter, tracer_provider, logger_provider, meter_provider, openai_client, vcr, ): """When no hook kwarg is given, load_completion_hook() provides the default.""" default_hook = MagicMock() instrumentor = OpenAIInstrumentor() with patch( "opentelemetry.instrumentation.openai_v2.load_completion_hook", return_value=default_hook, ): instrumentor.instrument( tracer_provider=tracer_provider, logger_provider=logger_provider, meter_provider=meter_provider, # no completion_hook kwarg — should fall back to load_completion_hook() ) try: with vcr.use_cassette("test_chat_completion_with_content.yaml"): openai_client.chat.completions.create( messages=USER_ONLY_PROMPT, model=DEFAULT_MODEL, stream=False, ) finally: instrumentor.uninstrument() default_hook.on_completion.assert_called_once() kwargs = default_hook.on_completion.call_args.kwargs assert kwargs["inputs"] assert kwargs["outputs"] assert kwargs["span"] is not None spans = span_exporter.get_finished_spans() assert len(spans) == 1 span_attrs = spans[0].attributes or {} assert "gen_ai.input.messages" in span_attrs assert "gen_ai.output.messages" in span_attrs test_embeddings.py000066400000000000000000000261221527070072300406240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Unit tests for OpenAI Embeddings API instrumentation.""" import pytest from openai import ( NOT_GIVEN, APIConnectionError, NotFoundError, OpenAI, ) try: from openai import not_given # pylint: disable=no-name-in-module except ImportError: not_given = NOT_GIVEN from opentelemetry.semconv._incubating.attributes import ( error_attributes as ErrorAttributes, ) from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, ) from opentelemetry.semconv._incubating.attributes import ( server_attributes as ServerAttributes, ) from opentelemetry.semconv._incubating.metrics import gen_ai_metrics from opentelemetry.util.genai.utils import is_experimental_mode from .test_utils import ( DEFAULT_EMBEDDING_MODEL, assert_all_attributes, assert_embedding_attributes, ) def test_embeddings_no_content(span_exporter, log_exporter, openai_client, instrument_no_content, vcr): """Test creating embeddings with content capture disabled""" latest_experimental_enabled = is_experimental_mode() input_text = "This is a test for embeddings" with vcr.use_cassette("test_embeddings_no_content.yaml"): response = openai_client.embeddings.create( model=DEFAULT_EMBEDDING_MODEL, input=input_text, ) # Verify spans spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert_embedding_attributes( spans[0], DEFAULT_EMBEDDING_MODEL, latest_experimental_enabled, response, ) # No logs should be emitted when content capture is disabled logs = log_exporter.get_finished_logs() assert len(logs) == 0 def test_embeddings_with_dimensions(span_exporter, metric_reader, openai_client, instrument_no_content, vcr): """Test creating embeddings with custom dimensions parameter""" latest_experimental_enabled = is_experimental_mode() input_text = "This is a test for embeddings with dimensions" dimensions = 512 # Using a smaller dimension than default with vcr.use_cassette("test_embeddings_with_dimensions.yaml"): response = openai_client.embeddings.create( model=DEFAULT_EMBEDDING_MODEL, input=input_text, dimensions=dimensions, ) # Verify spans spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert_embedding_attributes( spans[0], DEFAULT_EMBEDDING_MODEL, latest_experimental_enabled, response, ) # Verify dimensions attribute is set correctly assert spans[0].attributes["gen_ai.embeddings.dimension.count"] == dimensions # Verify actual embedding dimensions match the requested dimensions assert len(response.data[0].embedding) == dimensions # Verify metrics metrics = metric_reader.get_metrics_data().resource_metrics assert len(metrics) == 1 metric_data = metrics[0].scope_metrics[0].metrics duration_metric = next( (m for m in metric_data if m.name == gen_ai_metrics.GEN_AI_CLIENT_OPERATION_DURATION), None, ) assert duration_metric is not None # Verify the dimensions attribute is present in metrics for point in duration_metric.data.data_points: if "gen_ai.embeddings.dimension.count" in point.attributes: assert point.attributes["gen_ai.embeddings.dimension.count"] == dimensions break else: assert False, "Dimensions attribute not found in metrics" def test_embeddings_with_batch_input(span_exporter, metric_reader, openai_client, instrument_with_content, vcr): """Test creating embeddings with batch input (list of strings)""" latest_experimental_enabled = is_experimental_mode() input_texts = [ "This is the first test string for embeddings", "This is the second test string for embeddings", "This is the third test string for embeddings", ] with vcr.use_cassette("test_embeddings_with_batch_input.yaml"): response = openai_client.embeddings.create( model=DEFAULT_EMBEDDING_MODEL, input=input_texts, ) # Verify spans spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert_embedding_attributes( spans[0], DEFAULT_EMBEDDING_MODEL, latest_experimental_enabled, response, ) # Verify results contain the same number of embeddings as input texts assert len(response.data) == len(input_texts) def test_embeddings_with_encoding_format(span_exporter, metric_reader, openai_client, instrument_no_content, vcr): """Test creating embeddings with different encoding format""" latest_experimental_enabled = is_experimental_mode() input_text = "This is a test for embeddings with encoding format" encoding_format = "base64" with vcr.use_cassette("test_embeddings_with_encoding_format.yaml"): response = openai_client.embeddings.create( model=DEFAULT_EMBEDDING_MODEL, input=input_text, encoding_format=encoding_format, ) # Verify spans spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert_embedding_attributes( spans[0], DEFAULT_EMBEDDING_MODEL, latest_experimental_enabled, response, ) # Verify encoding_format attribute is set correctly assert spans[0].attributes["gen_ai.request.encoding_formats"] == (encoding_format,) @pytest.mark.parametrize("not_given_value", [NOT_GIVEN, not_given]) def test_embeddings_with_not_given_values( span_exporter, metric_reader, openai_client, instrument_no_content, not_given_value, vcr, ): """Test creating embeddings with NOT_GIVEN and not_given values""" latest_experimental_enabled = is_experimental_mode() input_text = "This is a test for embeddings with encoding format" with vcr.use_cassette("test_embeddings_with_not_given_values.yaml"): response = openai_client.embeddings.create( model=DEFAULT_EMBEDDING_MODEL, input=input_text, dimensions=not_given_value, ) # Verify spans spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert_embedding_attributes( spans[0], DEFAULT_EMBEDDING_MODEL, latest_experimental_enabled, response, ) assert "gen_ai.request.dimensions" not in spans[0].attributes def test_embeddings_bad_endpoint(span_exporter, metric_reader, instrument_no_content, vcr): """Test error handling for bad endpoint""" latest_experimental_enabled = is_experimental_mode() input_text = "This is a test for embeddings with bad endpoint" client = OpenAI(base_url="http://localhost:4242") with vcr.use_cassette("test_embeddings_bad_endpoint.yaml"): with pytest.raises(APIConnectionError): client.embeddings.create( model=DEFAULT_EMBEDDING_MODEL, input=input_text, timeout=0.1, ) # Verify spans spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert_all_attributes( spans[0], DEFAULT_EMBEDDING_MODEL, latest_experimental_enabled, operation_name="embeddings", server_address="localhost", ) assert 4242 == spans[0].attributes[ServerAttributes.SERVER_PORT] assert "APIConnectionError" == spans[0].attributes[ErrorAttributes.ERROR_TYPE] # Verify metrics metrics = metric_reader.get_metrics_data().resource_metrics assert len(metrics) == 1 metric_data = metrics[0].scope_metrics[0].metrics duration_metric = next( (m for m in metric_data if m.name == gen_ai_metrics.GEN_AI_CLIENT_OPERATION_DURATION), None, ) assert duration_metric is not None assert duration_metric.data.data_points[0].sum > 0 assert duration_metric.data.data_points[0].attributes[ErrorAttributes.ERROR_TYPE] == "APIConnectionError" def test_embeddings_model_not_found(span_exporter, metric_reader, openai_client, instrument_no_content, vcr): """Test error handling for non-existent model""" latest_experimental_enabled = is_experimental_mode() model_name = "non-existent-embedding-model" input_text = "This is a test for embeddings with bad model" with vcr.use_cassette("test_embeddings_model_not_found.yaml"): with pytest.raises(NotFoundError): openai_client.embeddings.create( model=model_name, input=input_text, ) # Verify spans spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert_all_attributes( spans[0], model_name, latest_experimental_enabled, operation_name="embeddings", ) assert "NotFoundError" == spans[0].attributes[ErrorAttributes.ERROR_TYPE] # Verify metrics metrics = metric_reader.get_metrics_data().resource_metrics assert len(metrics) == 1 metric_data = metrics[0].scope_metrics[0].metrics duration_metric = next( (m for m in metric_data if m.name == gen_ai_metrics.GEN_AI_CLIENT_OPERATION_DURATION), None, ) assert duration_metric is not None assert duration_metric.data.data_points[0].sum > 0 assert duration_metric.data.data_points[0].attributes[ErrorAttributes.ERROR_TYPE] == "NotFoundError" def test_embeddings_token_metrics(span_exporter, metric_reader, openai_client, instrument_no_content, vcr): """Test that token usage metrics are correctly recorded""" latest_experimental_enabled = is_experimental_mode() input_text = "This is a test for embeddings token metrics" with vcr.use_cassette("test_embeddings_token_metrics.yaml"): response = openai_client.embeddings.create( model=DEFAULT_EMBEDDING_MODEL, input=input_text, ) # Verify spans spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert_embedding_attributes( spans[0], DEFAULT_EMBEDDING_MODEL, latest_experimental_enabled, response, ) # Verify metrics metrics = metric_reader.get_metrics_data().resource_metrics assert len(metrics) == 1 metric_data = metrics[0].scope_metrics[0].metrics # Verify operation duration metric duration_metric = next( (m for m in metric_data if m.name == gen_ai_metrics.GEN_AI_CLIENT_OPERATION_DURATION), None, ) assert duration_metric is not None # Verify token usage metric token_metric = next( (m for m in metric_data if m.name == gen_ai_metrics.GEN_AI_CLIENT_TOKEN_USAGE), None, ) assert token_metric is not None # Find the input token data point input_token_point = None for point in token_metric.data.data_points: if point.attributes[GenAIAttributes.GEN_AI_TOKEN_TYPE] == GenAIAttributes.GenAiTokenTypeValues.INPUT.value: input_token_point = point break assert input_token_point is not None, "Input token metric not found" # Verify the token counts match what was reported in the response assert input_token_point.sum == response.usage.prompt_tokens test_request_attributes.py000066400000000000000000000065731527070072300424710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Unit tests for get_llm_request_attributes and span name logic in utils.py.""" from types import SimpleNamespace import pytest from opentelemetry.instrumentation.openai_v2.patch import ( _get_embeddings_span_name, ) from opentelemetry.instrumentation.openai_v2.utils import ( get_llm_request_attributes, ) from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, ) @pytest.fixture(autouse=True) def fixture_vcr(): """No VCR needed for these unit tests.""" yield def _make_client(base_url=None): return SimpleNamespace(_base_url=base_url) def test_model_omitted_when_missing(): """When 'model' is not in kwargs, GEN_AI_REQUEST_MODEL should be absent.""" attrs = get_llm_request_attributes( kwargs={}, client_instance=_make_client(), latest_experimental_enabled=False, ) assert GenAIAttributes.GEN_AI_REQUEST_MODEL not in attrs def test_model_omitted_when_missing_experimental(): """Same as above but with latest_experimental_enabled=True.""" attrs = get_llm_request_attributes( kwargs={}, client_instance=_make_client(), latest_experimental_enabled=True, ) assert GenAIAttributes.GEN_AI_REQUEST_MODEL not in attrs def test_model_preserved_when_provided(): """When 'model' is in kwargs, GEN_AI_REQUEST_MODEL should be set to its value.""" attrs = get_llm_request_attributes( kwargs={"model": "gpt-4o-mini"}, client_instance=_make_client(), latest_experimental_enabled=False, ) assert attrs[GenAIAttributes.GEN_AI_REQUEST_MODEL] == "gpt-4o-mini" def test_span_name_includes_model_when_present(): """Span name should be '{operation} {model}' when model is provided.""" attrs = get_llm_request_attributes( kwargs={"model": "gpt-4o"}, client_instance=_make_client(), latest_experimental_enabled=False, ) operation_name = attrs[GenAIAttributes.GEN_AI_OPERATION_NAME] model = attrs.get(GenAIAttributes.GEN_AI_REQUEST_MODEL) span_name = f"{operation_name} {model}" if model else operation_name assert span_name == "chat gpt-4o" def test_span_name_uses_operation_only_when_model_missing(): """Span name should be just '{operation}' when model is not provided.""" attrs = get_llm_request_attributes( kwargs={}, client_instance=_make_client(), latest_experimental_enabled=False, ) operation_name = attrs[GenAIAttributes.GEN_AI_OPERATION_NAME] model = attrs.get(GenAIAttributes.GEN_AI_REQUEST_MODEL) span_name = f"{operation_name} {model}" if model else operation_name assert span_name == "chat" def test_embeddings_span_name_includes_model(): """Embeddings span name should be '{operation} {model}' when model is present.""" span_attributes = { GenAIAttributes.GEN_AI_OPERATION_NAME: "embeddings", GenAIAttributes.GEN_AI_REQUEST_MODEL: "text-embedding-3-small", } assert _get_embeddings_span_name(span_attributes) == "embeddings text-embedding-3-small" def test_embeddings_span_name_without_model(): """Embeddings span name should be just '{operation}' when model is absent.""" span_attributes = { GenAIAttributes.GEN_AI_OPERATION_NAME: "embeddings", } assert _get_embeddings_span_name(span_attributes) == "embeddings" test_response_extractors.py000066400000000000000000000252521527070072300426420ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from types import SimpleNamespace from unittest import mock import pytest from opentelemetry.instrumentation.openai_v2 import response_extractors from opentelemetry.semconv._incubating.attributes import ( openai_attributes as OpenAIAttributes, ) from opentelemetry.util.genai.types import LLMInvocation try: # Responses types are not available in the oldest supported OpenAI SDK. # pylint: disable-next=no-name-in-module from openai.types.responses.response import Response HAS_RESPONSES_TYPES = True except ImportError: Response = None HAS_RESPONSES_TYPES = False pytestmark = pytest.mark.skipif( not HAS_RESPONSES_TYPES, reason="Responses SDK types require a newer openai SDK", ) @pytest.fixture(scope="module", name="loaded_module") def _loaded_module_fixture(): return response_extractors def _make_response(output=None, **overrides): payload = { "id": "resp_123", "created_at": 0.0, "model": "gpt-4.1", "object": "response", "output": output or [], "parallel_tool_calls": False, "tool_choice": "auto", "tools": [], "temperature": 1.0, "top_p": 1.0, "usage": { "input_tokens": 11, "input_tokens_details": {"cached_tokens": 0}, "output_tokens": 7, "output_tokens_details": {"reasoning_tokens": 0}, "total_tokens": 18, }, } payload.update(overrides) return Response.model_validate(payload) def test_extract_system_instruction_returns_text_for_string(loaded_module): params = loaded_module.extract_params(instructions="Be concise") instructions = loaded_module.get_system_instruction(params.instructions) assert [part.content for part in instructions] == ["Be concise"] def test_extract_input_messages_supports_string_and_mixed_message_content( loaded_module, ): from_string = loaded_module.get_input_messages("Hello") from_list = loaded_module.get_input_messages( [ {"role": "user", "content": "First"}, SimpleNamespace( role="assistant", content=[ {"text": "Second"}, SimpleNamespace(text="Third"), {"type": "input_image", "image_url": "ignored"}, ], ), {"role": None, "content": "ignored"}, ] ) assert [(msg.role, [part.content for part in msg.parts]) for msg in from_string] == [("user", ["Hello"])] assert [(msg.role, [part.content for part in msg.parts]) for msg in from_list] == [ ("user", ["First"]), ("assistant", ["Second", "Third"]), ] def test_extract_output_messages_maps_parts_and_finish_reasons(loaded_module): response = _make_response( output=[ { "id": "msg_1", "type": "message", "role": "assistant", "status": "completed", "content": [ {"type": "output_text", "text": "Done", "annotations": []}, {"type": "refusal", "refusal": "Cannot comply"}, ], }, { "id": "msg_2", "type": "message", "role": "assistant", "status": "incomplete", "content": [ { "type": "output_text", "text": "Partial", "annotations": [], } ], }, { "id": "msg_3", "type": "message", "role": "assistant", "status": "in_progress", "content": [ { "type": "output_text", "text": "Pending", "annotations": [], } ], }, { "id": "fc_1", "type": "function_call", "status": "completed", "name": "get_weather", "call_id": "call_123", "arguments": '{"city":"SF"}', }, { "id": "rs_1", "type": "reasoning", "status": "completed", "summary": [{"type": "summary_text", "text": "Thought step"}], "content": None, }, ] ) messages = loaded_module.get_output_messages_from_response(response) assert [(msg.role, msg.finish_reason) for msg in messages] == [ ("assistant", "stop"), ("assistant", "incomplete"), ("assistant", "tool_calls"), ("assistant", "stop"), ] assert [part.content for part in messages[0].parts] == [ "Done", "Cannot comply", ] assert [part.content for part in messages[1].parts] == ["Partial"] assert messages[2].parts[0].type == "tool_call" assert messages[2].parts[0].name == "get_weather" assert messages[2].parts[0].arguments == {"city": "SF"} assert messages[3].parts[0].type == "reasoning" assert messages[3].parts[0].content == "Thought step" def test_extract_finish_reasons_maps_terminal_message_and_tool_items( loaded_module, ): response = _make_response( output=[ { "id": "msg_1", "type": "message", "role": "assistant", "status": "completed", "content": [], }, { "id": "msg_2", "type": "message", "role": "assistant", "status": "in_progress", "content": [], }, { "id": "fc_1", "type": "function_call", "status": "incomplete", "name": "tool", "call_id": "call_1", "arguments": "{}", }, ] ) assert loaded_module.extract_finish_reasons(response) == [ "stop", "tool_calls", ] def test_extract_output_type_handles_text_format_mapping(loaded_module): assert loaded_module.extract_params(text={"format": {"type": "json_schema"}}).output_type == "json" assert loaded_module.extract_params(text={"format": {"type": "text"}}).output_type == "text" assert ( loaded_module.extract_params(text=SimpleNamespace(format=SimpleNamespace(type="json_schema"))).output_type == "json" ) assert loaded_module.extract_params(text=SimpleNamespace(format=SimpleNamespace(type="text"))).output_type == "text" assert loaded_module.extract_params(text={"format": "plain"}).output_type is None assert loaded_module.extract_params(text="plain").output_type is None def test_extractors_handle_missing_genai_types_import(loaded_module): with ( mock.patch.object(loaded_module, "Text", None), mock.patch.object(loaded_module, "InputMessage", None), mock.patch.object(loaded_module, "OutputMessage", None), ): assert loaded_module.get_system_instruction("hi") == [] assert loaded_module.get_input_messages("hi") == [] assert ( loaded_module.get_output_messages_from_response( _make_response( output=[ { "id": "msg_1", "type": "message", "role": "assistant", "status": "completed", "content": [], } ] ) ) == [] ) def test_set_invocation_response_attributes_populates_usage_and_metadata( loaded_module, ): invocation = LLMInvocation(request_model="gpt-4o-mini") result = _make_response( service_tier="scale", usage={ "input_tokens": 11, "input_tokens_details": { "cached_tokens": 3, "cache_creation_input_tokens": 5, }, "output_tokens": 7, "output_tokens_details": {"reasoning_tokens": 0}, "total_tokens": 18, }, ) loaded_module.set_invocation_response_attributes(invocation, result, capture_content=False) assert invocation.response_model_name == "gpt-4.1" assert invocation.response_id == "resp_123" assert invocation.input_tokens == 11 assert invocation.output_tokens == 7 assert invocation.cache_read_input_tokens == 3 assert invocation.cache_creation_input_tokens == 5 assert invocation.attributes == { OpenAIAttributes.OPENAI_RESPONSE_SERVICE_TIER: "scale", } def test_set_invocation_response_attributes_populates_output_messages( loaded_module, ): invocation = LLMInvocation(request_model="gpt-4o-mini") result = _make_response( output=[ { "id": "msg_1", "type": "message", "role": "assistant", "status": "completed", "content": [{"type": "output_text", "text": "Done", "annotations": []}], } ] ) loaded_module.set_invocation_response_attributes(invocation, result, capture_content=True) assert invocation.finish_reasons == ["stop"] assert [(message.role, message.finish_reason) for message in invocation.output_messages] == [("assistant", "stop")] assert [[part.content for part in message.parts] for message in invocation.output_messages] == [["Done"]] def test_extractors_ignore_invalid_request_shapes_without_validation( loaded_module, ): params = loaded_module.extract_params(instructions=["not-a-string"], input=42, text={"format": {"type": 42}}) assert loaded_module.get_system_instruction(params.instructions) == [] assert loaded_module.get_input_messages(params.input) == [] assert params.output_type is None def test_response_extractors_ignore_invalid_shapes_without_validation( loaded_module, ): invocation = LLMInvocation(request_model="gpt-4o-mini") invalid_result = SimpleNamespace(output=42, usage=42) assert loaded_module.get_output_messages_from_response(invalid_result) == [] assert loaded_module.extract_finish_reasons(invalid_result) == [] loaded_module.set_invocation_response_attributes(invocation, invalid_result, capture_content=True) assert invocation.response_model_name is None assert invocation.response_id is None assert invocation.input_tokens is None assert invocation.output_tokens is None assert invocation.finish_reasons is None assert not invocation.output_messages assert not invocation.attributes test_response_wrappers.py000066400000000000000000000305131527070072300423030ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from types import SimpleNamespace import pytest from opentelemetry.instrumentation.openai_v2.response_wrappers import ( AsyncResponseStreamManagerWrapper, AsyncResponseStreamWrapper, ResponseStreamManagerWrapper, ResponseStreamWrapper, ) class _FakeManager: def __init__(self, stream, suppressed=False, exit_error=None): self._stream = stream self._suppressed = suppressed self._exit_error = exit_error self.exit_args = None def __enter__(self): return self._stream def __exit__(self, exc_type, exc_val, exc_tb): self.exit_args = (exc_type, exc_val, exc_tb) if self._exit_error is not None: raise self._exit_error return self._suppressed class _FakeAsyncManager: def __init__(self, stream, suppressed=False, exit_error=None): self._stream = stream self._suppressed = suppressed self._exit_error = exit_error self.exit_args = None async def __aenter__(self): return self._stream async def __aexit__(self, exc_type, exc_val, exc_tb): self.exit_args = (exc_type, exc_val, exc_tb) if self._exit_error is not None: raise self._exit_error return self._suppressed def _noop_stop(): return None def _noop_fail(error): del error def _make_wrapper(manager): invocation = SimpleNamespace( request_model=None, stop=_noop_stop, fail=_noop_fail, ) return ResponseStreamManagerWrapper( manager=manager, invocation=invocation, capture_content=False, ) def _make_stream_wrapper(stream, invocation=None): if invocation is None: invocation = SimpleNamespace( request_model=None, stop=_noop_stop, fail=_noop_fail, ) return ResponseStreamWrapper( stream=stream, invocation=invocation, capture_content=False, ) def _make_async_manager_wrapper(manager): invocation = SimpleNamespace( request_model=None, stop=_noop_stop, fail=_noop_fail, ) return AsyncResponseStreamManagerWrapper( manager=manager, invocation=invocation, capture_content=False, ) def _make_async_stream_wrapper(stream, invocation=None): if invocation is None: invocation = SimpleNamespace( request_model=None, stop=_noop_stop, fail=_noop_fail, ) return AsyncResponseStreamWrapper( stream=stream, invocation=invocation, capture_content=False, ) class _FakeStreamWrapper: def __init__(self): self.exit_args = None def __exit__(self, exc_type, exc_val, exc_tb): self.exit_args = (exc_type, exc_val, exc_tb) return False class _FakeAsyncStreamWrapper: def __init__(self): self.exit_args = None async def __aexit__(self, exc_type, exc_val, exc_tb): self.exit_args = (exc_type, exc_val, exc_tb) return False class _FakeAsyncResponse: def __init__(self): self.aclose_calls = 0 async def aclose(self): self.aclose_calls += 1 class _FakeSyncResponse: def __init__(self): self.close_calls = 0 def close(self): self.close_calls += 1 class _FakeAsyncStream: def __init__( self, *, events=None, error=None, final_response=None, response=None, ): self._events = list(events or []) self._error = error self._final_response = final_response self._response = response self.close_calls = 0 self.get_final_response_calls = 0 async def __anext__(self): if self._events: return self._events.pop(0) if self._error is not None: raise self._error raise StopAsyncIteration async def close(self): self.close_calls += 1 async def get_final_response(self): self.get_final_response_calls += 1 return self._final_response def test_manager_exit_forwards_exception_to_stream_wrapper(): manager = _FakeManager(stream=SimpleNamespace(), suppressed=False) wrapper = _make_wrapper(manager) stream_wrapper = _FakeStreamWrapper() wrapper._stream_wrapper = stream_wrapper error = ValueError("boom") result = wrapper.__exit__(ValueError, error, None) assert result is False assert manager.exit_args == (ValueError, error, None) assert stream_wrapper.exit_args == (ValueError, error, None) assert wrapper._stream_wrapper is None def test_manager_exit_uses_none_exception_when_manager_suppresses(): manager = _FakeManager(stream=SimpleNamespace(), suppressed=True) wrapper = _make_wrapper(manager) stream_wrapper = _FakeStreamWrapper() wrapper._stream_wrapper = stream_wrapper error = RuntimeError("ignored") result = wrapper.__exit__(RuntimeError, error, None) assert result is True assert manager.exit_args == (RuntimeError, error, None) assert stream_wrapper.exit_args == (None, None, None) assert wrapper._stream_wrapper is None def test_manager_exit_still_finalizes_stream_wrapper_when_manager_raises(): manager_error = RuntimeError("manager failure") manager = _FakeManager(stream=SimpleNamespace(), suppressed=False, exit_error=manager_error) wrapper = _make_wrapper(manager) stream_wrapper = _FakeStreamWrapper() wrapper._stream_wrapper = stream_wrapper error = ValueError("outer") with pytest.raises(RuntimeError, match="manager failure"): wrapper.__exit__(ValueError, error, None) assert manager.exit_args == (ValueError, error, None) assert stream_wrapper.exit_args == (ValueError, error, None) assert wrapper._stream_wrapper is None def test_stream_wrapper_response_falls_back_to_public_response_attr(): response = _FakeSyncResponse() stream = SimpleNamespace(response=response) wrapper = _make_stream_wrapper(stream) stopped = [] wrapper._stop = stopped.append wrapper.response.close() assert response.close_calls == 1 assert stopped == [None] @pytest.mark.asyncio async def test_async_manager_exit_forwards_exception_to_stream_wrapper(): manager = _FakeAsyncManager(stream=SimpleNamespace(), suppressed=False) wrapper = _make_async_manager_wrapper(manager) stream_wrapper = _FakeAsyncStreamWrapper() wrapper._stream_wrapper = stream_wrapper error = ValueError("boom") result = await wrapper.__aexit__(ValueError, error, None) assert result is False assert manager.exit_args == (ValueError, error, None) assert stream_wrapper.exit_args == (ValueError, error, None) assert wrapper._stream_wrapper is None @pytest.mark.asyncio async def test_async_manager_enter_constructs_async_stream_wrapper(): stream = _FakeAsyncStream() manager = _FakeAsyncManager(stream=stream) wrapper = _make_async_manager_wrapper(manager) async with wrapper as result: assert isinstance(result, AsyncResponseStreamWrapper) assert result.stream is stream assert wrapper._stream_wrapper is result @pytest.mark.asyncio async def test_async_manager_exit_uses_none_exception_when_manager_suppresses(): manager = _FakeAsyncManager(stream=SimpleNamespace(), suppressed=True) wrapper = _make_async_manager_wrapper(manager) stream_wrapper = _FakeAsyncStreamWrapper() wrapper._stream_wrapper = stream_wrapper error = RuntimeError("ignored") result = await wrapper.__aexit__(RuntimeError, error, None) assert result is True assert manager.exit_args == (RuntimeError, error, None) assert stream_wrapper.exit_args == (None, None, None) assert wrapper._stream_wrapper is None @pytest.mark.asyncio async def test_async_manager_exit_still_finalizes_stream_wrapper_when_manager_raises(): manager_error = RuntimeError("manager failure") manager = _FakeAsyncManager(stream=SimpleNamespace(), suppressed=False, exit_error=manager_error) wrapper = _make_async_manager_wrapper(manager) stream_wrapper = _FakeAsyncStreamWrapper() wrapper._stream_wrapper = stream_wrapper error = ValueError("outer") with pytest.raises(RuntimeError, match="manager failure"): await wrapper.__aexit__(ValueError, error, None) assert manager.exit_args == (ValueError, error, None) assert stream_wrapper.exit_args == (ValueError, error, None) assert wrapper._stream_wrapper is None @pytest.mark.asyncio async def test_async_stream_wrapper_exit_closes_without_exception(): stream = _FakeAsyncStream() wrapper = _make_async_stream_wrapper(stream) stopped = [] wrapper._stop = stopped.append result = await wrapper.__aexit__(None, None, None) assert result is False assert stream.close_calls == 1 assert stopped == [None] @pytest.mark.asyncio async def test_async_stream_wrapper_exit_fails_and_closes_on_exception(): stream = _FakeAsyncStream() wrapper = _make_async_stream_wrapper(stream) stopped = [] failures = [] def record_failure(message, error_type): failures.append((message, error_type)) wrapper._stop = stopped.append wrapper._fail = record_failure error = ValueError("boom") result = await wrapper.__aexit__(ValueError, error, None) assert result is False assert stream.close_calls == 1 assert stopped == [None] assert failures == [("boom", ValueError)] @pytest.mark.asyncio async def test_async_stream_wrapper_close_closes_stream_and_stops(): stream = _FakeAsyncStream() wrapper = _make_async_stream_wrapper(stream) stopped = [] wrapper._stop = stopped.append await wrapper.close() assert stream.close_calls == 1 assert stopped == [None] @pytest.mark.asyncio async def test_async_stream_wrapper_processes_events_and_stops_on_completion(): event = SimpleNamespace(type="response.created") stream = _FakeAsyncStream(events=[event]) wrapper = _make_async_stream_wrapper(stream) processed = [] stopped = [] wrapper.process_event = processed.append wrapper._stop = stopped.append result = await anext(wrapper) assert result is event assert processed == [event] with pytest.raises(StopAsyncIteration): await anext(wrapper) assert stopped == [None] @pytest.mark.asyncio async def test_async_stream_wrapper_until_done_consumes_stream(): events = [ SimpleNamespace(type="response.created"), SimpleNamespace(type="response.in_progress"), ] stream = _FakeAsyncStream(events=events) wrapper = _make_async_stream_wrapper(stream) processed = [] stopped = [] wrapper.process_event = processed.append wrapper._stop = stopped.append result = await wrapper.until_done() assert result is wrapper assert processed == events assert stopped == [None] @pytest.mark.asyncio async def test_async_stream_wrapper_fails_and_reraises_stream_errors(): error = ValueError("boom") stream = _FakeAsyncStream(error=error) wrapper = _make_async_stream_wrapper(stream) failures = [] def record_failure(message, error_type): failures.append((message, error_type)) wrapper._fail = record_failure with pytest.raises(ValueError, match="boom"): await anext(wrapper) assert failures == [("boom", ValueError)] @pytest.mark.asyncio async def test_async_stream_response_aclose_finalizes_wrapper(): response = _FakeAsyncResponse() stream = _FakeAsyncStream(response=response) wrapper = _make_async_stream_wrapper(stream) stopped = [] wrapper._stop = stopped.append await wrapper.response.aclose() assert response.aclose_calls == 1 assert stopped == [None] @pytest.mark.asyncio async def test_async_stream_response_is_none_when_stream_has_no_response(): wrapper = _make_async_stream_wrapper(SimpleNamespace()) assert wrapper.response is None @pytest.mark.asyncio async def test_async_stream_get_final_response_waits_for_completion(): event = SimpleNamespace(type="response.in_progress") final_response = SimpleNamespace(id="response_123") stream = _FakeAsyncStream(events=[event], final_response=final_response) wrapper = _make_async_stream_wrapper(stream) wrapper.process_event = lambda current: current result = await wrapper.get_final_response() assert result is final_response assert stream.get_final_response_calls == 1 test_responses.py000066400000000000000000000551401527070072300405460ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import inspect import json import pytest from openai import APIConnectionError, BadRequestError, NotFoundError, OpenAI from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor from opentelemetry.instrumentation.openai_v2.response_wrappers import ( ResponseStreamWrapper, ) from opentelemetry.semconv._incubating.attributes import ( error_attributes as ErrorAttributes, ) from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, ) from opentelemetry.semconv._incubating.attributes import ( server_attributes as ServerAttributes, ) from opentelemetry.util.genai.utils import is_experimental_mode from .test_utils import ( DEFAULT_MODEL, USER_ONLY_EXPECTED_INPUT_MESSAGES, USER_ONLY_PROMPT, assert_all_attributes, assert_cache_attributes, assert_messages_attribute, format_simple_expected_output_message, get_responses_weather_tool_definition, ) try: # Responses is not available in the oldest supported OpenAI SDK, so keep # this import guarded. Pylint runs against the oldest dependency set and # cannot resolve this optional module there. # pylint: disable-next=no-name-in-module from openai.resources.responses.responses import Responses as _Responses HAS_RESPONSES_API = True _create_params = set(inspect.signature(_Responses.create).parameters) _has_tools_param = "tools" in _create_params _has_reasoning_param = "reasoning" in _create_params except ImportError: HAS_RESPONSES_API = False _has_tools_param = False _has_reasoning_param = False pytestmark = pytest.mark.skipif(not HAS_RESPONSES_API, reason="Responses API requires a newer openai SDK") SYSTEM_INSTRUCTIONS = "You are a helpful assistant." EXPECTED_SYSTEM_INSTRUCTIONS = [ { "type": "text", "content": SYSTEM_INSTRUCTIONS, } ] INVALID_MODEL = "this-model-does-not-exist" REASONING_MODEL = "gpt-5.4" REASONING_PROMPT = """ Write a bash script that takes a matrix represented as a string with format '[1,2],[3,4],[5,6]' and prints the transpose in the same format. """ def _skip_if_not_latest(): if not is_experimental_mode(): pytest.skip("Responses create instrumentation only supports the latest experimental semconv path") def _load_span_messages(span, attribute): value = span.attributes.get(attribute) assert value is not None return json.loads(value) def _assert_response_content(span, response, log_exporter): assert_messages_attribute( span.attributes[GenAIAttributes.GEN_AI_INPUT_MESSAGES], USER_ONLY_EXPECTED_INPUT_MESSAGES, ) assert json.loads(span.attributes[GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS]) == EXPECTED_SYSTEM_INSTRUCTIONS assert_messages_attribute( span.attributes[GenAIAttributes.GEN_AI_OUTPUT_MESSAGES], format_simple_expected_output_message(response.output_text), ) assert len(log_exporter.get_finished_logs()) == 0 def _assert_request_attrs( span, *, temperature=None, top_p=None, max_tokens=None, output_type=None, ): if temperature is not None: assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE] == temperature if top_p is not None: assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_TOP_P] == top_p if max_tokens is not None: assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS] == max_tokens if output_type is not None: assert span.attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] == output_type def _collect_completed_response(stream): response = None for event in stream: if event.type == "response.completed": response = event.response assert response is not None return response def test_responses_uninstrument_removes_patching(span_exporter, tracer_provider, logger_provider, meter_provider): instrumentor = OpenAIInstrumentor() instrumentor.instrument( tracer_provider=tracer_provider, logger_provider=logger_provider, meter_provider=meter_provider, ) instrumentor.uninstrument() assert len(span_exporter.get_finished_spans()) == 0 def test_responses_multiple_instrument_uninstrument_cycles(tracer_provider, logger_provider, meter_provider): instrumentor = OpenAIInstrumentor() instrumentor.instrument( tracer_provider=tracer_provider, logger_provider=logger_provider, meter_provider=meter_provider, ) instrumentor.uninstrument() instrumentor.instrument( tracer_provider=tracer_provider, logger_provider=logger_provider, meter_provider=meter_provider, ) instrumentor.uninstrument() instrumentor.instrument( tracer_provider=tracer_provider, logger_provider=logger_provider, meter_provider=meter_provider, ) instrumentor.uninstrument() @pytest.mark.vcr() def test_responses_create_basic(request, span_exporter, openai_client, instrument_no_content): _skip_if_not_latest() response = openai_client.responses.create( model=DEFAULT_MODEL, instructions=SYSTEM_INSTRUCTIONS, input=USER_ONLY_PROMPT[0]["content"], stream=False, ) (span,) = span_exporter.get_finished_spans() assert_all_attributes( span, DEFAULT_MODEL, True, response.id, response.model, response.usage.input_tokens, response.usage.output_tokens, response_service_tier=getattr(response, "service_tier", None), ) assert span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] == ("stop",) assert GenAIAttributes.GEN_AI_INPUT_MESSAGES not in span.attributes assert GenAIAttributes.GEN_AI_OUTPUT_MESSAGES not in span.attributes @pytest.mark.vcr() def test_responses_create_captures_content( request, span_exporter, log_exporter, openai_client, instrument_with_content, ): _skip_if_not_latest() response = openai_client.responses.create( model=DEFAULT_MODEL, instructions=SYSTEM_INSTRUCTIONS, input=USER_ONLY_PROMPT[0]["content"], stream=False, text={"format": {"type": "text"}}, ) (span,) = span_exporter.get_finished_spans() assert_all_attributes( span, DEFAULT_MODEL, True, response.id, response.model, response.usage.input_tokens, response.usage.output_tokens, response_service_tier=getattr(response, "service_tier", None), ) _assert_response_content(span, response, log_exporter) @pytest.mark.vcr() def test_responses_create_with_all_params(request, span_exporter, openai_client, instrument_no_content): _skip_if_not_latest() response = openai_client.responses.create( model=DEFAULT_MODEL, instructions=SYSTEM_INSTRUCTIONS, input=USER_ONLY_PROMPT[0]["content"], max_output_tokens=50, temperature=0.7, top_p=0.9, service_tier="default", text={"format": {"type": "text"}}, ) (span,) = span_exporter.get_finished_spans() assert_all_attributes( span, DEFAULT_MODEL, True, response.id, response.model, response.usage.input_tokens, response.usage.output_tokens, request_service_tier="default", response_service_tier=getattr(response, "service_tier", None), ) _assert_request_attrs( span, temperature=0.7, top_p=0.9, max_tokens=50, output_type="text", ) @pytest.mark.vcr() def test_responses_create_token_usage(request, span_exporter, openai_client, instrument_no_content): _skip_if_not_latest() response = openai_client.responses.create( model=DEFAULT_MODEL, instructions=SYSTEM_INSTRUCTIONS, input="Count to 5.", ) (span,) = span_exporter.get_finished_spans() assert span.attributes[GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS] == response.usage.input_tokens assert span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS] == response.usage.output_tokens @pytest.mark.vcr() def test_responses_create_aggregates_cache_tokens(request, span_exporter, openai_client, instrument_no_content): _skip_if_not_latest() response = openai_client.responses.create( model=DEFAULT_MODEL, instructions=SYSTEM_INSTRUCTIONS, input=USER_ONLY_PROMPT[0]["content"], ) (span,) = span_exporter.get_finished_spans() assert_cache_attributes(span, response.usage) @pytest.mark.vcr() def test_responses_create_stop_reason(request, span_exporter, openai_client, instrument_no_content): _skip_if_not_latest() openai_client.responses.create( model=DEFAULT_MODEL, instructions=SYSTEM_INSTRUCTIONS, input="Say hi.", ) (span,) = span_exporter.get_finished_spans() assert span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] == ("stop",) def test_responses_create_connection_error(span_exporter, instrument_no_content): _skip_if_not_latest() client = OpenAI(base_url="http://localhost:4242") with pytest.raises(APIConnectionError): client.responses.create( # pylint: disable=no-member model=DEFAULT_MODEL, input="Hello", timeout=0.1, ) (span,) = span_exporter.get_finished_spans() assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL assert span.attributes[ServerAttributes.SERVER_ADDRESS] == "localhost" assert span.attributes[ServerAttributes.SERVER_PORT] == 4242 assert span.attributes[ErrorAttributes.ERROR_TYPE] == "APIConnectionError" @pytest.mark.vcr() def test_responses_create_api_error(request, span_exporter, openai_client, instrument_no_content): _skip_if_not_latest() with pytest.raises((BadRequestError, NotFoundError)) as exc_info: openai_client.responses.create( model=INVALID_MODEL, input="Hello", ) (span,) = span_exporter.get_finished_spans() assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == INVALID_MODEL assert span.attributes[ErrorAttributes.ERROR_TYPE] == type(exc_info.value).__name__ @pytest.mark.vcr() def test_responses_create_streaming(request, span_exporter, openai_client, instrument_no_content): _skip_if_not_latest() with openai_client.responses.create( model=DEFAULT_MODEL, instructions=SYSTEM_INSTRUCTIONS, input=USER_ONLY_PROMPT[0]["content"], service_tier="default", stream=True, ) as stream: response = _collect_completed_response(stream) (span,) = span_exporter.get_finished_spans() assert_all_attributes( span, DEFAULT_MODEL, True, response.id, response.model, response.usage.input_tokens, response.usage.output_tokens, request_service_tier="default", response_service_tier=getattr(response, "service_tier", None), ) @pytest.mark.vcr() def test_responses_create_streaming_aggregates_cache_tokens( request, span_exporter, openai_client, instrument_no_content ): _skip_if_not_latest() with openai_client.responses.create( model=DEFAULT_MODEL, instructions=SYSTEM_INSTRUCTIONS, input=USER_ONLY_PROMPT[0]["content"], stream=True, ) as stream: response = _collect_completed_response(stream) (span,) = span_exporter.get_finished_spans() assert_cache_attributes(span, response.usage) @pytest.mark.vcr() def test_responses_create_streaming_captures_content( request, span_exporter, log_exporter, openai_client, instrument_with_content, ): _skip_if_not_latest() with openai_client.responses.create( model=DEFAULT_MODEL, instructions=SYSTEM_INSTRUCTIONS, input=USER_ONLY_PROMPT[0]["content"], stream=True, ) as stream: response = _collect_completed_response(stream) (span,) = span_exporter.get_finished_spans() assert_all_attributes( span, DEFAULT_MODEL, True, response.id, response.model, response.usage.input_tokens, response.usage.output_tokens, response_service_tier=getattr(response, "service_tier", None), ) _assert_response_content(span, response, log_exporter) @pytest.mark.vcr() def test_responses_create_streaming_iteration(request, span_exporter, openai_client, instrument_no_content): _skip_if_not_latest() stream = openai_client.responses.create( model=DEFAULT_MODEL, instructions=SYSTEM_INSTRUCTIONS, input="Say hi.", stream=True, ) events = list(stream) assert len(events) > 0 (span,) = span_exporter.get_finished_spans() assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL assert GenAIAttributes.GEN_AI_RESPONSE_ID in span.attributes assert GenAIAttributes.GEN_AI_RESPONSE_MODEL in span.attributes assert span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] == ("stop",) assert GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS in span.attributes assert GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS in span.attributes @pytest.mark.vcr() def test_responses_create_streaming_delegates_response_attribute(request, openai_client, instrument_no_content): _skip_if_not_latest() stream = openai_client.responses.create( model=DEFAULT_MODEL, instructions=SYSTEM_INSTRUCTIONS, input="Say hi.", stream=True, ) assert stream.response is not None assert stream.response.status_code == 200 assert stream.response.headers.get("x-request-id") is not None stream.close() def test_responses_create_streaming_connection_error(span_exporter, instrument_no_content): _skip_if_not_latest() client = OpenAI(base_url="http://localhost:4242") with pytest.raises(APIConnectionError): client.responses.create( # pylint: disable=no-member model=DEFAULT_MODEL, input="Hello", stream=True, timeout=0.1, ) (span,) = span_exporter.get_finished_spans() assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL assert span.attributes[ErrorAttributes.ERROR_TYPE] == "APIConnectionError" @pytest.mark.vcr() def test_responses_stream_wrapper_finalize_idempotent(request, span_exporter, openai_client, instrument_no_content): _skip_if_not_latest() stream = openai_client.responses.create( model=DEFAULT_MODEL, instructions=SYSTEM_INSTRUCTIONS, input=USER_ONLY_PROMPT[0]["content"], stream=True, ) response = _collect_completed_response(stream) stream.close() spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert_all_attributes( spans[0], DEFAULT_MODEL, True, response.id, response.model, response.usage.input_tokens, response.usage.output_tokens, response_service_tier=getattr(response, "service_tier", None), ) @pytest.mark.vcr() def test_responses_create_stream_propagation_error( request, span_exporter, openai_client, instrument_no_content, monkeypatch ): _skip_if_not_latest() stream = openai_client.responses.create( model=DEFAULT_MODEL, instructions=SYSTEM_INSTRUCTIONS, input=USER_ONLY_PROMPT[0]["content"], stream=True, ) class ErrorInjectingStreamDelegate: def __init__(self, inner): self._inner = inner self._count = 0 def __iter__(self): return self def __next__(self): if self._count == 1: raise ConnectionError("connection reset during stream") self._count += 1 return next(self._inner) def close(self): return self._inner.close() def __getattr__(self, name): return getattr(self._inner, name) monkeypatch.setattr(stream, "stream", ErrorInjectingStreamDelegate(stream.stream)) with pytest.raises(ConnectionError, match="connection reset during stream"): with stream: for _ in stream: pass (span,) = span_exporter.get_finished_spans() assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL assert span.attributes[ErrorAttributes.ERROR_TYPE] == "ConnectionError" @pytest.mark.vcr() def test_responses_create_streaming_user_exception(request, span_exporter, openai_client, instrument_no_content): _skip_if_not_latest() with pytest.raises(ValueError, match="User raised exception"): with openai_client.responses.create( model=DEFAULT_MODEL, instructions=SYSTEM_INSTRUCTIONS, input=USER_ONLY_PROMPT[0]["content"], stream=True, ) as stream: for _ in stream: raise ValueError("User raised exception") (span,) = span_exporter.get_finished_spans() assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL assert span.attributes[ErrorAttributes.ERROR_TYPE] == "ValueError" @pytest.mark.vcr() def test_responses_create_instrumentation_error_swallowed( request, span_exporter, openai_client, instrument_no_content, monkeypatch ): _skip_if_not_latest() def exploding_process_event(self, event): del self del event raise RuntimeError("instrumentation bug") monkeypatch.setattr(ResponseStreamWrapper, "process_event", exploding_process_event) with openai_client.responses.create( model=DEFAULT_MODEL, instructions=SYSTEM_INSTRUCTIONS, input=USER_ONLY_PROMPT[0]["content"], stream=True, ) as stream: events = list(stream) assert len(events) > 0 (span,) = span_exporter.get_finished_spans() assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL assert ErrorAttributes.ERROR_TYPE not in span.attributes @pytest.mark.vcr() @pytest.mark.skipif( not _has_tools_param, reason="openai SDK too old to support 'tools' parameter on Responses.create", ) def test_responses_create_captures_tool_call_content(request, span_exporter, openai_client, instrument_with_content): _skip_if_not_latest() openai_client.responses.create( model=DEFAULT_MODEL, input="What's the weather in Seattle right now?", tools=[get_responses_weather_tool_definition()], tool_choice={"type": "function", "name": "get_current_weather"}, ) (span,) = span_exporter.get_finished_spans() assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL assert span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] == ("tool_calls",) input_messages = _load_span_messages(span, GenAIAttributes.GEN_AI_INPUT_MESSAGES) assert input_messages[0]["role"] == "user" output_messages = _load_span_messages(span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES) tool_call_parts = [ part for message in output_messages for part in message.get("parts", []) if part.get("type") == "tool_call" ] assert len(tool_call_parts) > 0 assert tool_call_parts[0]["name"] == "get_current_weather" assert "arguments" in tool_call_parts[0] @pytest.mark.vcr() @pytest.mark.skipif( not _has_reasoning_param, reason=("openai SDK too old to support 'reasoning' parameter on Responses.create"), ) def test_responses_create_reports_reasoning_tokens(request, span_exporter, openai_client, instrument_with_content): _skip_if_not_latest() response = openai_client.responses.create( model=REASONING_MODEL, reasoning={"effort": "low"}, input=[ { "role": "user", "content": REASONING_PROMPT, } ], max_output_tokens=300, timeout=30.0, ) reasoning_tokens = getattr( getattr(response.usage, "output_tokens_details", None), "reasoning_tokens", None, ) assert reasoning_tokens is not None assert reasoning_tokens > 0 spans = span_exporter.get_finished_spans() assert len(spans) == 1 (span,) = spans assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == (REASONING_MODEL) assert span.attributes[GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS] == response.usage.input_tokens assert span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS] == response.usage.output_tokens assert span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] == ("stop",) output_messages = _load_span_messages(span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES) assert len(output_messages) > 0 @pytest.mark.vcr() def test_responses_create_with_content_span_unsampled( request, span_exporter, log_exporter, openai_client, instrument_with_content_unsampled, ): _skip_if_not_latest() openai_client.responses.create( model=DEFAULT_MODEL, instructions=SYSTEM_INSTRUCTIONS, input=USER_ONLY_PROMPT[0]["content"], stream=False, ) assert len(span_exporter.get_finished_spans()) == 0 assert len(log_exporter.get_finished_logs()) == 0 @pytest.mark.vcr() def test_responses_create_with_content_shapes( request, span_exporter, log_exporter, openai_client, instrument_with_content, ): _skip_if_not_latest() openai_client.responses.create( model=DEFAULT_MODEL, instructions=SYSTEM_INSTRUCTIONS, input=USER_ONLY_PROMPT[0]["content"], stream=False, ) (span,) = span_exporter.get_finished_spans() input_messages = _load_span_messages(span, GenAIAttributes.GEN_AI_INPUT_MESSAGES) output_messages = _load_span_messages(span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES) assert input_messages[0]["role"] == "user" assert input_messages[0]["parts"][0]["type"] == "text" assert output_messages[0]["role"] == "assistant" assert output_messages[0]["parts"][0]["type"] == "text" assert len(log_exporter.get_finished_logs()) == 0 @pytest.mark.vcr() def test_responses_create_event_only_no_content_in_span( request, span_exporter, log_exporter, openai_client, instrument_event_only ): _skip_if_not_latest() openai_client.responses.create( model=DEFAULT_MODEL, instructions=SYSTEM_INSTRUCTIONS, input=USER_ONLY_PROMPT[0]["content"], stream=False, ) (span,) = span_exporter.get_finished_spans() assert GenAIAttributes.GEN_AI_INPUT_MESSAGES not in span.attributes assert GenAIAttributes.GEN_AI_OUTPUT_MESSAGES not in span.attributes assert GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS not in span.attributes logs = log_exporter.get_finished_logs() assert len(logs) == 1 assert logs[0].log_record.event_name == "gen_ai.client.inference.operation.details" test_utils.py000066400000000000000000000232231527070072300376620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Shared test utilities for OpenAI instrumentation tests.""" import json from typing import Any from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, ) from opentelemetry.semconv._incubating.attributes import ( openai_attributes as OpenAIAttributes, ) from opentelemetry.semconv._incubating.attributes import ( server_attributes as ServerAttributes, ) DEFAULT_MODEL = "gpt-4o-mini" DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small" GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS = "gen_ai.usage.cache_creation.input_tokens" GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.cache_read.input_tokens" USER_ONLY_PROMPT = [{"role": "user", "content": "Say this is a test"}] USER_ONLY_EXPECTED_INPUT_MESSAGES = [ { "role": "user", "parts": [ { "type": "text", "content": USER_ONLY_PROMPT[0]["content"], } ], } ] WEATHER_TOOL_PROMPT = [ {"role": "system", "content": "You're a helpful assistant."}, { "role": "user", "content": "What's the weather in Seattle and San Francisco today?", }, ] WEATHER_TOOL_EXPECTED_INPUT_MESSAGES = [ { "role": "system", "parts": [ { "type": "text", "content": WEATHER_TOOL_PROMPT[0]["content"], } ], }, { "role": "user", "parts": [ { "type": "text", "content": WEATHER_TOOL_PROMPT[1]["content"], } ], }, ] def _assert_optional_attribute(span, attribute_name, expected_value): """Helper to assert optional span attributes.""" if expected_value is not None: assert expected_value == span.attributes[attribute_name] else: assert attribute_name not in span.attributes def assert_all_attributes( span: ReadableSpan, request_model: str, latest_experimental_enabled: bool, response_id: str = None, response_model: str = None, input_tokens: int | None = None, output_tokens: int | None = None, operation_name: str = "chat", server_address: str = "api.openai.com", server_port: int = 443, request_service_tier: str | None = None, response_service_tier: str | None = None, ): assert span.name == f"{operation_name} {request_model}" assert operation_name == span.attributes[GenAIAttributes.GEN_AI_OPERATION_NAME] provider_name_attr_name = "gen_ai.provider.name" if latest_experimental_enabled else GenAIAttributes.GEN_AI_SYSTEM assert GenAIAttributes.GenAiProviderNameValues.OPENAI.value == span.attributes[provider_name_attr_name] assert request_model == span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] _assert_optional_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, response_model) _assert_optional_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_ID, response_id) _assert_optional_attribute(span, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) _assert_optional_attribute(span, GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens) assert server_address == span.attributes[ServerAttributes.SERVER_ADDRESS] if server_port != 443 and server_port > 0: assert server_port == span.attributes[ServerAttributes.SERVER_PORT] request_service_tier_attr_name = ( OpenAIAttributes.OPENAI_REQUEST_SERVICE_TIER if latest_experimental_enabled else GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER ) _assert_optional_attribute( span, request_service_tier_attr_name, request_service_tier, ) response_service_tier_attr_name = ( OpenAIAttributes.OPENAI_RESPONSE_SERVICE_TIER if latest_experimental_enabled else GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER ) _assert_optional_attribute( span, response_service_tier_attr_name, response_service_tier, ) def assert_log_parent(log, span): """Assert that the log record has the correct parent span context""" if span: assert log.log_record.trace_id == span.get_span_context().trace_id assert log.log_record.span_id == span.get_span_context().span_id assert log.log_record.trace_flags == span.get_span_context().trace_flags def get_current_weather_tool_definition(): return { "type": "function", "function": { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. Boston, MA", }, }, "required": ["location"], "additionalProperties": False, }, }, } def get_responses_weather_tool_definition(): return { "type": "function", "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. Boston, MA", }, }, "required": ["location"], "additionalProperties": False, }, "strict": True, } EXPECTED_TOOL_DEFINITIONS = [ { "type": "function", "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. Boston, MA", }, }, "required": ["location"], "additionalProperties": False, }, } ] def remove_none_values(body): """Remove None values from a dictionary recursively""" result = {} for key, value in body.items(): if value is None: continue if isinstance(value, dict): result[key] = remove_none_values(value) elif isinstance(value, list): result[key] = [remove_none_values(i) if isinstance(i, dict) else i for i in value] else: result[key] = value return result def assert_completion_attributes( span: ReadableSpan, request_model: str, response: Any, latest_experimental_enabled: bool, operation_name: str = "chat", server_address: str = "api.openai.com", ): return assert_all_attributes( span, request_model, latest_experimental_enabled, response.id, response.model, response.usage.prompt_tokens, response.usage.completion_tokens, operation_name, server_address, ) def assert_messages_attribute(actual, expected): assert json.loads(actual) == expected def format_simple_expected_output_message(content: str, finish_reason: str = "stop"): return [ { "role": "assistant", "parts": [ { "type": "text", "content": content, } ], "finish_reason": finish_reason, } ] def _get_usage_details(usage): return getattr(usage, "input_tokens_details", None) or getattr(usage, "prompt_tokens_details", None) def assert_cache_attributes(span, usage): details = _get_usage_details(usage) assert details is not None cached_tokens = getattr(details, "cached_tokens", None) if cached_tokens is None: assert GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS not in span.attributes else: assert span.attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] == cached_tokens cache_creation = getattr(details, "cache_creation_input_tokens", None) if cache_creation is None: assert GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS not in span.attributes else: assert span.attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] == cache_creation def assert_message_in_logs(log, event_name, expected_content, parent_span): assert log.log_record.event_name == event_name assert log.log_record.attributes[GenAIAttributes.GEN_AI_SYSTEM] == GenAIAttributes.GenAiSystemValues.OPENAI.value if not expected_content: assert not log.log_record.body else: assert log.log_record.body assert dict(log.log_record.body) == remove_none_values(expected_content) assert_log_parent(log, parent_span) def assert_embedding_attributes( span: ReadableSpan, request_model: str, latest_experimental_enabled: bool, response, ): """Assert that the span contains all required attributes for embeddings operation""" # Use the common assertion function assert_all_attributes( span, request_model, latest_experimental_enabled, response_id=None, # Embeddings don't have a response ID response_model=response.model, input_tokens=response.usage.prompt_tokens, operation_name="embeddings", server_address="api.openai.com", ) # Assert embeddings-specific attributes if hasattr(span, "attributes") and "gen_ai.embeddings.dimension.count" in span.attributes: # If dimensions were specified, verify that they match the actual dimensions assert span.attributes["gen_ai.embeddings.dimension.count"] == len(response.data[0].embedding) opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/000077500000000000000000000000001527070072300340535ustar00rootroot00000000000000CHANGELOG.md000066400000000000000000000067501527070072300356150ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai# Changelog > [!WARNING] > This package is deprecated and will not be replaced — it only receives security patches and will be removed in the future. > Google [deprecated the generative AI modules of the Vertex AI SDK](https://cloud.google.com/vertex-ai/generative-ai/docs/deprecations/genai-vertexai-sdk) > that this package instruments. All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased - Fix compatibility with wrapt 2.x by using positional arguments in `wrap_function_wrapper()` calls ([#4445](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4445)) ## Version 2.2b0 (2025-12-19) - Fix overwritten log attributes in vertexai instrumentation ([#3925](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3925)) - Add the `GEN_AI_OUTPUT_TYPE`, `GEN_AI_REQUEST_CHOICE_COUNT` and `GEN_AI_REQUEST_SEED` sem conv request attributes to events/spans generated in the stable instrumentation. This was added pre sem conv 1.36 so it should be in the stable instrumentation. See ([#4011](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4011)). ## Version 2.1b0 (2025-10-16) - Update instrumentation to use the latest semantic convention changes made in https://github.com/open-telemetry/semantic-conventions/pull/2179. Now only a single event and span (`gen_ai.client.inference.operation.details`) are used to capture prompt and response content. These changes are opt-in, users will need to set the environment variable OTEL_SEMCONV_STABILITY_OPT_IN to `gen_ai_latest_experimental` to see them ([#3799](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3799)) and ([#3709](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3709)). Update instrumentation to call upload hook. - Implement uninstrument for `opentelemetry-instrumentation-vertexai` ([#3328](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3328)) - VertexAI support for async calling ([#3386](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3386)) - Migrate off the deprecated events API to use the logs API ([#3625](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3626)) - Update `gen_ai_latest_experimental` instrumentation to record files being passed to the model ([#3840](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3840)). ## Version 2.0b0 (2025-02-24) - Added Vertex AI spans for request parameters ([#3192](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3192)) - Initial VertexAI instrumentation ([#3123](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3123)) - Add server attributes to Vertex AI spans ([#3208](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3208)) - VertexAI emit user, system, and assistant events ([#3203](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3203)) - Add Vertex gen AI response attributes and `gen_ai.choice` events ([#3227](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3227)) - VertexAI stop serializing unset fields into event ([#3236](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3236)) - Vertex capture tool requests and responses ([#3255](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3255)) LICENSE000066400000000000000000000261351527070072300350100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000055161527070072300354720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexaiOpenTelemetry VertexAI Instrumentation ====================================== .. warning:: This package is deprecated as part of the GenAI package migration. It is not being migrated to the `opentelemetry-python-genai `_ repository, and no replacement package is planned there. |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-vertexai.svg :target: https://pypi.org/project/opentelemetry-instrumentation-vertexai/ This library allows tracing LLM requests and logging of messages made by the `VertexAI Python API library `_. Installation ------------ If your application is already instrumented with OpenTelemetry, add this package to your requirements. :: pip install opentelemetry-instrumentation-vertexai If you don't have an VertexAI application, yet, try our `examples `_. Check out `zero-code example `_ for a quick start. Usage ----- This section describes how to set up VertexAI instrumentation if you're setting OpenTelemetry up manually. Check out the `manual example `_ for more details. Instrumenting all clients ************************* When using the instrumentor, all clients will automatically trace VertexAI chat completion operations. You can also optionally capture prompts and completions as log events. Make sure to configure OpenTelemetry tracing, logging, and events to capture all telemetry emitted by the instrumentation. .. code-block:: python from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor from vertexai.generative_models import GenerativeModel VertexAIInstrumentor().instrument() vertexai.init() model = GenerativeModel("gemini-1.5-flash-002") response = model.generate_content("Write a short poem on OpenTelemetry.") Enabling message content ************************* Message content such as the contents of the prompt, completion, function arguments and return values are not captured by default. To capture message content as log events, set the environment variable `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` to `true`. Uninstrument ************ To uninstrument clients, call the uninstrument method: .. code-block:: python from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor VertexAIInstrumentor().instrument() # ... # Uninstrument all clients VertexAIInstrumentor().uninstrument() References ---------- * `OpenTelemetry VertexAI Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ examples/000077500000000000000000000000001527070072300356125ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexaimanual/000077500000000000000000000000001527070072300370675ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/examples.env000066400000000000000000000020121527070072300376530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/examples/manual# Uncomment and change to your OTLP endpoint # OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 # OTEL_EXPORTER_OTLP_PROTOCOL=grpc OTEL_SERVICE_NAME=opentelemetry-python-vertexai # Change to 'false' to hide prompt and completion content OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true # Alternatively set this env var to enable the latest semantic conventions: OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental # When using the latest experimental flag this env var controls which telemetry signals will have prompt and response content included in them. # Choices are NO_CONTENT, SPAN_ONLY, EVENT_ONLY, SPAN_AND_EVENT. OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_AND_EVENT # Optional hook that will upload prompt and response content to some external destination. # For example fsspec. OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK = "upload" # Required if using a completion hook. The path to upload content to for example gs://my_bucket. OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH = "gs://my_bucket"README.rst000066400000000000000000000024231527070072300405570ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/examples/manualOpenTelemetry VertexAI Instrumentation Example ============================================== This is an example of how to instrument VertexAI calls when configuring OpenTelemetry SDK and Instrumentations manually. When `main.py `_ is run, it exports traces and logs to an OTLP compatible endpoint. Traces include details such as the model used and the duration of the chat request. Logs capture the chat request and the generated response, providing a comprehensive view of the performance and behavior of your VertexAI requests. Note: `.env <.env>`_ file configures additional environment variables: - `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true` configures VertexAI instrumentation to capture prompt and completion contents on events. Setup ----- An OTLP compatible endpoint should be listening for traces and logs on http://localhost:4317. If not, update "OTEL_EXPORTER_OTLP_ENDPOINT" as well. Next, set up a virtual environment like this: :: python3 -m venv .venv source .venv/bin/activate pip install "python-dotenv[cli]" pip install -r requirements.txt Run --- Run the example like this: :: dotenv run -- python main.py You should see a poem generated by VertexAI while traces and logs export to your configured observability tool. main.py000066400000000000000000000025461527070072300403740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/examples/manual# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: skip-file import vertexai from vertexai.generative_models import GenerativeModel # NOTE: OpenTelemetry Python Logs and Events APIs are in beta from opentelemetry import _logs, trace from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( OTLPLogExporter, ) from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import BatchLogRecordProcessor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor # configure tracing trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(OTLPSpanExporter())) # configure logging and events _logs.set_logger_provider(LoggerProvider()) _logs.get_logger_provider().add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter())) # instrument VertexAI VertexAIInstrumentor().instrument() def main(): vertexai.init() model = GenerativeModel("gemini-1.5-flash-002") chat_completion = model.generate_content("Write a short poem on OpenTelemetry.") print(chat_completion.text) if __name__ == "__main__": main() requirements.txt000066400000000000000000000002261527070072300423530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/examples/manualgoogle-cloud-aiplatform>=1.64 opentelemetry-sdk~=1.29.0 opentelemetry-exporter-otlp-proto-grpc~=1.29.0 opentelemetry-instrumentation-vertexai~=2.0b0 zero-code/000077500000000000000000000000001527070072300375015ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/examples.env000066400000000000000000000021401527070072300402670ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/examples/zero-code# Uncomment and change to your OTLP endpoint # OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 # OTEL_EXPORTER_OTLP_PROTOCOL=grpc OTEL_SERVICE_NAME=opentelemetry-python-vertexai # Uncomment if your OTLP endpoint doesn't support logs # OTEL_LOGS_EXPORTER=console # Change to 'false' to hide prompt and completion content OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true # Alternatively set this env var to enable the latest semantic conventions: OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental # When using the latest experimental flag this env var controls which telemetry signals will have prompt and response content included in them. # Choices are NO_CONTENT, SPAN_ONLY, EVENT_ONLY, SPAN_AND_EVENT. OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_AND_EVENT # Optional hook that will upload prompt and response content to some external destination. # For example fsspec. OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK = "upload" # Required if using a completion hook. The path to upload content to for example gs://my_bucket. OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH = "gs://my_bucket" README.rst000066400000000000000000000021021527070072300411630ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/examples/zero-codeOpenTelemetry VertexAI Instrumentation Example ============================================== This is an example of how to instrument VertexAI calls with zero code changes, using `opentelemetry-instrument`. When `main.py `_ is run, it exports traces and logs to an OTLP compatible endpoint. Traces include details such as the model used and the duration of the chat request. Logs capture the chat request and the generated response, providing a comprehensive view of the performance and behavior of your VertexAI requests. Setup ----- An OTLP compatible endpoint should be listening for traces and logs on http://localhost:4318. If not, update "OTEL_EXPORTER_OTLP_ENDPOINT" as well. Next, set up a virtual environment like this: :: python3 -m venv .venv source .venv/bin/activate pip install "python-dotenv[cli]" pip install -r requirements.txt Run --- Run the example like this: :: dotenv run -- opentelemetry-instrument python main.py You should see a poem generated by VertexAI while traces and logs export to your configured observability tool. main.py000066400000000000000000000006071527070072300410020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/examples/zero-code# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import vertexai from vertexai.generative_models import GenerativeModel def main(): vertexai.init() model = GenerativeModel("gemini-1.5-flash-002") chat_completion = model.generate_content("Write a short poem on OpenTelemetry.") print(chat_completion.text) if __name__ == "__main__": main() requirements.txt000066400000000000000000000002631527070072300427660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/examples/zero-codegoogle-cloud-aiplatform>=1.64 opentelemetry-sdk~=1.28.2 opentelemetry-exporter-otlp-proto-http~=1.28.2 opentelemetry-distro~=0.49b2 opentelemetry-instrumentation-vertexai~=2.0b0 pyproject.toml000066400000000000000000000032431527070072300367120ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-vertexai" dynamic = ["version"] description = "OpenTelemetry Official VertexAI instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.40", "opentelemetry-instrumentation ~= 0.61b0", "opentelemetry-semantic-conventions ~= 0.61b0", "opentelemetry-util-genai >= 0.4b0, <0.5b0", ] [project.optional-dependencies] instruments = ["google-cloud-aiplatform >= 1.64"] [project.entry-points.opentelemetry_instrumentor] vertexai = "opentelemetry.instrumentation.vertexai:VertexAIInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation-genai/opentelemetry-instrumentation-vertexai" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/vertexai/version.py" [tool.hatch.build.targets.sdist] include = ["/src", "/tests"] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] src/000077500000000000000000000000001527070072300345635ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexaiopentelemetry/000077500000000000000000000000001527070072300374575ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/srcinstrumentation/000077500000000000000000000000001527070072300427225ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetryvertexai/000077500000000000000000000000001527070072300445515ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation__init__.py000066400000000000000000000125411527070072300466650ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ VertexAI client instrumentation supporting `google-cloud-aiplatform` SDK, it can be enabled by using ``VertexAIInstrumentor``. .. _vertexai: https://pypi.org/project/google-cloud-aiplatform/ Usage ----- .. code:: python import vertexai from vertexai.generative_models import GenerativeModel from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor VertexAIInstrumentor().instrument() vertexai.init() model = GenerativeModel("gemini-1.5-flash-002") chat_completion = model.generate_content( "Write a short poem on OpenTelemetry." ) API --- """ from __future__ import annotations from collections.abc import Collection from typing import Any from wrapt import ( wrap_function_wrapper, # type: ignore[reportUnknownVariableType] ) from opentelemetry._logs import get_logger from opentelemetry.instrumentation._semconv import ( _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _StabilityMode, ) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap from opentelemetry.instrumentation.vertexai.package import _instruments from opentelemetry.instrumentation.vertexai.patch import MethodWrappers from opentelemetry.instrumentation.vertexai.utils import is_content_enabled from opentelemetry.semconv.schemas import Schemas from opentelemetry.trace import get_tracer from opentelemetry.util.genai.completion_hook import load_completion_hook def _methods_to_wrap( method_wrappers: MethodWrappers, ): # This import is very slow, do it lazily in case instrument() is not called # pylint: disable=import-outside-toplevel from google.cloud.aiplatform_v1.services.prediction_service import ( # noqa: PLC0415 async_client, client, ) from google.cloud.aiplatform_v1beta1.services.prediction_service import ( # noqa: PLC0415 async_client as async_client_v1beta1, ) from google.cloud.aiplatform_v1beta1.services.prediction_service import ( # noqa: PLC0415 client as client_v1beta1, ) for client_class in ( client.PredictionServiceClient, client_v1beta1.PredictionServiceClient, ): yield ( client_class, client_class.generate_content.__name__, # type: ignore[reportUnknownMemberType] method_wrappers.generate_content, ) for client_class in ( async_client.PredictionServiceAsyncClient, async_client_v1beta1.PredictionServiceAsyncClient, ): yield ( client_class, client_class.generate_content.__name__, # type: ignore[reportUnknownMemberType] method_wrappers.agenerate_content, ) class VertexAIInstrumentor(BaseInstrumentor): def __init__(self) -> None: super().__init__() self._methods_to_unwrap: list[tuple[Any, str]] = [] def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs: Any): """Enable VertexAI instrumentation.""" completion_hook = kwargs.get("completion_hook") or load_completion_hook() sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.GEN_AI, ) tracer_provider = kwargs.get("tracer_provider") schema = Schemas.V1_28_0.value if sem_conv_opt_in_mode == _StabilityMode.DEFAULT else Schemas.V1_36_0.value tracer = get_tracer( __name__, "", tracer_provider, schema_url=schema, ) logger_provider = kwargs.get("logger_provider") logger = get_logger( __name__, "", logger_provider=logger_provider, schema_url=schema, ) sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.GEN_AI, ) if sem_conv_opt_in_mode == _StabilityMode.DEFAULT: # Type checker now knows sem_conv_opt_in_mode is a Literal[_StabilityMode.DEFAULT] method_wrappers = MethodWrappers( tracer, logger, is_content_enabled(sem_conv_opt_in_mode), sem_conv_opt_in_mode, completion_hook, ) elif sem_conv_opt_in_mode == _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL: # Type checker now knows it's the other literal method_wrappers = MethodWrappers( tracer, logger, is_content_enabled(sem_conv_opt_in_mode), sem_conv_opt_in_mode, completion_hook, ) else: raise RuntimeError(f"{sem_conv_opt_in_mode} mode not supported") for client_class, method_name, wrapper in _methods_to_wrap(method_wrappers): wrap_function_wrapper( client_class, method_name, wrapper, ) self._methods_to_unwrap.append((client_class, method_name)) def _uninstrument(self, **kwargs: Any) -> None: for client_class, method_name in self._methods_to_unwrap: unwrap(client_class, method_name) events.py000066400000000000000000000112451527070072300464320ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore[reportUnknownDeprecated] """ Factories for event types described in https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-events.md#system-event. Hopefully this code can be autogenerated by Weaver once Gen AI semantic conventions are schematized in YAML and the Weaver tool supports it. """ from __future__ import annotations from collections.abc import Iterable from dataclasses import asdict, dataclass from typing import Any, Literal from opentelemetry._logs import LogRecord from opentelemetry.semconv._incubating.attributes import gen_ai_attributes from opentelemetry.util.types import AnyValue def user_event( *, role: str = "user", content: AnyValue = None, ) -> LogRecord: """Creates a User event https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#user-event """ body: dict[str, AnyValue] = { "role": role, } if content is not None: body["content"] = content return LogRecord( event_name="gen_ai.user.message", attributes={ gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value, }, body=body, ) def assistant_event( *, role: str = "assistant", content: AnyValue = None, ) -> LogRecord: """Creates an Assistant event https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#assistant-event """ body: dict[str, AnyValue] = { "role": role, } if content is not None: body["content"] = content return LogRecord( event_name="gen_ai.assistant.message", attributes={ gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value, }, body=body, ) def system_event( *, role: str = "system", content: AnyValue = None, ) -> LogRecord: """Creates a System event https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#system-event """ body: dict[str, AnyValue] = { "role": role, } if content is not None: body["content"] = content return LogRecord( event_name="gen_ai.system.message", attributes={ gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value, }, body=body, ) def tool_event( *, role: str | None, id_: str, content: AnyValue = None, ) -> LogRecord: """Creates a Tool message event https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#event-gen_aitoolmessage """ if not role: role = "tool" body: dict[str, AnyValue] = { "role": role, "id": id_, } if content is not None: body["content"] = content return LogRecord( event_name="gen_ai.tool.message", attributes={ gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value, }, body=body, ) @dataclass class ChoiceMessage: """The message field for a gen_ai.choice event""" content: AnyValue = None role: str = "assistant" @dataclass class ChoiceToolCall: """The tool_calls field for a gen_ai.choice event""" @dataclass class Function: name: str arguments: AnyValue = None function: Function id: str type: Literal["function"] = "function" FinishReason = Literal["content_filter", "error", "length", "stop", "tool_calls"] def choice_event( *, finish_reason: FinishReason | str, index: int, message: ChoiceMessage, tool_calls: Iterable[ChoiceToolCall] = (), ) -> LogRecord: """Creates a choice event, which describes the Gen AI response message. https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/gen-ai/gen-ai-events.md#event-gen_aichoice """ body: dict[str, AnyValue] = { "finish_reason": finish_reason, "index": index, "message": _asdict_filter_nulls(message), } tool_calls_list = [_asdict_filter_nulls(tool_call) for tool_call in tool_calls] if tool_calls_list: body["tool_calls"] = tool_calls_list return LogRecord( event_name="gen_ai.choice", attributes={ gen_ai_attributes.GEN_AI_SYSTEM: gen_ai_attributes.GenAiSystemValues.VERTEX_AI.value, }, body=body, ) def _asdict_filter_nulls(instance: Any) -> dict[str, AnyValue]: return asdict( instance, dict_factory=lambda kvs: {k: v for (k, v) in kvs if v is not None}, ) package.py000066400000000000000000000002021527070072300465100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("google-cloud-aiplatform >= 1.64",) patch.py000066400000000000000000000277001527070072300462300ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Awaitable, Callable, MutableSequence from contextlib import contextmanager from dataclasses import asdict from typing import ( TYPE_CHECKING, Any, Literal, cast, overload, ) from opentelemetry._logs import Logger, LogRecord from opentelemetry.instrumentation._semconv import ( _StabilityMode, ) from opentelemetry.instrumentation.vertexai.utils import ( GenerateContentParams, _map_finish_reason, convert_content_to_message_parts, get_genai_request_attributes, get_genai_response_attributes, get_server_attributes, get_span_name, request_to_events, response_to_events, ) from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAI, ) from opentelemetry.trace import SpanKind, Tracer from opentelemetry.util.genai.completion_hook import CompletionHook from opentelemetry.util.genai.types import ( ContentCapturingMode, InputMessage, OutputMessage, ) from opentelemetry.util.genai.utils import gen_ai_json_dumps if TYPE_CHECKING: from google.cloud.aiplatform_v1.services.prediction_service import client from google.cloud.aiplatform_v1.types import ( content, prediction_service, ) from google.cloud.aiplatform_v1beta1.services.prediction_service import ( client as client_v1beta1, ) from google.cloud.aiplatform_v1beta1.types import ( content as content_v1beta1, ) from google.cloud.aiplatform_v1beta1.types import ( prediction_service as prediction_service_v1beta1, ) # Use parameter signature from # https://github.com/googleapis/python-aiplatform/blob/v1.76.0/google/cloud/aiplatform_v1/services/prediction_service/client.py#L2088 # to handle named vs positional args robustly def _extract_params( request: prediction_service.GenerateContentRequest | prediction_service_v1beta1.GenerateContentRequest | dict[Any, Any] | None = None, *, model: str | None = None, contents: MutableSequence[content.Content] | MutableSequence[content_v1beta1.Content] | None = None, **_kwargs: Any, ) -> GenerateContentParams: # Request vs the named parameters are mututally exclusive or the RPC will fail if not request: return GenerateContentParams( model=model or "", contents=contents, ) if isinstance(request, dict): return GenerateContentParams(**request) return GenerateContentParams( model=request.model, contents=request.contents, system_instruction=request.system_instruction, tools=request.tools, tool_config=request.tool_config, labels=request.labels, safety_settings=request.safety_settings, generation_config=request.generation_config, ) # For details about GEN_AI_LATEST_EXPERIMENTAL stability mode see # https://github.com/open-telemetry/semantic-conventions/blob/v1.37.0/docs/gen-ai/gen-ai-agent-spans.md?plain=1#L18-L37 class MethodWrappers: @overload def __init__( self, tracer: Tracer, logger: Logger, capture_content: ContentCapturingMode, sem_conv_opt_in_mode: Literal[_StabilityMode.GEN_AI_LATEST_EXPERIMENTAL], completion_hook: CompletionHook, ) -> None: ... @overload def __init__( self, tracer: Tracer, logger: Logger, capture_content: bool, sem_conv_opt_in_mode: Literal[_StabilityMode.DEFAULT], completion_hook: CompletionHook, ) -> None: ... def __init__( self, tracer: Tracer, logger: Logger, capture_content: bool | ContentCapturingMode, sem_conv_opt_in_mode: Literal[_StabilityMode.DEFAULT, _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL], completion_hook: CompletionHook, ) -> None: self.tracer = tracer self.logger = logger self.capture_content = capture_content self.sem_conv_opt_in_mode = sem_conv_opt_in_mode self.completion_hook = completion_hook @contextmanager def _with_new_instrumentation( self, capture_content: ContentCapturingMode, instance: client.PredictionServiceClient | client_v1beta1.PredictionServiceClient, args: Any, kwargs: Any, ): params = _extract_params(*args, **kwargs) request_attributes = get_genai_request_attributes(True, params) with self.tracer.start_as_current_span( name=f"{GenAI.GenAiOperationNameValues.CHAT.value} {request_attributes.get(GenAI.GEN_AI_REQUEST_MODEL, '')}".strip(), # pyright: ignore[reportDeprecated] kind=SpanKind.CLIENT, ) as span: def handle_response( response: prediction_service.GenerateContentResponse | prediction_service_v1beta1.GenerateContentResponse | None, ) -> None: attributes = ( get_server_attributes(instance.api_endpoint) | request_attributes | get_genai_response_attributes(response) ) event = LogRecord( event_name="gen_ai.client.inference.operation.details", ) event.attributes = attributes.copy() system_instructions, inputs, outputs = [], [], [] if params.system_instruction: system_instructions = convert_content_to_message_parts(params.system_instruction) if params.contents: inputs = [ InputMessage( role=content.role, parts=convert_content_to_message_parts(content), ) for content in params.contents ] if response: outputs = [ OutputMessage( finish_reason=_map_finish_reason(candidate.finish_reason), role=candidate.content.role, parts=convert_content_to_message_parts(candidate.content), ) for candidate in response.candidates ] self.completion_hook.on_completion( inputs=inputs, outputs=outputs, system_instruction=system_instructions, span=span, log_record=event, ) content_attributes = { k: [asdict(x) for x in v] for k, v in [ ( GenAI.GEN_AI_SYSTEM_INSTRUCTIONS, system_instructions, ), (GenAI.GEN_AI_INPUT_MESSAGES, inputs), (GenAI.GEN_AI_OUTPUT_MESSAGES, outputs), ] if v } if span.is_recording(): span.set_attributes(attributes) if capture_content in ( ContentCapturingMode.SPAN_AND_EVENT, ContentCapturingMode.SPAN_ONLY, ): span.set_attributes({k: gen_ai_json_dumps(v) for k, v in content_attributes.items()}) if capture_content in ( ContentCapturingMode.SPAN_AND_EVENT, ContentCapturingMode.EVENT_ONLY, ): event.attributes |= content_attributes self.logger.emit(event) yield handle_response @contextmanager def _with_default_instrumentation( self, capture_content: bool, instance: client.PredictionServiceClient | client_v1beta1.PredictionServiceClient, args: Any, kwargs: Any, ): params = _extract_params(*args, **kwargs) api_endpoint: str = instance.api_endpoint span_attributes = { **get_genai_request_attributes(False, params), **get_server_attributes(api_endpoint), } span_name = get_span_name(span_attributes) with self.tracer.start_as_current_span( name=span_name, kind=SpanKind.CLIENT, attributes=span_attributes, ) as span: for event in request_to_events(params=params, capture_content=capture_content): self.logger.emit(event) # TODO: set error.type attribute # https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md def handle_response( response: prediction_service.GenerateContentResponse | prediction_service_v1beta1.GenerateContentResponse, ) -> None: if span.is_recording(): # When streaming, this is called multiple times so attributes would be # overwritten. In practice, it looks the API only returns the interesting # attributes on the last streamed response. However, I couldn't find # documentation for this and setting attributes shouldn't be too expensive. span.set_attributes(get_genai_response_attributes(response)) for event in response_to_events(response=response, capture_content=capture_content): self.logger.emit(event) yield handle_response def generate_content( self, wrapped: Callable[ ..., prediction_service.GenerateContentResponse | prediction_service_v1beta1.GenerateContentResponse, ], instance: client.PredictionServiceClient | client_v1beta1.PredictionServiceClient, args: Any, kwargs: Any, ) -> prediction_service.GenerateContentResponse | prediction_service_v1beta1.GenerateContentResponse: if self.sem_conv_opt_in_mode == _StabilityMode.DEFAULT: capture_content_bool = cast(bool, self.capture_content) with self._with_default_instrumentation(capture_content_bool, instance, args, kwargs) as handle_response: response = wrapped(*args, **kwargs) handle_response(response) return response else: capture_content = cast(ContentCapturingMode, self.capture_content) with self._with_new_instrumentation(capture_content, instance, args, kwargs) as handle_response: response = None try: response = wrapped(*args, **kwargs) return response finally: handle_response(response) async def agenerate_content( self, wrapped: Callable[ ..., Awaitable[prediction_service.GenerateContentResponse | prediction_service_v1beta1.GenerateContentResponse], ], instance: client.PredictionServiceClient | client_v1beta1.PredictionServiceClient, args: Any, kwargs: Any, ) -> prediction_service.GenerateContentResponse | prediction_service_v1beta1.GenerateContentResponse: if self.sem_conv_opt_in_mode == _StabilityMode.DEFAULT: capture_content_bool = cast(bool, self.capture_content) with self._with_default_instrumentation(capture_content_bool, instance, args, kwargs) as handle_response: response = await wrapped(*args, **kwargs) handle_response(response) return response else: capture_content = cast(ContentCapturingMode, self.capture_content) with self._with_new_instrumentation(capture_content, instance, args, kwargs) as handle_response: response = None try: response = await wrapped(*args, **kwargs) return response finally: handle_response(response) py.typed000066400000000000000000000000001527070072300462360ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexaiutils.py000066400000000000000000000335171527070072300462740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore[reportUnknownDeprecated] from __future__ import annotations import logging import re from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass from os import environ from typing import ( TYPE_CHECKING, Literal, cast, overload, ) from urllib.parse import urlparse from google.protobuf import json_format from opentelemetry._logs import LogRecord from opentelemetry.instrumentation._semconv import ( _StabilityMode, ) from opentelemetry.instrumentation.vertexai.events import ( ChoiceMessage, ChoiceToolCall, assistant_event, choice_event, system_event, tool_event, user_event, ) from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, ) from opentelemetry.semconv.attributes import server_attributes from opentelemetry.util.genai.types import ( Blob, ContentCapturingMode, FinishReason, MessagePart, Text, ToolCallRequest, ToolCallResponse, Uri, ) from opentelemetry.util.genai.utils import get_content_capturing_mode from opentelemetry.util.types import AnyValue, AttributeValue if TYPE_CHECKING: from google.cloud.aiplatform_v1.types import ( content, prediction_service, tool, ) from google.cloud.aiplatform_v1beta1.types import ( content as content_v1beta1, ) from google.cloud.aiplatform_v1beta1.types import ( prediction_service as prediction_service_v1beta1, ) from google.cloud.aiplatform_v1beta1.types import ( tool as tool_v1beta1, ) _MODEL = "model" @dataclass(frozen=True) class GenerateContentParams: model: str contents: Sequence[content.Content] | Sequence[content_v1beta1.Content] | None = None system_instruction: content.Content | content_v1beta1.Content | None = None tools: Sequence[tool.Tool] | Sequence[tool_v1beta1.Tool] | None = None tool_config: tool.ToolConfig | tool_v1beta1.ToolConfig | None = None labels: Mapping[str, str] | None = None safety_settings: Sequence[content.SafetySetting] | Sequence[content_v1beta1.SafetySetting] | None = None generation_config: content.GenerationConfig | content_v1beta1.GenerationConfig | None = None def get_server_attributes( endpoint: str, ) -> dict[str, AttributeValue]: """Get server.* attributes from the endpoint, which is a hostname with optional port e.g. - ``us-central1-aiplatform.googleapis.com`` - ``us-central1-aiplatform.googleapis.com:5431`` """ parsed = urlparse(f"scheme://{endpoint}") if not parsed.hostname: return {} return { server_attributes.SERVER_ADDRESS: parsed.hostname, server_attributes.SERVER_PORT: parsed.port or 443, } def get_genai_request_attributes( # pylint: disable=too-many-branches use_latest_semconvs: bool, params: GenerateContentParams, operation_name: GenAIAttributes.GenAiOperationNameValues = GenAIAttributes.GenAiOperationNameValues.CHAT, ): model = _get_model_name(params.model) generation_config = params.generation_config attributes: dict[str, AttributeValue] = { GenAIAttributes.GEN_AI_OPERATION_NAME: operation_name.value, GenAIAttributes.GEN_AI_REQUEST_MODEL: model, } if not use_latest_semconvs: attributes[GenAIAttributes.GEN_AI_SYSTEM] = GenAIAttributes.GenAiSystemValues.VERTEX_AI.value if not generation_config: return attributes # Check for optional fields # https://proto-plus-python.readthedocs.io/en/stable/fields.html#optional-fields if "temperature" in generation_config: attributes[GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE] = generation_config.temperature if "top_p" in generation_config: # There is also a top_k parameter ( The maximum number of tokens to consider when sampling.), # but no semconv yet exists for it. attributes[GenAIAttributes.GEN_AI_REQUEST_TOP_P] = generation_config.top_p if "max_output_tokens" in generation_config: attributes[GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS] = generation_config.max_output_tokens if "presence_penalty" in generation_config: attributes[GenAIAttributes.GEN_AI_REQUEST_PRESENCE_PENALTY] = generation_config.presence_penalty if "frequency_penalty" in generation_config: attributes[GenAIAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY] = generation_config.frequency_penalty if "stop_sequences" in generation_config: attributes[GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES] = generation_config.stop_sequences if "seed" in generation_config: attributes[GenAIAttributes.GEN_AI_REQUEST_SEED] = generation_config.seed if "candidate_count" in generation_config: attributes[GenAIAttributes.GEN_AI_REQUEST_CHOICE_COUNT] = generation_config.candidate_count if "response_mime_type" in generation_config: if generation_config.response_mime_type == "text/plain": attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] = "text" elif generation_config.response_mime_type == "application/json": attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] = "json" else: attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] = generation_config.response_mime_type return attributes def get_genai_response_attributes( response: prediction_service.GenerateContentResponse | prediction_service_v1beta1.GenerateContentResponse | None, ) -> dict[str, AttributeValue]: if not response: return {} finish_reasons: list[str] = [_map_finish_reason(candidate.finish_reason) for candidate in response.candidates] return { GenAIAttributes.GEN_AI_RESPONSE_MODEL: response.model_version, GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS: finish_reasons, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS: response.usage_metadata.prompt_token_count, GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS: response.usage_metadata.candidates_token_count, } _MODEL_STRIP_RE = re.compile(r"^projects/(.*)/locations/(.*)/publishers/google/models/") def _get_model_name(model: str) -> str: return _MODEL_STRIP_RE.sub("", model) OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" @overload def is_content_enabled( mode: Literal[_StabilityMode.GEN_AI_LATEST_EXPERIMENTAL], ) -> ContentCapturingMode: ... @overload def is_content_enabled(mode: Literal[_StabilityMode.DEFAULT]) -> bool: ... def is_content_enabled( mode: Literal[_StabilityMode.DEFAULT, _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL], ) -> bool | ContentCapturingMode: if mode == _StabilityMode.DEFAULT: capture_content = environ.get(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "false") return capture_content.lower() == "true" return get_content_capturing_mode() def get_span_name(span_attributes: Mapping[str, AttributeValue]) -> str: name = span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME] model = span_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] if not model: return f"{name}" return f"{name} {model}" def request_to_events(*, params: GenerateContentParams, capture_content: bool) -> Iterable[LogRecord]: # System message if params.system_instruction: request_content = _parts_to_any_value( capture_content=capture_content, parts=params.system_instruction.parts, ) yield system_event(role=params.system_instruction.role, content=request_content) for content in params.contents or []: # Assistant message if content.role == _MODEL: request_content = _parts_to_any_value(capture_content=capture_content, parts=content.parts) yield assistant_event(role=content.role, content=request_content) continue # Tool event # # Function call results can be parts inside of a user Content or in a separate Content # entry without a role. That may cause duplication in a user event, see # https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3280 function_responses = [part.function_response for part in content.parts if "function_response" in part] for idx, function_response in enumerate(function_responses): yield tool_event( id_=f"{function_response.name}_{idx}", role=content.role, content=json_format.MessageToDict( function_response._pb.response # type: ignore[reportUnknownMemberType] ) if capture_content else None, ) if len(function_responses) == len(content.parts): # If the content only contained function responses, don't emit a user event continue request_content = _parts_to_any_value(capture_content=capture_content, parts=content.parts) yield user_event(role=content.role, content=request_content) def _modality_from_mime_type(mime_type: str) -> str: """Infer modality from MIME type prefix.""" if mime_type.startswith("image/"): return "image" if mime_type.startswith("video/"): return "video" if mime_type.startswith("audio/"): return "audio" return mime_type def convert_content_to_message_parts( content: content.Content | content_v1beta1.Content, ) -> list[MessagePart]: parts: MessagePart = [] for idx, part in enumerate(content.parts): if "function_response" in part: part = part.function_response parts.append( ToolCallResponse( id=f"{part.name}_{idx}", response=json_format.MessageToDict(part._pb.response), # type: ignore[reportUnknownMemberType] ) ) elif "function_call" in part: part = part.function_call parts.append( ToolCallRequest( id=f"{part.name}_{idx}", name=part.name, arguments=json_format.MessageToDict( part._pb.args, # type: ignore[reportUnknownMemberType] ), ) ) elif "text" in part: parts.append(Text(content=part.text)) elif "inline_data" in part: part = part.inline_data mime_type = part.mime_type or "" parts.append( Blob( mime_type=mime_type, modality=_modality_from_mime_type(mime_type), content=part.data or b"", ) ) elif "file_data" in part: part = part.file_data mime_type = part.mime_type or "" parts.append( Uri( mime_type=mime_type, modality=_modality_from_mime_type(mime_type), uri=part.file_uri or "", ) ) else: logging.warning("Unknown part dropped from telemetry %s", part) return parts def response_to_events( *, response: prediction_service.GenerateContentResponse | prediction_service_v1beta1.GenerateContentResponse, capture_content: bool, ) -> Iterable[LogRecord]: for candidate in response.candidates: tool_calls = _extract_tool_calls(candidate=candidate, capture_content=capture_content) # The original function_call Part is still duplicated in message, see # https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3280 yield choice_event( finish_reason=_map_finish_reason(candidate.finish_reason), index=candidate.index, # default to "model" since Vertex uses that instead of assistant message=ChoiceMessage( role=candidate.content.role or _MODEL, content=_parts_to_any_value( capture_content=capture_content, parts=candidate.content.parts, ), ), tool_calls=tool_calls, ) def _extract_tool_calls( *, candidate: content.Candidate | content_v1beta1.Candidate, capture_content: bool, ) -> Iterable[ChoiceToolCall]: for idx, part in enumerate(candidate.content.parts): if "function_call" not in part: continue yield ChoiceToolCall( # Make up an id with index since vertex expects the indices to line up instead of # using ids. id=f"{part.function_call.name}_{idx}", function=ChoiceToolCall.Function( name=part.function_call.name, arguments=json_format.MessageToDict( part.function_call._pb.args # type: ignore[reportUnknownMemberType] ) if capture_content else None, ), ) def _parts_to_any_value( *, capture_content: bool, parts: Sequence[content.Part] | Sequence[content_v1beta1.Part], ) -> list[dict[str, AnyValue]] | None: if not capture_content: return None return [ cast( "dict[str, AnyValue]", type(part).to_dict( # type: ignore[reportUnknownMemberType] part, always_print_fields_with_no_presence=False ), ) for part in parts ] def _map_finish_reason( finish_reason: content.Candidate.FinishReason | content_v1beta1.Candidate.FinishReason, ) -> FinishReason | str: EnumType = type(finish_reason) # pylint: disable=invalid-name if finish_reason is EnumType.FINISH_REASON_UNSPECIFIED or finish_reason is EnumType.OTHER: return "error" if finish_reason is EnumType.STOP: return "stop" if finish_reason is EnumType.MAX_TOKENS: return "length" # If there is no 1:1 mapping to an OTel preferred enum value, use the exact vertex reason return finish_reason.name version.py000066400000000000000000000001471527070072300466120ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "2.3b0.dev" tests/000077500000000000000000000000001527070072300351365ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai__init__.py000066400000000000000000000000001527070072300372350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/testscassettes/000077500000000000000000000000001527070072300371345ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/teststest_function_call_choice.yaml000066400000000000000000000106551527070072300452200ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/cassettesinteractions: - request: body: |- { "contents": [ { "role": "user", "parts": [ { "text": "Get weather details in New Delhi and San Francisco?" } ] } ], "tools": [ { "functionDeclarations": [ { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": 6, "properties": { "location": { "type": 1, "description": "The location for which to get the weather. It can be a city name, a city name and state, or a zip code. Examples: 'San Francisco', 'San Francisco, CA', '95616', etc." } }, "propertyOrdering": [ "location" ] } } ] } ] } headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '824' Content-Type: - application/json User-Agent: - python-requests/2.32.3 method: POST uri: https://us-central1-aiplatform.googleapis.com/v1/projects/fake-project/locations/us-central1/publishers/google/models/gemini-2.5-pro:generateContent?%24alt=json%3Benum-encoding%3Dint response: body: string: |- { "candidates": [ { "content": { "role": "model", "parts": [ { "functionCall": { "name": "get_current_weather", "args": { "location": "New Delhi" } }, "thoughtSignature": "CuUGAcu98PA0CgDJVjnZvciC3yQjuC6R/hJiEIzLX88DIJjiqSv2B+3ZhTR62dWOcR8xHdVOUjJB+LgQO/KTzyZ9FZBM5OdkXsXMC+KsrEsjnjDq0wSAtDe8zDhztTD2mmgFT7i0asHN60aoxdkgM3myRHyzmaNjs+F2Ua3wS9MiAqzYdKoPry5kzkL/KQBI3dpsL/JiiGKFgXQ7y+hL3epdjU4ySjVw/x5zKC/MyHHpQIhHwhzKYZR3I1TUR3b0ziBZJknQHFaxNckeDKfY3aXuawPqqqf+RrUd7MEQOE0nnJF/jl7zwhxevPglX9R//MTpzV8bZIBLKu84GYIM1h7x2iHN0t5arER3VDk0kgIG0FsOaMrv+BqRykSsQc/9K2gLdB+ouGSlLJLQ8amnyiuxQC25RHRPlGvJKJCq2wCl2pEAhd7HUoCbiN145pbdoYi+L4vigSIsob7NvGHW/RJiZd1lHXAVJUCmzmhyNMj3iYW3qvO5+H41q5EZjlUT8RNBwN9Lv4V71CRdQnw5oYfH1WncPoVdbtQwPqIzhqJ93JKLJGdSkkNrODdZlt96iOHDchD0PLLz/vGg90La+v+sAvwnKRX/SGXruySlTDKntua4UBFauFD7Cyv/VQvyNpqiP39jnTdjW3J42vKLEtpf/Ldagf1JUcZ/BXQUYamhzuEdRVBg3XB0kbXA/e59vSsqXHPDFurbWzv8q56Jxu/lE8H416iPlWuR25J8SqWahHO039YtOrjGW9+/C69bFxh9HwSJtmUkoX0P0cZmRuYkOl3mpl4sRz/GfebNu7+G4+q9O5SSnpmzxEGUNccRiiTiZK89YPj3vqnODydK5qYTyMaTkYGkInxqCoJbCvXuNnYArwnVuWGKMHHJfmlV9et24zipKdMfb/ZTfdSy9uni4mgMOqd6b4er08Qft1NKktxqmU87izBd5Vt6VSfYsJqW4lrRs4f+28TfFXl2znfKt16Pi+8AuyH+49zS/xkZS5J48rGg0xWvowPkT2vSz/nYGb5xlEDPDABDvfwwib+T32d7cj6dQgZHzQWnPjZgSlSfOYuU2QSZtO5nx8oS2jnF+sb2Ywst0ZMV+ECOsH5OSjQ31Nmd6/PIHGVtFI6veBK78Xon2iuoHNwwqYZj8+hAXRMTkyY=" }, { "functionCall": { "name": "get_current_weather", "args": { "location": "San Francisco" } } } ] }, "finishReason": 1, "avgLogprobs": -1.0146095752716064 } ], "usageMetadata": { "promptTokenCount": 74, "candidatesTokenCount": 16, "totalTokenCount": 290, "trafficType": 1, "promptTokensDetails": [ { "modality": 1, "tokenCount": 74 } ], "candidatesTokensDetails": [ { "modality": 1, "tokenCount": 16 } ], "thoughtsTokenCount": 200 }, "modelVersion": "gemini-2.5-pro", "createTime": "2025-08-19T14:55:25.379701Z", "responseId": "XZCkaLWWF8qm1dkP7rOm6AM" } headers: Content-Type: - application/json; charset=UTF-8 Transfer-Encoding: - chunked Vary: - Origin - X-Origin - Referer content-length: - '2273' status: code: 200 message: OK version: 1 test_function_call_choice_no_content.yaml000066400000000000000000000127011527070072300474400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/cassettesinteractions: - request: body: |- { "contents": [ { "role": "user", "parts": [ { "text": "Get weather details in New Delhi and San Francisco?" } ] } ], "tools": [ { "functionDeclarations": [ { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": 6, "properties": { "location": { "type": 1, "description": "The location for which to get the weather. It can be a city name, a city name and state, or a zip code. Examples: 'San Francisco', 'San Francisco, CA', '95616', etc." } }, "propertyOrdering": [ "location" ] } } ] } ] } headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '824' Content-Type: - application/json User-Agent: - python-requests/2.32.3 method: POST uri: https://us-central1-aiplatform.googleapis.com/v1/projects/fake-project/locations/us-central1/publishers/google/models/gemini-2.5-pro:generateContent?%24alt=json%3Benum-encoding%3Dint response: body: string: |- { "candidates": [ { "content": { "role": "model", "parts": [ { "functionCall": { "name": "get_current_weather", "args": { "location": "New Delhi" } }, "thoughtSignature": "CvUMAcu98PCpw5XB3NsEMaSIsgMKQrbACcz+SUZ/0Yix+Ha8XBPVSElLdPHCDnwIVhxjuEUB5BY7g1JzVH6akIVmr/14Y4YF7K1ed9ds/c9FdqkrEKVrC8sVYO16/jFhxrBDS7rD8XQHhjVAuf1W1+I8AH4mhuhTqaYEzjvq+Ea0NsiysqxqCyLneKW2nY5QNT0xi/rUEri19FScPfxcCdn7uDcMyvEQaXFiWdyA3jUIfgrh7E42BHv+xj0LGdWQ8FXJNBjFed6qcarpul7yAImSlyoXJxAgnJx53XgvacYUN9Eup9Gh5b8bU2c6q8qFMsggiq15kSUN3udbiu0cAilezHalh/OkctHmyXbmLF9qSwr6ZO9ie7Mm6pJxT3lea6VPXeFsUkgqt9TxRvOITDxMPSeoCLBFbPmCbvICrBcRHfms05SzOq9tyfJCPU43X0ge49dnTceHrRvF3IrK7pFzetpK30xFwbsKISARZHZuK49iUUjexHlH2ePpCrWgKkp0YqbGuamJWtj1F6Lj/nw45O+ke+55WhPAtam2NAX4MYkN/AMCVyemq5sNi8nC+Pc5T1GCsGmBDYyy8fftG2pZlwMfS4Mu2ta4kD92cj4Zt29Mvb0jIF1smWWiLSfiqRCAt41oK5CZRUz4cEqTulOfTY5NqBUHODsqWkxG6Zei90KFGb99itmHvy9j/HNPHLbqccGiIdJTWE52EFlTCXNoIgn9wPXMiLs+ScdgVcfcl4Tq9DoPrezddjEgLknz5dwOefayzNBSR0xVk3nQiPvWkCMJaqCQ2rmVou8yRugAltW2jsImPcoOdSdmMIJ4MA6O4GGNus8PAaov4VJJY33BSRZ+qsdMhndpcxCO4c7ygkRv+3QaxdgYTlxPyiPm6Gi3BbxHoU7pAlO/rEgUUqA3CvuCHObeUCC7Re/lPPL7HJS0sn6iXbQ+xZuQ0yucd664MQYTNOxrw0IL6oZUM4u8APJJbB9MixypSWC9av68O6+nANO7e6hvQTsvBcDezLt54cFoykqGDSzxMXvjCY4Vf0HQYzWMQU/hsv8Xe8f2iHdS2xgpUKfBqoQmduPPU13Z1EdKfGA1fQv6psM4fO4fqIi4SqcXuRZqDA2kzVyqkRR811m9oe/Prje9IytviLoMuRTO37WUxeGyPvE74c64SAb2F9/AGO+BhzFAUEHcFqU1i1YilrbURamlt3ckajCfCNCb/93jw64QaeE8lmVhL7wzgjjSme0lXWqjnY8QTH2bUb5nuapyGZcyNrwFnvK5NNLHgS9opAXN+3pKT/LPAAwTRgGH1w0JiwVPa0lfNHnSPCtTBNU35B8SSEWyln416wKOhh+uaJuB8NxuGaDyTwAgvvvH/7rWMAWmyJj3eS3AXUdjdqfOi3cNb/4H5aWXi4ydt/uLv2X+carFVrmq0dFxTqbSkBt+N2DTVWtI+aQG3OQ6gI7vOj3E82CuB/A6/atONMJ8SwNAgDG4iT4Z2sgDV5csuvNx7ssp/PS06lcziZEZyO8YRem8C/Worda9AT2DvWqRdDmuB1vEuhH7RNBsq8s4NZolgNLD3kI6OdvaFK6bvfuFzdsG4P2Ngw7v6AfQYYVPcz9WgovoLSPOy7UA0baxboSbTwxUqnWnX/AgxDbPIzNXKY2UUAA/nnK3RXHUcW/uKdz4C5i3ECttqBGK8gfSPS61fHCSqrlGYmbjlFHmg2trMbyu9pM4DEWBq57AR9BwL6/Smhv7uRWHnOxg/tHNEH7JOOHy4zW/d3Ni4EyLzE/loxQ3wZPzw2gas9ROMq8Z1ht22g/MUd+sL34fx/0hEIvZ9Tu21wyznCyr6R/mfXK9n75kqpehQ87p87W95HK0cvT8sqnhPrCFbghGWwtd9F1pRUOHrYESqQ63PK3Sgt91PcGu8lYTr/CS7fkAXmdRLXoioXVKP0EjwRFbO7pryHY1nOwZEU4RprNYyJYyVOavxpc8pC+blUIHrANKwTyYVnX7/Vfa3VFAfC/1PcObSeUpbuc+7MzYzltjzZnly8yILKDmulagTxoRQOiKvqCM3fq0OMYiivAs2+Zii4ZR8B+bM87NKkHvF69VJDF+iwwWT9MjXh8Ju0rvtrxds64Xl8/E9Uv+gnRp3VOIcttqsNuCssSnV5Yj7LRS1glUXbIwBJeXSjevjydOuQOYdn8S6s5Sd95g91tc8vpqGGP1" }, { "functionCall": { "name": "get_current_weather", "args": { "location": "San Francisco" } } } ] }, "finishReason": 1, "avgLogprobs": -1.5602271556854248 } ], "usageMetadata": { "promptTokenCount": 74, "candidatesTokenCount": 16, "totalTokenCount": 482, "trafficType": 1, "promptTokensDetails": [ { "modality": 1, "tokenCount": 74 } ], "candidatesTokensDetails": [ { "modality": 1, "tokenCount": 16 } ], "thoughtsTokenCount": 392 }, "modelVersion": "gemini-2.5-pro", "createTime": "2025-08-19T14:55:28.661450Z", "responseId": "YJCkaMqvKPfQgLUPuoKz8QU" } headers: Content-Type: - application/json; charset=UTF-8 Transfer-Encoding: - chunked Vary: - Origin - X-Origin - Referer content-length: - '3317' status: code: 200 message: OK version: 1 test_generate_content.yaml000066400000000000000000000040201527070072300443770ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/cassettesinteractions: - request: body: |- { "contents": [ { "role": "user", "parts": [ { "text": "Say this is a test" } ] } ] } headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '141' Content-Type: - application/json User-Agent: - python-requests/2.32.3 method: POST uri: https://us-central1-aiplatform.googleapis.com/v1/projects/fake-project/locations/us-central1/publishers/google/models/gemini-2.5-pro:generateContent?%24alt=json%3Benum-encoding%3Dint response: body: string: |- { "candidates": [ { "content": { "role": "model", "parts": [ { "text": "This is a test." } ] }, "finishReason": 1, "avgLogprobs": -12.052484893798828 } ], "usageMetadata": { "promptTokenCount": 5, "candidatesTokenCount": 5, "totalTokenCount": 304, "trafficType": 1, "promptTokensDetails": [ { "modality": 1, "tokenCount": 5 } ], "candidatesTokensDetails": [ { "modality": 1, "tokenCount": 5 } ], "thoughtsTokenCount": 294 }, "modelVersion": "gemini-2.5-pro", "createTime": "2025-08-19T15:20:47.464265Z", "responseId": "T5akaImrHM2Dm9IPlu_Z0QI" } headers: Content-Type: - application/json; charset=UTF-8 Transfer-Encoding: - chunked Vary: - Origin - X-Origin - Referer content-length: - '740' status: code: 200 message: OK version: 1 test_generate_content_all_events.yaml000066400000000000000000000051071527070072300466220ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/cassettesinteractions: - request: body: |- { "contents": [ { "role": "user", "parts": [ { "text": "My name is OpenTelemetry" } ] }, { "role": "model", "parts": [ { "text": "Hello OpenTelemetry!" } ] }, { "role": "user", "parts": [ { "text": "Address me by name and say this is a test" } ] } ], "systemInstruction": { "role": "user", "parts": [ { "text": "You are a clever language model" } ] } } headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '548' Content-Type: - application/json User-Agent: - python-requests/2.32.3 method: POST uri: https://us-central1-aiplatform.googleapis.com/v1/projects/fake-project/locations/us-central1/publishers/google/models/gemini-2.5-pro:generateContent?%24alt=json%3Benum-encoding%3Dint response: body: string: |- { "candidates": [ { "content": { "role": "model", "parts": [ { "text": "OpenTelemetry, this is a test." } ] }, "finishReason": 1, "avgLogprobs": -8.984109878540039 } ], "usageMetadata": { "promptTokenCount": 25, "candidatesTokenCount": 8, "totalTokenCount": 439, "trafficType": 1, "promptTokensDetails": [ { "modality": 1, "tokenCount": 25 } ], "candidatesTokensDetails": [ { "modality": 1, "tokenCount": 8 } ], "thoughtsTokenCount": 406 }, "modelVersion": "gemini-2.5-pro", "createTime": "2025-08-19T14:55:12.290295Z", "responseId": "UJCkaPfbEdDxgLUPhardyQg" } headers: Content-Type: - application/json; charset=UTF-8 Transfer-Encoding: - chunked Vary: - Origin - X-Origin - Referer content-length: - '757' status: code: 200 message: OK version: 1 test_generate_content_extra_params.yaml000066400000000000000000000037131527070072300471550ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/cassettesinteractions: - request: body: |- { "contents": [ { "role": "user", "parts": [ { "text": "Say this is a test" } ] } ], "generationConfig": { "temperature": 0.2, "topP": 0.95, "topK": 2.0, "maxOutputTokens": 5, "stopSequences": [ "\n\n\n" ], "presencePenalty": -1.5, "frequencyPenalty": 1.0, "seed": 12345 } } headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '376' Content-Type: - application/json User-Agent: - python-requests/2.32.3 method: POST uri: https://us-central1-aiplatform.googleapis.com/v1/projects/fake-project/locations/us-central1/publishers/google/models/gemini-2.5-pro:generateContent?%24alt=json%3Benum-encoding%3Dint response: body: string: |- { "candidates": [ { "content": { "role": "model" }, "finishReason": 2 } ], "usageMetadata": { "promptTokenCount": 5, "totalTokenCount": 9, "trafficType": 1, "promptTokensDetails": [ { "modality": 1, "tokenCount": 5 } ], "thoughtsTokenCount": 4 }, "modelVersion": "gemini-2.5-pro", "createTime": "2025-08-19T15:18:43.287437Z", "responseId": "05WkaM3FEaGSmecPxYzosQM" } headers: Content-Type: - application/json; charset=UTF-8 Transfer-Encoding: - chunked Vary: - Origin - X-Origin - Referer content-length: - '468' status: code: 200 message: OK version: 1 test_generate_content_invalid_role.yaml000066400000000000000000000023371527070072300471370ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/cassettesinteractions: - request: body: |- { "contents": [ { "role": "invalid_role", "parts": [ { "text": "Say this is a test" } ] } ] } headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '149' Content-Type: - application/json User-Agent: - python-requests/2.32.3 method: POST uri: https://us-central1-aiplatform.googleapis.com/v1/projects/fake-project/locations/us-central1/publishers/google/models/gemini-2.5-pro:generateContent?%24alt=json%3Benum-encoding%3Dint response: body: string: |- { "error": { "code": 400, "message": "Please use a valid role: user, model.", "status": "INVALID_ARGUMENT", "details": [] } } headers: Content-Type: - application/json; charset=UTF-8 Transfer-Encoding: - chunked Vary: - Origin - X-Origin - Referer content-length: - '817' status: code: 400 message: Bad Request version: 1 test_generate_content_invalid_temperature.yaml000066400000000000000000000026441527070072300505340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/cassettesinteractions: - request: body: |- { "contents": [ { "role": "user", "parts": [ { "text": "Say this is a test" } ] } ], "generationConfig": { "temperature": 1000.0 } } headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '196' Content-Type: - application/json User-Agent: - python-requests/2.32.3 method: POST uri: https://us-central1-aiplatform.googleapis.com/v1/projects/fake-project/locations/us-central1/publishers/google/models/gemini-2.5-pro:generateContent?%24alt=json%3Benum-encoding%3Dint response: body: string: |- { "error": { "code": 400, "message": "Unable to submit request because it has a temperature value of 1000 but the supported range is from 0 (inclusive) to 2.0001 (exclusive). Update the value and try again.", "status": "INVALID_ARGUMENT", "details": [] } } headers: Content-Type: - application/json; charset=UTF-8 Transfer-Encoding: - chunked Vary: - Origin - X-Origin - Referer content-length: - '1472' status: code: 400 message: Bad Request version: 1 test_generate_content_missing_model.yaml000066400000000000000000000024501527070072300473150ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/cassettesinteractions: - request: body: |- { "contents": [ { "role": "user", "parts": [ { "text": "Say this is a test" } ] } ] } headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '141' Content-Type: - application/json User-Agent: - python-requests/2.32.3 method: POST uri: https://us-central1-aiplatform.googleapis.com/v1/projects/fake-project/locations/us-central1/publishers/google/models/gemini-does-not-exist:generateContent?%24alt=json%3Benum-encoding%3Dint response: body: string: |- { "error": { "code": 404, "message": "Publisher Model `projects/fake-project/locations/us-central1/publishers/google/models/gemini-does-not-exist` not found.", "status": "NOT_FOUND", "details": [] } } headers: Content-Type: - application/json; charset=UTF-8 Transfer-Encoding: - chunked Vary: - Origin - X-Origin - Referer content-length: - '1139' status: code: 404 message: Not Found version: 1 test_generate_content_with_files.yaml000066400000000000000000000051621527070072300466240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/cassettesinteractions: - request: body: |- { "contents": [ { "role": "user", "parts": [ { "text": "Say this is a test" }, { "fileData": { "mimeType": "image/jpeg", "fileUri": "https://images.pdimagearchive.org/collections/microscopic-delights/1lede-0021.jpg" } }, { "inlineData": { "mimeType": "image/jpeg", "data": "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" } } ] } ] } headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '554' Content-Type: - application/json User-Agent: - python-requests/2.32.3 method: POST uri: https://us-central1-aiplatform.googleapis.com/v1/projects/fake-project/locations/us-central1/publishers/google/models/gemini-2.5-pro:generateContent?%24alt=json%3Benum-encoding%3Dint response: body: string: |- { "candidates": [ { "content": { "role": "model", "parts": [ { "text": "This is a test." } ] }, "finishReason": 1, "avgLogprobs": -24.462081909179688 } ], "usageMetadata": { "promptTokenCount": 521, "candidatesTokenCount": 5, "totalTokenCount": 950, "trafficType": 1, "promptTokensDetails": [ { "modality": 2, "tokenCount": 516 }, { "modality": 1, "tokenCount": 5 } ], "candidatesTokensDetails": [ { "modality": 1, "tokenCount": 5 } ], "thoughtsTokenCount": 424 }, "modelVersion": "gemini-2.5-pro", "createTime": "2025-10-13T16:29:47.639271Z", "responseId": "-yjtaKeCJ5KYmecP76S4-AI" } headers: Content-Type: - application/json; charset=UTF-8 Transfer-Encoding: - chunked Vary: - Origin - X-Origin - Referer content-length: - '808' status: code: 200 message: OK version: 1 test_generate_content_without_events.yaml000066400000000000000000000040121527070072300475470ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/cassettesinteractions: - request: body: |- { "contents": [ { "role": "user", "parts": [ { "text": "Say this is a test" } ] } ] } headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '141' Content-Type: - application/json User-Agent: - python-requests/2.32.3 method: POST uri: https://us-central1-aiplatform.googleapis.com/v1/projects/fake-project/locations/us-central1/publishers/google/models/gemini-2.5-pro:generateContent?%24alt=json%3Benum-encoding%3Dint response: body: string: |- { "candidates": [ { "content": { "role": "model", "parts": [ { "text": "This is a test." } ] }, "finishReason": 1, "avgLogprobs": -14.465771484375 } ], "usageMetadata": { "promptTokenCount": 5, "candidatesTokenCount": 5, "totalTokenCount": 354, "trafficType": 1, "promptTokensDetails": [ { "modality": 1, "tokenCount": 5 } ], "candidatesTokensDetails": [ { "modality": 1, "tokenCount": 5 } ], "thoughtsTokenCount": 344 }, "modelVersion": "gemini-2.5-pro", "createTime": "2025-08-19T15:18:37.008618Z", "responseId": "zZWkaKpDipOZ5w__8qRQ" } headers: Content-Type: - application/json; charset=UTF-8 Transfer-Encoding: - chunked Vary: - Origin - X-Origin - Referer content-length: - '734' status: code: 200 message: OK version: 1 test_preview_generate_content_all_input_events.yaml000066400000000000000000000051141527070072300516000ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/cassettesinteractions: - request: body: |- { "contents": [ { "role": "user", "parts": [ { "text": "My name is OpenTelemetry" } ] }, { "role": "model", "parts": [ { "text": "Hello OpenTelemetry!" } ] }, { "role": "user", "parts": [ { "text": "Address me by name and say this is a test" } ] } ], "systemInstruction": { "role": "user", "parts": [ { "text": "You are a clever language model" } ] } } headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '548' Content-Type: - application/json User-Agent: - python-requests/2.32.3 method: POST uri: https://us-central1-aiplatform.googleapis.com/v1beta1/projects/fake-project/locations/us-central1/publishers/google/models/gemini-2.5-pro:generateContent?%24alt=json%3Benum-encoding%3Dint response: body: string: |- { "candidates": [ { "content": { "role": "model", "parts": [ { "text": "OpenTelemetry, this is a test." } ] }, "finishReason": 1, "avgLogprobs": -7.633238792419434 } ], "usageMetadata": { "promptTokenCount": 25, "candidatesTokenCount": 8, "totalTokenCount": 400, "trafficType": 1, "promptTokensDetails": [ { "modality": 1, "tokenCount": 25 } ], "candidatesTokensDetails": [ { "modality": 1, "tokenCount": 8 } ], "thoughtsTokenCount": 367 }, "modelVersion": "gemini-2.5-pro", "createTime": "2025-08-19T14:55:21.164378Z", "responseId": "WZCkaJqECuavgLUP4fGpsQs" } headers: Content-Type: - application/json; charset=UTF-8 Transfer-Encoding: - chunked Vary: - Origin - X-Origin - Referer content-length: - '757' status: code: 200 message: OK version: 1 test_tool_events.yaml000066400000000000000000000166241527070072300434310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/cassettesinteractions: - request: body: |- { "contents": [ { "role": "user", "parts": [ { "text": "Get weather details in New Delhi and San Francisco?" } ] }, { "role": "model", "parts": [ { "functionCall": { "name": "get_current_weather", "args": { "location": "New Delhi" } } }, { "functionCall": { "name": "get_current_weather", "args": { "location": "San Francisco" } } } ] }, { "role": "user", "parts": [ { "functionResponse": { "name": "get_current_weather", "response": { "content": "{\"temperature\": 35, \"unit\": \"C\"}" } } }, { "functionResponse": { "name": "get_current_weather", "response": { "content": "{\"temperature\": 25, \"unit\": \"C\"}" } } } ] } ], "tools": [ { "functionDeclarations": [ { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": 6, "properties": { "location": { "type": 1, "description": "The location for which to get the weather. It can be a city name, a city name and state, or a zip code. Examples: 'San Francisco', 'San Francisco, CA', '95616', etc." } }, "propertyOrdering": [ "location" ] } } ] } ] } headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '1731' Content-Type: - application/json User-Agent: - python-requests/2.32.3 method: POST uri: https://us-central1-aiplatform.googleapis.com/v1/projects/fake-project/locations/us-central1/publishers/google/models/gemini-2.5-pro:generateContent?%24alt=json%3Benum-encoding%3Dint response: body: string: |- { "candidates": [ { "content": { "role": "model", "parts": [ { "text": "The current temperature in New Delhi is 35\u00b0C, and in San Francisco, it is 25\u00b0C.", "thoughtSignature": "CvkTAcu98PDKNriXzBqqOq6Z8+JY3Kh2JIFAPWejBW6qG9oykQy8c/2X1Z9S/rjfGiZWs0O2Wv6audk8a2RAKSNv8+XEOWcqIATzdUdBSNe1In6lX9X7ToamJmbmMc5gA2adQuqq9/m6YPwPro2x9RabqQrLoawGKPPAbJKj5wxX5eFbcM+QRwP+YB/1e/6PmSZ313MDu6GEWH6KvagJwDT0Fm7GLcJIuWRrAVWqJ5ssClSVP7ZNjynsi8cHnJ8hTqZxXFqSpOMQHwl1rZH3Z263Vcjq02f9RPjhWOOkkOCqexG0IHGlvkJWdG1Z62rXNtgFPgvNjE4CLaHNFydD3Inuh03X0X3+/RflG6o2VoAjg9TNw8Fc2/UZoDXX54GQOegXdAk/U2oG+8CEi7Xc3eJj3hkPxzeRGsczlxvI/grNx8b9qHgPJ19mLUV0eO3Jj5rQVM0kMmAJEfFgiE3vsPvTyRx25K8u/jjohSOyIoHqH3YWmCxmYXvC4i0ouMbLoX9gc8caQtFIla9KfApcVEWushi84Or9ZNmKKUhtf9Xg3eRx2940oRNXdAG3susiDujjNAoEZXmz8Z+/ELOWLgFhGHYCjzzbm5ybQ5TYtoiYq66N63Lqq2uCwi+JN3eY87gEmVj80HJVIn+A0iwN0yg3vRptO52ih/kI3Vy3LHqHrE+Vx2ZuAzlwL1S83h2sZu18DCpfRFUkpc+myhUj3DvsZ1Ob7g5urwKCOCCJtSmV6RHYx9UfF01/ZWrpuMOQt2bJWi3M28EHPEC3F21f0HYMSyFnRsN5f4c6bcG54bDaUj0OeHwUxC23vd6HDVyyJOG2Z9qVUn8xOpAv1A2uGS4vYiF5sTT0Fj7+LN0HsIvn7401u3DLoKB3aREuX2iXCFlhH7pXZgqxMDV9ThXJDRoEoFSYpi4OYCS95m9aT38eCzdbqa40Pu2AGrT4XMaFcYwm9OXyaSaEnbbnlSwzJFDNy70Byu/MF4IWmgCzLP1DHEP1CkH5BGDmMd54DizjQm53ngPihK3IFfYmG9aPB3n9RHxCaxYvvSREwHEFG5Ag6zYI2P7k4DK9thHSaf2/s+ziKXeGtvLDHkVLuBKAJvL71D2QocV5V8brFjQDI/wzmhbyJ/SOSfReHVnVq/GYKL1rfliHEwKp+F0i63H1yfzUNef7m+KESgMommnu9D3rg5LalRwrhbKsjBkNdX8CE35qQnX77Be+f6rWsPktA9wDD/QALuJHeqA3QxHmBnOcTjtiCN/aF3rg8VrI8OL+ce7a0Fq+GPSgj3OwlvVmZT4glkMFVvpPwb9Bcj3SSMvtY8enLNM/nqRURHfvwmjAFm4KmStVHwX5CvqtjO0WbgpODjvFiXazBnTsmt8eUmCQvJuhCv14H/HCkdbECEqOEFGLB9hZCAXvVqczctTVqcvad8coPPcQ/Scj0D0QVbK7YMDETiLw9FrEVSGjRpKLnPTbJKyRZZqwy4HRD/GucKt7VS8t6/LUYAy4jKGfWoWUpwTcXSL6EMD4FLV8iPsKHY/aYhOZhQ8ce2MLX7UXfeHKue61yDRUviaexR4ctuM5+WnUEEnBKtVMVjLS6XwgDyZldntlYKkajsvvtFT/6W8UC80lcpMwajs5xH5Eh++ogkICuqcdlmbL3KlkxJeZE4T8kSb32Lna2G5Pn4MK8kQJ1j5p7/3BOvjAGZV1/vywGb0ST4cjCWJDJiVeCUBYY6IZYFrEE4eVKH6s4bwxNU93tluunSlHLDPLjxp4l6M01mDriKqtHOJ7fStaouJwh0vJfsuJ0DKmfGMpbI5Cw1sx6930jvMo0LwtUU6KaH36jcsKOymThM5xj0sWZXmhoQz8zv+WLLM4HhffccnS0BnCeInchRqimX1sMjO4THAzSPmXT5+vonqlE+mCdAy8S706STIS40PL/ZWmNuONUXNCnvODFcKl0YKe3lTSN2uE+hkCX2FcBs48AOmeB62kBpHGQr8UIiKheLcq1+5Dj4AO0TUniOAx4+6h5YmCTZJ2LctJrFWrz1/vC05JLN1D9QKM+KwGNExURHWBpyiBp+VI5NaQrTHaXMqzY9LDrQ2LyTNb/s/lQtTsk2AeDokJE39hRnsHP6o/lNUOEIS2xZVIiVwqRw4PpybolBy0y9zqj+mAYEharImI1cpN7JBQz/NGTPe/JrppxTxRinJPTyMEpj8RaYFN4TRuW+9vp2fl+ezIsj2oelhmWKF86xTrQdfL/W1zXJ/OuC+MpNKoo8CVJ11CGF5Q9e6vrQF6nRiqcfWEEC2IxJoCUATrDyWPBPgTcJBi7xXjJYo7zfKwDEaQWRAKLmVoNH0eyTwPs3Kr6d0aUSwp5pmlncyuqNIbQffbXtMBeI7Sts8mxaae4YFiNhga6dzfqFmDI1C4uDDsyY1DdMbpDT7uBGl3YedYiex/2/J2HohFoOXoubQuX//oN8cjOLSHCF/ZxhnBuwcUBLNcE0uQduCV6AlxP/yX8R0+q5A2Ndst2SOygIptrqScD9uxG4cG9gYJGyRiUdtrZku3+1+w8R/7Vk6GjWyIwOsBLWOC9GU/wdDlu3tbX9PiPHQUEMgf/y7FuY9z3qD2fY51a0qQyQc+pF6DyvUzcwcPz83ArwpkiXd2Ll3CXxtPNCVteS/bmdAOWIqBkvISsLYhxSzN11bef2IY0I8JrJZCNgNr5/nBEllZ5+U2ynLC4WkLhXSm3SO+ToZTslJX9lZRhgAPBxwUY5qy4+UmDCjNl+G7BAJLmA8f61omiDiEK1ZBIfXTdDpLXFUs7V2Dn4iDvJKYBwpPbARkTsJROgxYGWEkjIM0ITecj4XnkcwODbCja7WEUTSgyr6a1vjBzmn+azFUrkUKX8vMnMGTkaF6Ypi68LB2OUjxP6SlFor/AWl8is1AmFWjznWny//nSe9s1dnNoX75UDmPlZLFR3r+2t7N7/Q7DlRBE2LMLXaqZi+SbE++dphRYdamTqTbh2vI0BaeZiZYb6/1ElbpAkip160bfuyOxTrDqIeMo/YtWMBEiPESQrnZmFr4cWbjkDAJzZ75qhRE3nMTDMzdITHCLxdOac2GU+7Waf97XGnT/T3S64Uda1tjIO30qtXmQGA3UqanIrUrE2Myqhlh1b3SsWKFqxfdAqzbvgHOHVIPgGrX5PeDzthNX1kUa9TFNye0roIzxC6wx2zW79rJmy8JBtmatLjtbKkgvWdfMcPNTgop6LPEzD5s67Sv+oGW3I3Q9HS7WDdU7eWqH2ZvvyNJumsSOIwtfCpm114l+jylkhf6FBagmuT8d3yiZSaTogAySsZpr/NRBCALQBMKGENboc3EjUDJIhwZfBjR33RjxecAY2gkWGi8/RbfA9Phbqc2LkiJeWFHWL+B/lrDL3hbdMfJCvPflDZU" } ] }, "finishReason": 1, "avgLogprobs": -1.1418917729304388 } ], "usageMetadata": { "promptTokenCount": 128, "candidatesTokenCount": 26, "totalTokenCount": 846, "trafficType": 1, "promptTokensDetails": [ { "modality": 1, "tokenCount": 128 } ], "candidatesTokensDetails": [ { "modality": 1, "tokenCount": 26 } ], "thoughtsTokenCount": 692 }, "modelVersion": "gemini-2.5-pro", "createTime": "2025-08-19T14:55:33.172473Z", "responseId": "ZZCkaLnDCuavgLUP4fGpsQs" } headers: Content-Type: - application/json; charset=UTF-8 Transfer-Encoding: - chunked Vary: - Origin - X-Origin - Referer content-length: - '4256' status: code: 200 message: OK version: 1 test_tool_events_no_content.yaml000066400000000000000000000147011527070072300456510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/cassettesinteractions: - request: body: |- { "contents": [ { "role": "user", "parts": [ { "text": "Get weather details in New Delhi and San Francisco?" } ] }, { "role": "model", "parts": [ { "functionCall": { "name": "get_current_weather", "args": { "location": "New Delhi" } } }, { "functionCall": { "name": "get_current_weather", "args": { "location": "San Francisco" } } } ] }, { "role": "user", "parts": [ { "functionResponse": { "name": "get_current_weather", "response": { "content": "{\"temperature\": 35, \"unit\": \"C\"}" } } }, { "functionResponse": { "name": "get_current_weather", "response": { "content": "{\"temperature\": 25, \"unit\": \"C\"}" } } } ] } ], "tools": [ { "functionDeclarations": [ { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": 6, "properties": { "location": { "type": 1, "description": "The location for which to get the weather. It can be a city name, a city name and state, or a zip code. Examples: 'San Francisco', 'San Francisco, CA', '95616', etc." } }, "propertyOrdering": [ "location" ] } } ] } ] } headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '1731' Content-Type: - application/json User-Agent: - python-requests/2.32.3 method: POST uri: https://us-central1-aiplatform.googleapis.com/v1/projects/fake-project/locations/us-central1/publishers/google/models/gemini-2.5-pro:generateContent?%24alt=json%3Benum-encoding%3Dint response: body: string: |- { "candidates": [ { "content": { "role": "model", "parts": [ { "text": "The weather in New Delhi is 35\u00b0C and in San Francisco is 25\u00b0C.", "thoughtSignature": "CqcOAcu98PDUiUq32HLxu6y5JNxvlcEjIaedPcBi5V86Hbf3vRgAXC4k0aMma0v1gotZVHrinF9edI9bEAdQdFR+2xOsaV1ntNeO4o35ymNNpm1rEv2p047eWxSABiXJ3VANecxNqQuVgxZyhClOn2BNmR/xGb43REabpcMzboVGVT6iKSJ/g3sCIt4bddY1IQ5zTdSV7lvCyZLuu9736VCJjULzAslhxSb9/xBlQu/pvcak6CmFFLZuDamqeJ3RvhalpklF6qz/Eq9nhhpdRPTERmGU7mNe4fiSpql6JxelO56ksKKGzSG+USa9vxcCQRRgAoaLlMHRegwPjfoA4hHgbpLBIUk9BNXrzmW5APOpJO3rqgIvKQ/WsZzIBI3l+slI51WHgVLr4qNvDUe8VKLhbApVj3L+rb5dQ/u6U9V2oSVA5pQFt6Lqubyjg8yCl0q+0dsvIYUqJSaDRONMfS5hwVu+RTU/SOEXLwTNEiDDuj3wO8xQuyitDAHqya797sOH2ThlSoT/c7V3Du00TBCk0eq8XaxyTobPMCaKNbrqjRTfMn5fortZlPwyAJqoTZsaSeaRufCFpgBMy34/QcLiCdVwKN6rLgBpDhtzThnyBzavDP2ltu8sxWEisJXEOel0q8LvBqTqlFY9dWrRScnB/TvitGnwqlV0PVpWU60wH/2Agexw0+aFlMRYxiFRjuHPK9R2UfLbEzVpDC8M+f16QRZOnWQc8L+cxEW/xHT6hCUxcTmSrpldAs2Ss8A21NcPTlr0lNviZxB+4xDq1dxB/VMS/qOnGVbr11nEOOVj+bWJyP/LLTjdUigGs+ISAzmEr2zkUirLdlmz/Mz1YXk9CMIus8WlPVzhQZuXY8dhSKxks0w8bJ5TjJelAwPGFgz6iWSz44IeaH22Fxdvub28/mdmvy3OerI3vfwlePUC9L8n2cg7gZjIE59PXi2x1EUTJfvcBrVTO5qqPuRpJZ9/d3ryEtS9tHsMnzfcEiS2lxNb+EqKaHk74hqvt6vEA+eQaYtMqG5TO79dpGDt0UD1cJEwngfMiL85+5qWHW4g2hSSJYgg55GtdZbxdGs960p8G02FbqUOSVcDGcIBDCkvFSGHJqMATvYQzChiwP2AnDVLN0aWd43UEl3rHESdOGLOqrxetcWmVsDacowhiy6jleS/Xml7d8mqqs2483XCOVMMYPcFh9budRed7PIf7eAYhWzGwt123JLbxqwBOBf1MHUJl2OQBy62HCJLYMlRhr1s2KVRK9Een5cgUa3H07r/s46YS2A0KOP/B0Ub0MflvGBAgo8LABj5cY2Ao1hTKcnBjgJPTNS0awETbtzU3Y74SPMsJ1Ipu2o76TFS3YFq1Fbit1VsceuYxzcqU04NFBtXAU7YojLv9LBOZXk6SukOWI/2cNUOHlzlWsSi34heVFGH4AcDbhc0/QZHhjSTE9FkBY9feTtSnTR16Q1bg3K9+7rxArD51Wj8q1ZNhVtZrzpuWGqoneI0WqJ25U5f1jSzQFW2HyzmIs/B5KiFEk4Do0EiaVcCQ5/1UGReYbgtSKa8PAvUGh2Lqiev1VMl6i4cDs7s3U9swvZdBdK+SauYC7UqEVov1G8UR8Bth2YncqUPSv7W2X7ppA6PwDXSCqCSgp3wnfEW8tQIa6AQW2s4Udp0lbYWr3Vu2DSK3iu8mVa3ybvHs5Go4SR9zJ8xElMn4adprz8jokYpoXK94DVeaUetKqTw6X3FzRb+K+e/W0MHZVEQtAjk2wwcNqAfs8JdXi2Mh/l+0PFp6Hhz0rI1rzZ3B08lVR+QU+0XpN5rerUmq44oE6t+dP6Cv8yl5kVBCjvtHZ9DLUGeSEofOR3JXs34rkWeOLfQ+vRZkYoiIRKkqFhf/RnRoU18vmfsbvK9C3XcO3wmfo58Iw2uYd9L/p6VdB27pPUiBJbTAzXbEA3CWUbedMg2vDZiucS4iBkWlmN91CofwgmNfiImdcauz5ug3TC5qRWUIqX/nNMSscPjpbhp3nmja727fyH1p4ytpXQbRX0do28KpY5XQ9yF9gAWxG2zHxtBGiZVG4iXRlpZlfmFUc4kQLUTrInYUoagh9zF6Cxwrz7smRqgOkWKOYiOTiR9ofTkvpmQpdGpDOsxjcPRd3V+jjkfZwT5yzsM7IOAzzhZKKvPWgfJfzaAI43W+dUWiMWzM5KX7Wliu5J3uEXtldo0lW2WpOiuFK5GIPBh5482IbSAtA14YEplOYVCrCI9UG356TImFYyCLuNKAxbzA3U9TSJCk0tUbssWKq8aIaoEbtvE43P1G8x5teVFR5Wu1nZypgKgYFSS5tRL+niDV82yaCOTIGjdA6yFHq8gRlv9xkmZjyPIB4LfAhAIEz9X0+hNvZBfTofi1UXRh/54GIqr1YIXmJVYKb/srAU5Qne5lQdvFauFIQbnlESVUeb1RA==" } ] }, "finishReason": 1, "avgLogprobs": -1.18875130740079 } ], "usageMetadata": { "promptTokenCount": 128, "candidatesTokenCount": 22, "totalTokenCount": 575, "trafficType": 1, "promptTokensDetails": [ { "modality": 1, "tokenCount": 128 } ], "candidatesTokensDetails": [ { "modality": 1, "tokenCount": 22 } ], "thoughtsTokenCount": 425 }, "modelVersion": "gemini-2.5-pro", "createTime": "2025-08-19T14:55:39.882212Z", "responseId": "a5CkaKTsNa3hgLUP59n1oQo" } headers: Content-Type: - application/json; charset=UTF-8 Transfer-Encoding: - chunked Vary: - Origin - X-Origin - Referer content-length: - '3277' status: code: 200 message: OK version: 1 test_tool_events_with_completion_hook.yaml000066400000000000000000000147011527070072300477270ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/cassettesinteractions: - request: body: |- { "contents": [ { "role": "user", "parts": [ { "text": "Get weather details in New Delhi and San Francisco?" } ] }, { "role": "model", "parts": [ { "functionCall": { "name": "get_current_weather", "args": { "location": "New Delhi" } } }, { "functionCall": { "name": "get_current_weather", "args": { "location": "San Francisco" } } } ] }, { "role": "user", "parts": [ { "functionResponse": { "name": "get_current_weather", "response": { "content": "{\"temperature\": 35, \"unit\": \"C\"}" } } }, { "functionResponse": { "name": "get_current_weather", "response": { "content": "{\"temperature\": 25, \"unit\": \"C\"}" } } } ] } ], "tools": [ { "functionDeclarations": [ { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": 6, "properties": { "location": { "type": 1, "description": "The location for which to get the weather. It can be a city name, a city name and state, or a zip code. Examples: 'San Francisco', 'San Francisco, CA', '95616', etc." } }, "propertyOrdering": [ "location" ] } } ] } ] } headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '1731' Content-Type: - application/json User-Agent: - python-requests/2.32.3 method: POST uri: https://us-central1-aiplatform.googleapis.com/v1/projects/fake-project/locations/us-central1/publishers/google/models/gemini-2.5-pro:generateContent?%24alt=json%3Benum-encoding%3Dint response: body: string: |- { "candidates": [ { "content": { "role": "model", "parts": [ { "text": "The weather in New Delhi is 35\u00b0C and in San Francisco is 25\u00b0C.", "thoughtSignature": "CqcOAcu98PDUiUq32HLxu6y5JNxvlcEjIaedPcBi5V86Hbf3vRgAXC4k0aMma0v1gotZVHrinF9edI9bEAdQdFR+2xOsaV1ntNeO4o35ymNNpm1rEv2p047eWxSABiXJ3VANecxNqQuVgxZyhClOn2BNmR/xGb43REabpcMzboVGVT6iKSJ/g3sCIt4bddY1IQ5zTdSV7lvCyZLuu9736VCJjULzAslhxSb9/xBlQu/pvcak6CmFFLZuDamqeJ3RvhalpklF6qz/Eq9nhhpdRPTERmGU7mNe4fiSpql6JxelO56ksKKGzSG+USa9vxcCQRRgAoaLlMHRegwPjfoA4hHgbpLBIUk9BNXrzmW5APOpJO3rqgIvKQ/WsZzIBI3l+slI51WHgVLr4qNvDUe8VKLhbApVj3L+rb5dQ/u6U9V2oSVA5pQFt6Lqubyjg8yCl0q+0dsvIYUqJSaDRONMfS5hwVu+RTU/SOEXLwTNEiDDuj3wO8xQuyitDAHqya797sOH2ThlSoT/c7V3Du00TBCk0eq8XaxyTobPMCaKNbrqjRTfMn5fortZlPwyAJqoTZsaSeaRufCFpgBMy34/QcLiCdVwKN6rLgBpDhtzThnyBzavDP2ltu8sxWEisJXEOel0q8LvBqTqlFY9dWrRScnB/TvitGnwqlV0PVpWU60wH/2Agexw0+aFlMRYxiFRjuHPK9R2UfLbEzVpDC8M+f16QRZOnWQc8L+cxEW/xHT6hCUxcTmSrpldAs2Ss8A21NcPTlr0lNviZxB+4xDq1dxB/VMS/qOnGVbr11nEOOVj+bWJyP/LLTjdUigGs+ISAzmEr2zkUirLdlmz/Mz1YXk9CMIus8WlPVzhQZuXY8dhSKxks0w8bJ5TjJelAwPGFgz6iWSz44IeaH22Fxdvub28/mdmvy3OerI3vfwlePUC9L8n2cg7gZjIE59PXi2x1EUTJfvcBrVTO5qqPuRpJZ9/d3ryEtS9tHsMnzfcEiS2lxNb+EqKaHk74hqvt6vEA+eQaYtMqG5TO79dpGDt0UD1cJEwngfMiL85+5qWHW4g2hSSJYgg55GtdZbxdGs960p8G02FbqUOSVcDGcIBDCkvFSGHJqMATvYQzChiwP2AnDVLN0aWd43UEl3rHESdOGLOqrxetcWmVsDacowhiy6jleS/Xml7d8mqqs2483XCOVMMYPcFh9budRed7PIf7eAYhWzGwt123JLbxqwBOBf1MHUJl2OQBy62HCJLYMlRhr1s2KVRK9Een5cgUa3H07r/s46YS2A0KOP/B0Ub0MflvGBAgo8LABj5cY2Ao1hTKcnBjgJPTNS0awETbtzU3Y74SPMsJ1Ipu2o76TFS3YFq1Fbit1VsceuYxzcqU04NFBtXAU7YojLv9LBOZXk6SukOWI/2cNUOHlzlWsSi34heVFGH4AcDbhc0/QZHhjSTE9FkBY9feTtSnTR16Q1bg3K9+7rxArD51Wj8q1ZNhVtZrzpuWGqoneI0WqJ25U5f1jSzQFW2HyzmIs/B5KiFEk4Do0EiaVcCQ5/1UGReYbgtSKa8PAvUGh2Lqiev1VMl6i4cDs7s3U9swvZdBdK+SauYC7UqEVov1G8UR8Bth2YncqUPSv7W2X7ppA6PwDXSCqCSgp3wnfEW8tQIa6AQW2s4Udp0lbYWr3Vu2DSK3iu8mVa3ybvHs5Go4SR9zJ8xElMn4adprz8jokYpoXK94DVeaUetKqTw6X3FzRb+K+e/W0MHZVEQtAjk2wwcNqAfs8JdXi2Mh/l+0PFp6Hhz0rI1rzZ3B08lVR+QU+0XpN5rerUmq44oE6t+dP6Cv8yl5kVBCjvtHZ9DLUGeSEofOR3JXs34rkWeOLfQ+vRZkYoiIRKkqFhf/RnRoU18vmfsbvK9C3XcO3wmfo58Iw2uYd9L/p6VdB27pPUiBJbTAzXbEA3CWUbedMg2vDZiucS4iBkWlmN91CofwgmNfiImdcauz5ug3TC5qRWUIqX/nNMSscPjpbhp3nmja727fyH1p4ytpXQbRX0do28KpY5XQ9yF9gAWxG2zHxtBGiZVG4iXRlpZlfmFUc4kQLUTrInYUoagh9zF6Cxwrz7smRqgOkWKOYiOTiR9ofTkvpmQpdGpDOsxjcPRd3V+jjkfZwT5yzsM7IOAzzhZKKvPWgfJfzaAI43W+dUWiMWzM5KX7Wliu5J3uEXtldo0lW2WpOiuFK5GIPBh5482IbSAtA14YEplOYVCrCI9UG356TImFYyCLuNKAxbzA3U9TSJCk0tUbssWKq8aIaoEbtvE43P1G8x5teVFR5Wu1nZypgKgYFSS5tRL+niDV82yaCOTIGjdA6yFHq8gRlv9xkmZjyPIB4LfAhAIEz9X0+hNvZBfTofi1UXRh/54GIqr1YIXmJVYKb/srAU5Qne5lQdvFauFIQbnlESVUeb1RA==" } ] }, "finishReason": 1, "avgLogprobs": -1.18875130740079 } ], "usageMetadata": { "promptTokenCount": 128, "candidatesTokenCount": 22, "totalTokenCount": 575, "trafficType": 1, "promptTokensDetails": [ { "modality": 1, "tokenCount": 128 } ], "candidatesTokensDetails": [ { "modality": 1, "tokenCount": 22 } ], "thoughtsTokenCount": 425 }, "modelVersion": "gemini-2.5-pro", "createTime": "2025-08-19T14:55:39.882212Z", "responseId": "a5CkaKTsNa3hgLUP59n1oQo" } headers: Content-Type: - application/json; charset=UTF-8 Transfer-Encoding: - chunked Vary: - Origin - X-Origin - Referer content-length: - '3277' status: code: 200 message: OK version: 1 conftest.py000066400000000000000000000305031527070072300373360ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Unit tests configuration module.""" import asyncio import json import os import re from collections.abc import Callable, Generator, Mapping, MutableMapping from typing import ( Any, Concatenate, Protocol, TypeVar, ) import pytest import vertexai import yaml from google.auth.aio.credentials import ( AnonymousCredentials as AsyncAnonymousCredentials, ) from google.auth.credentials import AnonymousCredentials from google.cloud.aiplatform.initializer import _set_async_rest_credentials from typing_extensions import ParamSpec from vcr import VCR from vcr.record_mode import RecordMode from vcr.request import Request from vertexai.generative_models import ( GenerativeModel, ) from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor from opentelemetry.instrumentation.vertexai.utils import ( OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, ) from opentelemetry.sdk._logs import LoggerProvider # Backward compatibility for InMemoryLogExporter -> InMemoryLogRecordExporter rename try: from opentelemetry.sdk._logs.export import ( # pylint: disable=no-name-in-module InMemoryLogRecordExporter, SimpleLogRecordProcessor, ) except ImportError: # Fallback to old name for compatibility with older SDK versions from opentelemetry.sdk._logs.export import ( InMemoryLogExporter as InMemoryLogRecordExporter, ) from opentelemetry.sdk._logs.export import ( SimpleLogRecordProcessor, ) from opentelemetry.sdk.metrics import ( MeterProvider, ) from opentelemetry.sdk.metrics.export import ( InMemoryMetricReader, ) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) FAKE_PROJECT = "fake-project" @pytest.fixture(scope="function", name="span_exporter") def fixture_span_exporter(): exporter = InMemorySpanExporter() yield exporter @pytest.fixture(scope="function", name="log_exporter") def fixture_log_exporter(): exporter = InMemoryLogRecordExporter() yield exporter @pytest.fixture(scope="function", name="metric_reader") def fixture_metric_reader(): exporter = InMemoryMetricReader() yield exporter @pytest.fixture(scope="function", name="tracer_provider") def fixture_tracer_provider(span_exporter): provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(span_exporter)) return provider @pytest.fixture(scope="function", name="logger_provider") def fixture_logger_provider(log_exporter): provider = LoggerProvider() provider.add_log_record_processor(SimpleLogRecordProcessor(log_exporter)) return provider @pytest.fixture(scope="function", name="meter_provider") def fixture_meter_provider(metric_reader): return MeterProvider( metric_readers=[metric_reader], ) @pytest.fixture(autouse=True) def vertexai_init(vcr: VCR) -> None: # When not recording (in CI), don't do any auth. That prevents trying to read application # default credentials from the filesystem or metadata server and oauth token exchange. This # is not the interesting part of our instrumentation to test. credentials = None project = None if vcr.record_mode == RecordMode.NONE: credentials = AnonymousCredentials() project = FAKE_PROJECT vertexai.init(api_transport="rest", credentials=credentials, project=project) @pytest.fixture(scope="function") def instrument_no_content(tracer_provider, logger_provider, meter_provider, request): # Reset global state.. _OpenTelemetrySemanticConventionStability._initialized = False os.environ.update({OTEL_SEMCONV_STABILITY_OPT_IN: "stable"}) os.environ.update({OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "False"}) instrumentor = VertexAIInstrumentor() instrumentor.instrument( tracer_provider=tracer_provider, logger_provider=logger_provider, meter_provider=meter_provider, ) yield instrumentor os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None) if instrumentor.is_instrumented_by_opentelemetry: instrumentor.uninstrument() @pytest.fixture(scope="function") def instrument_no_content_with_experimental_semconvs(tracer_provider, logger_provider, meter_provider, request): # Reset global state.. _OpenTelemetrySemanticConventionStability._initialized = False os.environ.update({OTEL_SEMCONV_STABILITY_OPT_IN: "gen_ai_latest_experimental"}) os.environ.update({OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "NO_CONTENT"}) instrumentor = VertexAIInstrumentor() instrumentor.instrument( tracer_provider=tracer_provider, logger_provider=logger_provider, meter_provider=meter_provider, ) yield instrumentor os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None) if instrumentor.is_instrumented_by_opentelemetry: instrumentor.uninstrument() @pytest.fixture(scope="function") def instrument_with_experimental_semconvs(tracer_provider, logger_provider, meter_provider): # Reset global state.. _OpenTelemetrySemanticConventionStability._initialized = False os.environ.update({OTEL_SEMCONV_STABILITY_OPT_IN: "gen_ai_latest_experimental"}) os.environ.update({OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "SPAN_AND_EVENT"}) instrumentor = VertexAIInstrumentor() instrumentor.instrument( tracer_provider=tracer_provider, logger_provider=logger_provider, meter_provider=meter_provider, ) yield instrumentor os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None) if instrumentor.is_instrumented_by_opentelemetry: instrumentor.uninstrument() @pytest.fixture(scope="function") def instrument_with_upload_hook(tracer_provider, logger_provider, meter_provider): # Reset global state.. _OpenTelemetrySemanticConventionStability._initialized = False os.environ.update( { OTEL_SEMCONV_STABILITY_OPT_IN: "gen_ai_latest_experimental", "OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK": "upload", "OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH": "memory://", OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "SPAN_AND_EVENT", } ) instrumentor = VertexAIInstrumentor() instrumentor.instrument( tracer_provider=tracer_provider, logger_provider=logger_provider, meter_provider=meter_provider, ) yield instrumentor os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None) os.environ.pop("OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK", None) os.environ.pop("OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH", None) if instrumentor.is_instrumented_by_opentelemetry: instrumentor.uninstrument() @pytest.fixture(scope="function") def instrument_with_content(tracer_provider, logger_provider, meter_provider, request): # Reset global state.. _OpenTelemetrySemanticConventionStability._initialized = False os.environ.update({OTEL_SEMCONV_STABILITY_OPT_IN: "stable"}) os.environ.update({OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "True"}) instrumentor = VertexAIInstrumentor() instrumentor.instrument( tracer_provider=tracer_provider, logger_provider=logger_provider, meter_provider=meter_provider, ) yield instrumentor os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None) if instrumentor.is_instrumented_by_opentelemetry: instrumentor.uninstrument() @pytest.fixture(scope="module") def vcr_config(): filter_header_regexes = [ r"X-.*", "Server", "Date", "Expires", "Authorization", ] def filter_headers(headers: Mapping[str, str]) -> Mapping[str, str]: return { key: val for key, val in headers.items() if not any(re.match(filter_re, key, re.IGNORECASE) for filter_re in filter_header_regexes) } def before_record_cb(request: Request): request.headers = filter_headers(request.headers) request.uri = re.sub(r"/projects/[^/]+/", "/projects/fake-project/", request.uri) return request def before_response_cb(response: MutableMapping[str, Any]): response["headers"] = filter_headers(response["headers"]) return response return { "decode_compressed_response": True, "before_record_request": before_record_cb, "before_record_response": before_response_cb, "ignore_hosts": ["oauth2.googleapis.com"], } class LiteralBlockScalar(str): """Formats the string as a literal block scalar, preserving whitespace and without interpreting escape characters""" def literal_block_scalar_presenter(dumper, data): """Represents a scalar string as a literal block, via '|' syntax""" return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|") yaml.add_representer(LiteralBlockScalar, literal_block_scalar_presenter) def process_string_value(string_value): """Pretty-prints JSON or returns long strings as a LiteralBlockScalar""" try: json_data = json.loads(string_value) return LiteralBlockScalar(json.dumps(json_data, indent=2)) except (ValueError, TypeError): if len(string_value) > 80: return LiteralBlockScalar(string_value) return string_value def convert_body_to_literal(data): """Searches the data for body strings, attempting to pretty-print JSON""" if isinstance(data, dict): for key, value in data.items(): # Handle response body case (e.g., response.body.string) if key == "body" and isinstance(value, dict) and "string" in value: value["string"] = process_string_value(value["string"]) # Handle request body case (e.g., request.body) elif key == "body" and isinstance(value, str): data[key] = process_string_value(value) else: convert_body_to_literal(value) elif isinstance(data, list): for idx, choice in enumerate(data): data[idx] = convert_body_to_literal(choice) return data class PrettyPrintJSONBody: """This makes request and response body recordings more readable.""" @staticmethod def serialize(cassette_dict): cassette_dict = convert_body_to_literal(cassette_dict) return yaml.dump(cassette_dict, default_flow_style=False, allow_unicode=True) @staticmethod def deserialize(cassette_string): return yaml.load(cassette_string, Loader=yaml.Loader) @pytest.fixture(scope="module", autouse=True) def fixture_vcr(vcr): vcr.register_serializer("yaml", PrettyPrintJSONBody) return vcr _P = ParamSpec("_P") _R = TypeVar("_R") def _copy_signature( func_type: Callable[_P, _R], ) -> Callable[[Callable[..., Any]], Callable[Concatenate[GenerativeModel, _P], _R]]: return lambda func: func # Type annotation for fixture to make LSP work properly class GenerateContentFixture(Protocol): @_copy_signature(GenerativeModel.generate_content) def __call__(self): ... @pytest.fixture( name="generate_content", params=( pytest.param(False, id="sync"), pytest.param(True, id="async"), ), ) def fixture_generate_content( request: pytest.FixtureRequest, vcr: VCR, ) -> Generator[GenerateContentFixture, None, None]: """This fixture parameterizes tests that use it to test calling both GenerativeModel.generate_content() and GenerativeModel.generate_content_async(). """ is_async: bool = request.param if is_async: # See # https://github.com/googleapis/python-aiplatform/blob/cb0e5fedbf45cb0531c0b8611fb7fabdd1f57e56/google/cloud/aiplatform/initializer.py#L717-L729 _set_async_rest_credentials(credentials=AsyncAnonymousCredentials()) def wrapper(model: GenerativeModel, *args, **kwargs) -> None: if is_async: return asyncio.run(model.generate_content_async(*args, **kwargs)) return model.generate_content(*args, **kwargs) with vcr.use_cassette(request.node.originalname, allow_playback_repeats=True): yield wrapper requirements.latest.txt000066400000000000000000000052011527070072300417130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests# Copyright The OpenTelemetry Authors # # 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. # ******************************** # WARNING: NOT HERMETIC !!!!!!!!!! # ******************************** # # This "requirements.txt" is installed in conjunction # with multiple other dependencies in the top-level "tox.ini" # file. In particular, please see: # # vertexai-latest: {[testenv]test_deps} # vertexai-latest: -r {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/requirements.latest.txt # # This provides additional dependencies, namely: # # opentelemetry-api # opentelemetry-sdk # opentelemetry-semantic-conventions # # ... with a "dev" version based on the latest distribution. # This variant of the requirements aims to test the system using # the newest supported version of external dependencies. annotated-types==0.7.0 asgiref==3.8.1 cachetools==5.5.0 certifi==2024.8.30 charset-normalizer==3.4.0 Deprecated==1.3.1 docstring_parser==0.16 exceptiongroup==1.2.2 google-api-core[grpc, async_rest]==2.23.0 google-auth[aiohttp]==2.45.0 google-cloud-aiplatform==1.79.0 google-cloud-bigquery==3.27.0 google-cloud-core==2.4.1 google-cloud-resource-manager==1.13.1 google-cloud-storage==2.19.0 google-crc32c==1.5.0 google-resumable-media==2.7.2 googleapis-common-protos==1.66.0 grpc-google-iam-v1==0.13.1 grpcio-status==1.68.1 idna==3.10 importlib_metadata==8.5.0 iniconfig==2.0.0 multidict==6.1.0 packaging==24.2 pluggy==1.6.0 propcache==0.2.0 proto-plus==1.25.0 protobuf==5.29.1 pyasn1==0.6.1 pyasn1_modules==0.4.1 pydantic==2.12.5 pytest==7.4.4 pytest-asyncio==0.21.0 pytest-vcr==1.0.2 python-dateutil==2.9.0.post0 PyYAML==6.0.2 requests==2.33.1 rsa==4.9 fsspec==2025.9.0 six==1.17.0 tomli==2.2.1 typing_extensions==4.14.1 urllib3==1.26.20 vcrpy==6.0.2 wrapt==2.1.2 yarl==1.15.2 zipp==3.20.2 grpcio>=1.75.1 ; python_version >= "3.14" shapely==2.0.6 ; python_version < "3.10" shapely==2.1.2 ; python_version >= "3.10" # test with the latest version of opentelemetry-api, sdk, and semantic conventions -e opentelemetry-instrumentation -e instrumentation-genai/opentelemetry-instrumentation-vertexai[instruments] -e util/opentelemetry-util-genai[upload] requirements.oldest.txt000066400000000000000000000040721527070072300417160ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests# Copyright The OpenTelemetry Authors # # 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. # This variant of the requirements aims to test the system using # the oldest supported version of external dependencies. annotated-types==0.7.0 cachetools==5.5.0 certifi==2024.8.30 charset-normalizer==3.4.0 Deprecated==1.2.14 docstring_parser==0.16 exceptiongroup==1.2.2 google-api-core[grpc, async_rest]==2.23.0 google-auth[aiohttp]==2.36.0 google-cloud-aiplatform==1.79.0 google-cloud-bigquery==3.27.0 google-cloud-core==2.4.1 google-cloud-resource-manager==1.13.1 google-cloud-storage==2.19.0 google-crc32c==1.5.0 google-resumable-media==2.7.2 googleapis-common-protos==1.66.0 grpc-google-iam-v1==0.13.1 grpcio-status==1.68.1 idna==3.10 importlib-metadata==6.11.0 iniconfig==2.0.0 multidict==6.1.0 packaging==24.0 pluggy==1.6.0 propcache==0.2.0 proto-plus==1.25.0 protobuf==5.29.1 pyasn1==0.6.1 pyasn1_modules==0.4.1 pydantic==2.12.5 pytest==7.4.4 pytest-asyncio==0.21.0 pytest-vcr==1.0.2 python-dateutil==2.9.0.post0 PyYAML==6.0.2 requests==2.33.1 rsa==4.9 six==1.17.0 tomli==2.2.1 typing_extensions==4.14.1 urllib3==1.26.20 vcrpy==6.0.2 wrapt==1.16.0 yarl==1.15.2 zipp==3.20.2 grpcio>=1.75.1 ; python_version >= "3.14" shapely==2.0.6 ; python_version < "3.10" shapely==2.1.2 ; python_version >= "3.10" # when updating, also update in pyproject.toml opentelemetry-api==1.40 opentelemetry-sdk==1.40 opentelemetry-semantic-conventions==0.61b0 opentelemetry-instrumentation==0.61b0 opentelemetry-util-genai[upload]==0.4b0 fsspec==2025.9.0 -e instrumentation-genai/opentelemetry-instrumentation-vertexai[instruments] shared_test_utils.py000066400000000000000000000064201527070072300412370ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from vertexai.generative_models import ( Content, FunctionDeclaration, GenerativeModel, Part, Tool, ) def weather_tool() -> Tool: # Adapted from https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling#parallel-samples get_current_weather_func = FunctionDeclaration( name="get_current_weather", description="Get the current weather in a given location", parameters={ "type": "object", "properties": { "location": { "type": "string", "description": "The location for which to get the weather. " "It can be a city name, a city name and state, or a zip code. " "Examples: 'San Francisco', 'San Francisco, CA', '95616', etc.", }, }, }, ) return Tool( function_declarations=[get_current_weather_func], ) def ask_about_weather(generate_content: callable) -> None: model = GenerativeModel("gemini-2.5-pro", tools=[weather_tool()]) # Model will respond asking for function calls generate_content( model, [ # User asked about weather Content( role="user", parts=[ Part.from_text("Get weather details in New Delhi and San Francisco?"), ], ), ], ) def ask_about_weather_function_response( generate_content: callable, ) -> None: model = GenerativeModel("gemini-2.5-pro", tools=[weather_tool()]) generate_content( model, [ # User asked about weather Content( role="user", parts=[ Part.from_text("Get weather details in New Delhi and San Francisco?"), ], ), # Model requests two function calls Content( role="model", parts=[ Part.from_dict( { "function_call": { "name": "get_current_weather", "args": {"location": "New Delhi"}, } }, ), Part.from_dict( { "function_call": { "name": "get_current_weather", "args": {"location": "San Francisco"}, } }, ), ], ), # User responds with function responses Content( role="user", parts=[ Part.from_function_response( name="get_current_weather", response={"content": '{"temperature": 35, "unit": "C"}'}, ), Part.from_function_response( name="get_current_weather", response={"content": '{"temperature": 25, "unit": "C"}'}, ), ], ), ], ) test_chat_completions.py000066400000000000000000000331171527070072300421070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import pytest from google.api_core.exceptions import BadRequest, NotFound from vertexai.generative_models import ( Content, GenerationConfig, GenerativeModel, Part, ) from vertexai.preview.generative_models import ( GenerativeModel as PreviewGenerativeModel, ) from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor # Backward compatibility for InMemoryLogExporter -> InMemoryLogRecordExporter rename try: from opentelemetry.sdk._logs._internal.export.in_memory_log_exporter import ( # pylint: disable=no-name-in-module InMemoryLogRecordExporter, ) except ImportError: # Fallback to old name for compatibility with older SDK versions from opentelemetry.sdk._logs._internal.export.in_memory_log_exporter import ( InMemoryLogExporter as InMemoryLogRecordExporter, ) from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) from opentelemetry.trace import StatusCode @pytest.mark.vcr() def test_generate_content( span_exporter: InMemorySpanExporter, log_exporter: InMemoryLogRecordExporter, generate_content: callable, instrument_with_content: VertexAIInstrumentor, ): model = GenerativeModel("gemini-2.5-pro") generate_content( model, [ Content( role="user", parts=[ Part.from_text("Say this is a test"), ], ), ], ) # Emits span spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert spans[0].name == "chat gemini-2.5-pro" assert dict(spans[0].attributes) == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "gen_ai.response.finish_reasons": ("stop",), "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.system": "vertex_ai", "gen_ai.usage.input_tokens": 5, "gen_ai.usage.output_tokens": 5, "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, } logs = log_exporter.get_finished_logs() # Emits user and choice events assert len(logs) == 2 user_log, choice_log = [log_data.log_record for log_data in logs] span_context = spans[0].get_span_context() assert user_log.trace_id == span_context.trace_id assert user_log.span_id == span_context.span_id assert user_log.trace_flags == span_context.trace_flags assert user_log.attributes == {"gen_ai.system": "vertex_ai"} assert user_log.event_name == "gen_ai.user.message" assert user_log.body == { "content": [{"text": "Say this is a test"}], "role": "user", } assert choice_log.trace_id == span_context.trace_id assert choice_log.span_id == span_context.span_id assert choice_log.trace_flags == span_context.trace_flags assert choice_log.attributes == {"gen_ai.system": "vertex_ai"} assert choice_log.event_name == "gen_ai.choice" assert choice_log.body == { "finish_reason": "stop", "index": 0, "message": { "content": [{"text": "This is a test."}], "role": "model", }, } @pytest.mark.vcr() def test_generate_content_without_events( span_exporter: InMemorySpanExporter, log_exporter: InMemoryLogRecordExporter, generate_content: callable, instrument_no_content: VertexAIInstrumentor, ): model = GenerativeModel("gemini-2.5-pro") generate_content( model, [ Content(role="user", parts=[Part.from_text("Say this is a test")]), ], ) # Emits span spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert spans[0].name == "chat gemini-2.5-pro" assert dict(spans[0].attributes) == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "gen_ai.response.finish_reasons": ("stop",), "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.system": "vertex_ai", "gen_ai.usage.input_tokens": 5, "gen_ai.usage.output_tokens": 5, "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, } logs = log_exporter.get_finished_logs() # Emits user and choice event without body.content assert len(logs) == 2 user_log, choice_log = [log_data.log_record for log_data in logs] assert user_log.attributes == {"gen_ai.system": "vertex_ai"} assert user_log.event_name == "gen_ai.user.message" assert user_log.body == {"role": "user"} assert choice_log.attributes == {"gen_ai.system": "vertex_ai"} assert choice_log.event_name == "gen_ai.choice" assert choice_log.body == { "finish_reason": "stop", "index": 0, "message": {"role": "model"}, } @pytest.mark.vcr() def test_generate_content_empty_model( span_exporter: InMemorySpanExporter, generate_content: callable, instrument_with_content: VertexAIInstrumentor, ): model = GenerativeModel("") try: generate_content( model, [Content(role="user", parts=[Part.from_text("Say this is a test")])], ) except ValueError: pass spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert spans[0].name == "chat" # Captures invalid params assert dict(spans[0].attributes) == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "", "gen_ai.system": "vertex_ai", "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, } assert_span_error(spans[0]) @pytest.mark.vcr() def test_generate_content_missing_model( span_exporter: InMemorySpanExporter, generate_content: callable, instrument_with_content: VertexAIInstrumentor, ): model = GenerativeModel("gemini-does-not-exist") try: generate_content( model, [Content(role="user", parts=[Part.from_text("Say this is a test")])], ) except NotFound: pass spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert spans[0].name == "chat gemini-does-not-exist" # Captures invalid params assert dict(spans[0].attributes) == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-does-not-exist", "gen_ai.system": "vertex_ai", "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, } assert_span_error(spans[0]) @pytest.mark.vcr() def test_generate_content_invalid_temperature( span_exporter: InMemorySpanExporter, generate_content: callable, instrument_with_content: VertexAIInstrumentor, ): model = GenerativeModel("gemini-2.5-pro") try: # Temperature out of range causes error generate_content( model, [Content(role="user", parts=[Part.from_text("Say this is a test")])], generation_config=GenerationConfig(temperature=1000), ) except BadRequest: pass spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert spans[0].name == "chat gemini-2.5-pro" assert dict(spans[0].attributes) == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "gen_ai.request.temperature": 1000.0, "gen_ai.system": "vertex_ai", "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, } assert_span_error(spans[0]) @pytest.mark.vcr() def test_generate_content_invalid_role( log_exporter: InMemoryLogRecordExporter, generate_content: callable, instrument_with_content: VertexAIInstrumentor, ): model = GenerativeModel("gemini-2.5-pro") try: # Fails because role must be "user" or "model" generate_content( model, [ Content( role="invalid_role", parts=[Part.from_text("Say this is a test")], ) ], ) except BadRequest: pass # Emits the faulty content which caused the request to fail logs = log_exporter.get_finished_logs() assert len(logs) == 1 log = logs[0].log_record assert log.attributes == { "gen_ai.system": "vertex_ai", } assert log.event_name == "gen_ai.user.message" assert log.body == { "content": [{"text": "Say this is a test"}], "role": "invalid_role", } @pytest.mark.vcr() def test_generate_content_extra_params( span_exporter, instrument_no_content, generate_content: callable, ): generation_config = GenerationConfig( top_k=2, top_p=0.95, temperature=0.2, stop_sequences=["\n\n\n"], max_output_tokens=5, presence_penalty=-1.5, frequency_penalty=1.0, seed=12345, ) model = GenerativeModel("gemini-2.5-pro") generate_content( model, [ Content(role="user", parts=[Part.from_text("Say this is a test")]), ], generation_config=generation_config, ) spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert dict(spans[0].attributes) == { "gen_ai.operation.name": "chat", "gen_ai.request.frequency_penalty": 1.0, "gen_ai.request.max_tokens": 5, "gen_ai.request.seed": 12345, "gen_ai.request.model": "gemini-2.5-pro", "gen_ai.request.presence_penalty": -1.5, "gen_ai.request.stop_sequences": ("\n\n\n",), "gen_ai.request.temperature": 0.20000000298023224, "gen_ai.request.top_p": 0.949999988079071, "gen_ai.response.finish_reasons": ("length",), "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.system": "vertex_ai", "gen_ai.usage.input_tokens": 5, "gen_ai.usage.output_tokens": 0, "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, } def assert_span_error(span: ReadableSpan) -> None: # Sets error status assert span.status.status_code == StatusCode.ERROR # TODO: check thate error.type is set # https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md # Records exception event error_events = [e for e in span.events if e.name == "exception"] assert error_events != [] @pytest.mark.vcr() def test_generate_content_all_events( log_exporter: InMemoryLogRecordExporter, generate_content: callable, instrument_with_content: VertexAIInstrumentor, ): generate_content_all_input_events( GenerativeModel( "gemini-2.5-pro", system_instruction=Part.from_text("You are a clever language model"), ), log_exporter, instrument_with_content, ) @pytest.mark.vcr() def test_preview_generate_content_all_input_events( log_exporter: InMemoryLogRecordExporter, generate_content: callable, instrument_with_content: VertexAIInstrumentor, ): generate_content_all_input_events( PreviewGenerativeModel( "gemini-2.5-pro", system_instruction=Part.from_text("You are a clever language model"), ), log_exporter, instrument_with_content, ) def generate_content_all_input_events( model: GenerativeModel | PreviewGenerativeModel, log_exporter: InMemoryLogRecordExporter, instrument_with_content: VertexAIInstrumentor, ): model.generate_content( [ Content(role="user", parts=[Part.from_text("My name is OpenTelemetry")]), Content(role="model", parts=[Part.from_text("Hello OpenTelemetry!")]), Content( role="user", parts=[Part.from_text("Address me by name and say this is a test")], ), ], generation_config=GenerationConfig(seed=12345, response_mime_type="text/plain"), ) # Emits a system event, 2 users events, an assistant event, and the choice (response) event logs = log_exporter.get_finished_logs() assert len(logs) == 5 system_log, user_log1, assistant_log, user_log2, choice_log = [log_data.log_record for log_data in logs] assert system_log.attributes == {"gen_ai.system": "vertex_ai"} assert system_log.event_name == "gen_ai.system.message" assert system_log.body == { "content": [{"text": "You are a clever language model"}], # The API only allows user and model, so system instruction is considered a user role "role": "user", } assert user_log1.attributes == {"gen_ai.system": "vertex_ai"} assert user_log1.event_name == "gen_ai.user.message" assert user_log1.body == { "content": [{"text": "My name is OpenTelemetry"}], "role": "user", } assert assistant_log.attributes == {"gen_ai.system": "vertex_ai"} assert assistant_log.event_name == "gen_ai.assistant.message" assert assistant_log.body == { "content": [{"text": "Hello OpenTelemetry!"}], "role": "model", } assert user_log2.attributes == {"gen_ai.system": "vertex_ai"} assert user_log2.event_name == "gen_ai.user.message" assert user_log2.body == { "content": [{"text": "Address me by name and say this is a test"}], "role": "user", } assert choice_log.attributes == {"gen_ai.system": "vertex_ai"} assert choice_log.event_name == "gen_ai.choice" assert choice_log.body == { "finish_reason": "stop", "index": 0, "message": { "content": [{"text": "OpenTelemetry, this is a test."}], "role": "model", }, } test_chat_completions_experimental.py000066400000000000000000000405001527070072300446560ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import pytest from google.api_core.exceptions import BadRequest, NotFound from vertexai.generative_models import ( Content, GenerationConfig, GenerativeModel, Image, Part, ) from vertexai.preview.generative_models import ( GenerativeModel as PreviewGenerativeModel, ) from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor # Backward compatibility for InMemoryLogExporter -> InMemoryLogRecordExporter rename try: from opentelemetry.sdk._logs._internal.export.in_memory_log_exporter import ( # pylint: disable=no-name-in-module InMemoryLogRecordExporter, ) except ImportError: # Fallback to old name for compatibility with older SDK versions from opentelemetry.sdk._logs._internal.export.in_memory_log_exporter import ( InMemoryLogExporter as InMemoryLogRecordExporter, ) from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) from opentelemetry.trace import StatusCode @pytest.mark.vcr() def test_generate_content_with_files( span_exporter: InMemorySpanExporter, log_exporter: InMemoryLogRecordExporter, generate_content: callable, instrument_with_experimental_semconvs: VertexAIInstrumentor, ): model = GenerativeModel("gemini-2.5-pro") generate_content( model, [ Content( role="user", parts=[ Part.from_text("Say this is a test"), Part.from_uri( mime_type="image/jpeg", uri="https://images.pdimagearchive.org/collections/microscopic-delights/1lede-0021.jpg", ), Part.from_image( Image.from_bytes( "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" ) ), ], ), ], ) # Emits span spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert spans[0].name == "chat gemini-2.5-pro" assert dict(spans[0].attributes) == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "gen_ai.response.finish_reasons": ("stop",), "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.usage.input_tokens": 521, "gen_ai.usage.output_tokens": 5, "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, "gen_ai.input.messages": '[{"role":"user","parts":[{"content":"Say this is a test","type":"text"},{"mime_type":"image/jpeg","modality":"image","uri":"https://images.pdimagearchive.org/collections/microscopic-delights/1lede-0021.jpg","type":"uri"},{"mime_type":"image/jpeg","modality":"image","content":"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==","type":"blob"}]}]', "gen_ai.output.messages": '[{"role":"model","parts":[{"content":"This is a test.","type":"text"}],"finish_reason":"stop"}]', } logs = log_exporter.get_finished_logs() assert len(logs) == 1 log = logs[0].log_record assert log.attributes == { "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.response.finish_reasons": ("stop",), "gen_ai.usage.input_tokens": 521, "gen_ai.usage.output_tokens": 5, "gen_ai.input.messages": ( { "role": "user", "parts": ( {"content": "Say this is a test", "type": "text"}, { "mime_type": "image/jpeg", "modality": "image", "uri": "https://images.pdimagearchive.org/collections/microscopic-delights/1lede-0021.jpg", "type": "uri", }, { "mime_type": "image/jpeg", "modality": "image", "content": b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x05\x00\x00\x00\x05\x08\x06\x00\x00\x00\x8do&\xe5\x00\x00\x00\x1cIDAT\x08\xd7c\xf8\xff\xff?\xc3\x7f\x06 \x05\xc3 \x12\x84\xd01\xf1\x82X\xcd\x04\x00\x0e\xf55\xcb\xd1\x8e\x0e\x1f\x00\x00\x00\x00IEND\xaeB`\x82", "type": "blob", }, ), }, ), "gen_ai.output.messages": ( { "role": "model", "parts": ({"content": "This is a test.", "type": "text"},), "finish_reason": "stop", }, ), } @pytest.mark.vcr() def test_generate_content_without_events( span_exporter: InMemorySpanExporter, log_exporter: InMemoryLogRecordExporter, generate_content: callable, instrument_with_experimental_semconvs: VertexAIInstrumentor, ): model = GenerativeModel("gemini-2.5-pro") generate_content( model, [ Content(role="user", parts=[Part.from_text("Say this is a test")]), ], ) # Emits span spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert spans[0].name == "chat gemini-2.5-pro" assert dict(spans[0].attributes) == { "gen_ai.operation.name": "chat", "gen_ai.output.messages": '[{"role":"model","parts":[{"content":"This is a test.","type":"text"}],"finish_reason":"stop"}]', "gen_ai.input.messages": '[{"role":"user","parts":[{"content":"Say this is a test","type":"text"}]}]', "gen_ai.request.model": "gemini-2.5-pro", "gen_ai.response.finish_reasons": ("stop",), "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.usage.input_tokens": 5, "gen_ai.usage.output_tokens": 5, "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, } logs = log_exporter.get_finished_logs() assert len(logs) == 1 log = logs[0].log_record print(log.attributes) assert log.attributes == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.response.finish_reasons": ("stop",), "gen_ai.usage.input_tokens": 5, "gen_ai.usage.output_tokens": 5, "gen_ai.input.messages": ( { "role": "user", "parts": ({"content": "Say this is a test", "type": "text"},), }, ), "gen_ai.output.messages": ( { "role": "model", "parts": ({"content": "This is a test.", "type": "text"},), "finish_reason": "stop", }, ), } @pytest.mark.vcr() def test_generate_content_empty_model( span_exporter: InMemorySpanExporter, generate_content: callable, instrument_with_experimental_semconvs: VertexAIInstrumentor, ): model = GenerativeModel("") try: generate_content( model, [Content(role="user", parts=[Part.from_text("Say this is a test")])], ) except ValueError: pass spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert spans[0].name == "chat" # Captures invalid params assert dict(spans[0].attributes) == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "", "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, "gen_ai.input.messages": '[{"role":"user","parts":[{"content":"Say this is a test","type":"text"}]}]', } assert_span_error(spans[0]) @pytest.mark.vcr() def test_generate_content_missing_model( span_exporter: InMemorySpanExporter, generate_content: callable, instrument_with_experimental_semconvs: VertexAIInstrumentor, ): model = GenerativeModel("gemini-does-not-exist") try: generate_content( model, [Content(role="user", parts=[Part.from_text("Say this is a test")])], ) except NotFound: pass spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert spans[0].name == "chat gemini-does-not-exist" # Captures invalid params assert dict(spans[0].attributes) == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-does-not-exist", "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, "gen_ai.input.messages": '[{"role":"user","parts":[{"content":"Say this is a test","type":"text"}]}]', } assert_span_error(spans[0]) @pytest.mark.vcr() def test_generate_content_invalid_temperature( span_exporter: InMemorySpanExporter, generate_content: callable, instrument_with_experimental_semconvs: VertexAIInstrumentor, ): model = GenerativeModel("gemini-2.5-pro") try: # Temperature out of range causes error generate_content( model, [Content(role="user", parts=[Part.from_text("Say this is a test")])], generation_config=GenerationConfig(temperature=1000), ) except BadRequest: pass spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert spans[0].name == "chat gemini-2.5-pro" assert dict(spans[0].attributes) == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "gen_ai.request.temperature": 1000.0, "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, "gen_ai.input.messages": '[{"role":"user","parts":[{"content":"Say this is a test","type":"text"}]}]', } assert_span_error(spans[0]) @pytest.mark.vcr() def test_generate_content_invalid_role( log_exporter: InMemoryLogRecordExporter, generate_content: callable, instrument_with_experimental_semconvs: VertexAIInstrumentor, ): model = GenerativeModel("gemini-2.5-pro") try: # Fails because role must be "user" or "model" generate_content( model, [ Content( role="invalid_role", parts=[Part.from_text("Say this is a test")], ) ], ) except BadRequest: pass # Emits the faulty content which caused the request to fail logs = log_exporter.get_finished_logs() assert len(logs) == 1 log = logs[0].log_record assert log.attributes == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, "gen_ai.input.messages": ( { "role": "invalid_role", "parts": ({"type": "text", "content": "Say this is a test"},), }, ), } @pytest.mark.vcr() def test_generate_content_extra_params( span_exporter, instrument_no_content_with_experimental_semconvs, generate_content: callable, ): generation_config = GenerationConfig( top_k=2, top_p=0.95, temperature=0.2, stop_sequences=["\n\n\n"], max_output_tokens=5, presence_penalty=-1.5, frequency_penalty=1.0, seed=12345, ) model = GenerativeModel("gemini-2.5-pro") generate_content( model, [ Content(role="user", parts=[Part.from_text("Say this is a test")]), ], generation_config=generation_config, ) spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert dict(spans[0].attributes) == { "gen_ai.operation.name": "chat", "gen_ai.request.frequency_penalty": 1.0, "gen_ai.request.max_tokens": 5, "gen_ai.request.model": "gemini-2.5-pro", "gen_ai.request.presence_penalty": -1.5, "gen_ai.request.stop_sequences": ("\n\n\n",), "gen_ai.request.temperature": 0.20000000298023224, "gen_ai.request.top_p": 0.949999988079071, "gen_ai.response.finish_reasons": ("length",), "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.request.seed": 12345, "gen_ai.usage.input_tokens": 5, "gen_ai.usage.output_tokens": 0, "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, } def assert_span_error(span: ReadableSpan) -> None: # Sets error status assert span.status.status_code == StatusCode.ERROR # TODO: check thate error.type is set # https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md # Records exception event error_events = [e for e in span.events if e.name == "exception"] assert error_events != [] @pytest.mark.vcr() def test_generate_content_all_events( log_exporter: InMemoryLogRecordExporter, generate_content: callable, instrument_with_experimental_semconvs: VertexAIInstrumentor, ): generate_content_all_input_events( GenerativeModel( "gemini-2.5-pro", system_instruction=Part.from_text("You are a clever language model"), ), log_exporter, instrument_with_experimental_semconvs, ) @pytest.mark.vcr() def test_preview_generate_content_all_input_events( log_exporter: InMemoryLogRecordExporter, generate_content: callable, instrument_with_experimental_semconvs: VertexAIInstrumentor, ): generate_content_all_input_events( PreviewGenerativeModel( "gemini-2.5-pro", system_instruction=Part.from_text("You are a clever language model"), ), log_exporter, instrument_with_experimental_semconvs, ) def generate_content_all_input_events( model: GenerativeModel | PreviewGenerativeModel, log_exporter: InMemoryLogRecordExporter, instrument_with_experimental_semconvs: VertexAIInstrumentor, ): model.generate_content( [ Content(role="user", parts=[Part.from_text("My name is OpenTelemetry")]), Content(role="model", parts=[Part.from_text("Hello OpenTelemetry!")]), Content( role="user", parts=[Part.from_text("Address me by name and say this is a test")], ), ], generation_config=GenerationConfig(seed=12345, response_mime_type="text/plain"), ) # Emits a single log. logs = log_exporter.get_finished_logs() assert len(logs) == 1 log = logs[0].log_record assert log.attributes == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "gen_ai.request.seed": 12345, "gen_ai.output.type": "text", "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.response.finish_reasons": ("stop",), "gen_ai.usage.input_tokens": 25, "gen_ai.usage.output_tokens": 8, "gen_ai.system_instructions": ({"type": "text", "content": "You are a clever language model"},), "gen_ai.input.messages": ( { "role": "user", "parts": ( { "type": "text", "content": "My name is OpenTelemetry", }, ), }, { "role": "model", "parts": ({"type": "text", "content": "Hello OpenTelemetry!"},), }, { "role": "user", "parts": ( { "type": "text", "content": "Address me by name and say this is a test", }, ), }, ), "gen_ai.output.messages": ( { "role": "model", "parts": ( { "type": "text", "content": "OpenTelemetry, this is a test.", }, ), "finish_reason": "stop", }, ), } test_function_calling.py000066400000000000000000000232671527070072300420770ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import pytest from tests.shared_test_utils import ( ask_about_weather, ask_about_weather_function_response, ) from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor # Backward compatibility for InMemoryLogExporter -> InMemoryLogRecordExporter rename try: from opentelemetry.sdk._logs._internal.export.in_memory_log_exporter import ( # pylint: disable=no-name-in-module InMemoryLogRecordExporter, ) except ImportError: # Fallback to old name for compatibility with older SDK versions from opentelemetry.sdk._logs._internal.export.in_memory_log_exporter import ( InMemoryLogExporter as InMemoryLogRecordExporter, ) from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) @pytest.mark.vcr() def test_function_call_choice( span_exporter: InMemorySpanExporter, log_exporter: InMemoryLogRecordExporter, instrument_with_content: VertexAIInstrumentor, generate_content: callable, ): ask_about_weather(generate_content) # Emits span spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert spans[0].name == "chat gemini-2.5-pro" assert dict(spans[0].attributes) == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "gen_ai.response.finish_reasons": ("stop",), "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.system": "vertex_ai", "gen_ai.usage.input_tokens": 74, "gen_ai.usage.output_tokens": 16, "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, } # Emits user and choice events logs = log_exporter.get_finished_logs() assert len(logs) == 2 user_log, choice_log = [log_data.log_record for log_data in logs] assert user_log.attributes == {"gen_ai.system": "vertex_ai"} assert user_log.event_name == "gen_ai.user.message" assert user_log.body == { "content": [{"text": "Get weather details in New Delhi and San Francisco?"}], "role": "user", } assert choice_log.attributes == {"gen_ai.system": "vertex_ai"} assert choice_log.event_name == "gen_ai.choice" assert choice_log.body == { "finish_reason": "stop", "index": 0, "message": { "content": [ { "function_call": { "args": {"location": "New Delhi"}, "name": "get_current_weather", } }, { "function_call": { "args": {"location": "San Francisco"}, "name": "get_current_weather", } }, ], "role": "model", }, "tool_calls": [ { "function": { "arguments": {"location": "New Delhi"}, "name": "get_current_weather", }, "id": "get_current_weather_0", "type": "function", }, { "function": { "arguments": {"location": "San Francisco"}, "name": "get_current_weather", }, "id": "get_current_weather_1", "type": "function", }, ], } @pytest.mark.vcr() def test_function_call_choice_no_content( log_exporter: InMemoryLogRecordExporter, instrument_no_content: VertexAIInstrumentor, generate_content: callable, ): ask_about_weather(generate_content) # Emits user and choice events logs = log_exporter.get_finished_logs() assert len(logs) == 2 user_log, choice_log = [log_data.log_record for log_data in logs] assert user_log.attributes == {"gen_ai.system": "vertex_ai"} assert user_log.event_name == "gen_ai.user.message" assert user_log.body == { "role": "user", } assert choice_log.attributes == {"gen_ai.system": "vertex_ai"} assert choice_log.event_name == "gen_ai.choice" assert choice_log.body == { "finish_reason": "stop", "index": 0, "message": {"role": "model"}, "tool_calls": [ { "function": {"name": "get_current_weather"}, "id": "get_current_weather_0", "type": "function", }, { "function": {"name": "get_current_weather"}, "id": "get_current_weather_1", "type": "function", }, ], } @pytest.mark.vcr() def test_tool_events( span_exporter: InMemorySpanExporter, log_exporter: InMemoryLogRecordExporter, instrument_with_content: VertexAIInstrumentor, generate_content: callable, ): ask_about_weather_function_response(generate_content) # Emits span spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert spans[0].name == "chat gemini-2.5-pro" assert dict(spans[0].attributes) == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "gen_ai.response.finish_reasons": ("stop",), "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.system": "vertex_ai", "gen_ai.usage.input_tokens": 128, "gen_ai.usage.output_tokens": 26, "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, } logs = log_exporter.get_finished_logs() # Emits user, assistant, two tool, and choice events assert len(logs) == 5 user_log, assistant_log, tool_log1, tool_log2, choice_log = [log_data.log_record for log_data in logs] assert user_log.attributes == {"gen_ai.system": "vertex_ai"} assert user_log.event_name == "gen_ai.user.message" assert user_log.body == { "content": [{"text": "Get weather details in New Delhi and San Francisco?"}], "role": "user", } assert assistant_log.attributes == {"gen_ai.system": "vertex_ai"} assert assistant_log.event_name == "gen_ai.assistant.message" assert assistant_log.body == { "role": "model", "content": [ { "function_call": { "name": "get_current_weather", "args": {"location": "New Delhi"}, } }, { "function_call": { "name": "get_current_weather", "args": {"location": "San Francisco"}, } }, ], } assert tool_log1.attributes == {"gen_ai.system": "vertex_ai"} assert tool_log1.event_name == "gen_ai.tool.message" assert tool_log1.body == { "role": "user", "id": "get_current_weather_0", "content": {"content": '{"temperature": 35, "unit": "C"}'}, } assert tool_log2.attributes == {"gen_ai.system": "vertex_ai"} assert tool_log2.event_name == "gen_ai.tool.message" assert tool_log2.body == { "role": "user", "id": "get_current_weather_1", "content": {"content": '{"temperature": 25, "unit": "C"}'}, } assert choice_log.attributes == {"gen_ai.system": "vertex_ai"} assert choice_log.event_name == "gen_ai.choice" assert choice_log.body == { "finish_reason": "stop", "index": 0, "message": { "content": [{"text": "The current temperature in New Delhi is 35°C, and in San Francisco, it is 25°C."}], "role": "model", }, } @pytest.mark.vcr() def test_tool_events_no_content( span_exporter: InMemorySpanExporter, log_exporter: InMemoryLogRecordExporter, instrument_no_content: VertexAIInstrumentor, generate_content: callable, ): ask_about_weather_function_response(generate_content) # Emits span spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert spans[0].name == "chat gemini-2.5-pro" assert dict(spans[0].attributes) == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "gen_ai.response.finish_reasons": ("stop",), "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.system": "vertex_ai", "gen_ai.usage.input_tokens": 128, "gen_ai.usage.output_tokens": 22, "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, } logs = log_exporter.get_finished_logs() # Emits user, assistant, two tool, and choice events assert len(logs) == 5 user_log, assistant_log, tool_log1, tool_log2, choice_log = [log_data.log_record for log_data in logs] assert user_log.attributes == {"gen_ai.system": "vertex_ai"} assert user_log.event_name == "gen_ai.user.message" assert user_log.body == {"role": "user"} assert assistant_log.attributes == {"gen_ai.system": "vertex_ai"} assert assistant_log.event_name == "gen_ai.assistant.message" assert assistant_log.body == {"role": "model"} assert tool_log1.attributes == { "gen_ai.system": "vertex_ai", } assert tool_log1.event_name == "gen_ai.tool.message" assert tool_log1.body == { "role": "user", "id": "get_current_weather_0", } assert tool_log1.event_name == "gen_ai.tool.message" assert tool_log2.attributes == {"gen_ai.system": "vertex_ai"} assert tool_log2.body == { "role": "user", "id": "get_current_weather_1", } assert tool_log2.event_name == "gen_ai.tool.message" assert choice_log.attributes == {"gen_ai.system": "vertex_ai"} assert choice_log.event_name == "gen_ai.choice" assert choice_log.body == { "finish_reason": "stop", "index": 0, "message": {"role": "model"}, } test_function_calling_experimental.py000066400000000000000000000321071527070072300446450ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import time from typing import Any import fsspec import pytest from tests.shared_test_utils import ( ask_about_weather, ask_about_weather_function_response, ) from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor # Backward compatibility for InMemoryLogExporter -> InMemoryLogRecordExporter rename try: from opentelemetry.sdk._logs._internal.export.in_memory_log_exporter import ( # pylint: disable=no-name-in-module InMemoryLogRecordExporter, ) except ImportError: # Fallback to old name for compatibility with older SDK versions from opentelemetry.sdk._logs._internal.export.in_memory_log_exporter import ( InMemoryLogExporter as InMemoryLogRecordExporter, ) from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) def test_function_call_choice( span_exporter: InMemorySpanExporter, log_exporter: InMemoryLogRecordExporter, instrument_with_experimental_semconvs: VertexAIInstrumentor, generate_content: callable, ): ask_about_weather(generate_content) # Emits span spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert spans[0].name == "chat gemini-2.5-pro" assert dict(spans[0].attributes) == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "gen_ai.response.finish_reasons": ("stop",), "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.usage.input_tokens": 74, "gen_ai.usage.output_tokens": 16, "server.address": "us-central1-aiplatform.googleapis.com", "gen_ai.input.messages": '[{"role":"user","parts":[{"content":"Get weather details in New Delhi and San Francisco?","type":"text"}]}]', "gen_ai.output.messages": '[{"role":"model","parts":[{"arguments":{"location":"New Delhi"},"name":"get_current_weather","id":"get_current_weather_0","type":"tool_call"},{"arguments":{"location":"San Francisco"},"name":"get_current_weather","id":"get_current_weather_1","type":"tool_call"}],"finish_reason":"stop"}]', "server.port": 443, } # Emits user and choice events logs = log_exporter.get_finished_logs() assert len(logs) == 1 log = logs[0].log_record assert log.attributes == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.response.finish_reasons": ("stop",), "gen_ai.usage.input_tokens": 74, "gen_ai.usage.output_tokens": 16, "gen_ai.input.messages": ( { "role": "user", "parts": ( { "type": "text", "content": "Get weather details in New Delhi and San Francisco?", }, ), }, ), "gen_ai.output.messages": ( { "role": "model", "parts": ( { "type": "tool_call", "arguments": {"location": "New Delhi"}, "name": "get_current_weather", "id": "get_current_weather_0", }, { "type": "tool_call", "arguments": {"location": "San Francisco"}, "name": "get_current_weather", "id": "get_current_weather_1", }, ), "finish_reason": "stop", }, ), } @pytest.mark.vcr() def test_function_call_choice_no_content( log_exporter: InMemoryLogRecordExporter, instrument_no_content_with_experimental_semconvs: VertexAIInstrumentor, generate_content: callable, ): ask_about_weather(generate_content) # Emits user and choice events logs = log_exporter.get_finished_logs() assert len(logs) == 1 log = logs[0].log_record assert log.attributes == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.response.finish_reasons": ("stop",), "gen_ai.usage.input_tokens": 74, "gen_ai.usage.output_tokens": 16, } @pytest.mark.vcr() def test_tool_events( span_exporter: InMemorySpanExporter, log_exporter: InMemoryLogRecordExporter, instrument_with_experimental_semconvs: VertexAIInstrumentor, generate_content: callable, ): ask_about_weather_function_response(generate_content) # Emits span spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert spans[0].name == "chat gemini-2.5-pro" assert dict(spans[0].attributes) == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "gen_ai.response.finish_reasons": ("stop",), "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.usage.input_tokens": 128, "gen_ai.usage.output_tokens": 26, "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, "gen_ai.input.messages": '[{"role":"user","parts":[{"content":"Get weather details in New Delhi and San Francisco?","type":"text"}]},{"role":"model","parts":[{"arguments":{"location":"New Delhi"},"name":"get_current_weather","id":"get_current_weather_0","type":"tool_call"},{"arguments":{"location":"San Francisco"},"name":"get_current_weather","id":"get_current_weather_1","type":"tool_call"}]},{"role":"user","parts":[{"response":{"content":"{\\"temperature\\": 35, \\"unit\\": \\"C\\"}"},"id":"get_current_weather_0","type":"tool_call_response"},{"response":{"content":"{\\"temperature\\": 25, \\"unit\\": \\"C\\"}"},"id":"get_current_weather_1","type":"tool_call_response"}]}]', "gen_ai.output.messages": '[{"role":"model","parts":[{"content":"The current temperature in New Delhi is 35\\u00b0C, and in San Francisco, it is 25\\u00b0C.","type":"text"}],"finish_reason":"stop"}]', } logs = log_exporter.get_finished_logs() assert len(logs) == 1 log = logs[0].log_record assert log.attributes == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.response.finish_reasons": ("stop",), "gen_ai.usage.input_tokens": 128, "gen_ai.usage.output_tokens": 26, "gen_ai.input.messages": ( { "role": "user", "parts": ( { "type": "text", "content": "Get weather details in New Delhi and San Francisco?", }, ), }, { "role": "model", "parts": ( { "type": "tool_call", "arguments": {"location": "New Delhi"}, "name": "get_current_weather", "id": "get_current_weather_0", }, { "type": "tool_call", "arguments": {"location": "San Francisco"}, "name": "get_current_weather", "id": "get_current_weather_1", }, ), }, { "role": "user", "parts": ( { "type": "tool_call_response", "response": {"content": '{"temperature": 35, "unit": "C"}'}, "id": "get_current_weather_0", }, { "type": "tool_call_response", "response": {"content": '{"temperature": 25, "unit": "C"}'}, "id": "get_current_weather_1", }, ), }, ), "gen_ai.output.messages": ( { "role": "model", "parts": ( { "type": "text", "content": "The current temperature in New Delhi is 35°C, and in San Francisco, it is 25°C.", }, ), "finish_reason": "stop", }, ), } @pytest.mark.vcr() def test_tool_events_no_content( span_exporter: InMemorySpanExporter, log_exporter: InMemoryLogRecordExporter, instrument_no_content_with_experimental_semconvs: VertexAIInstrumentor, generate_content: callable, ): ask_about_weather_function_response(generate_content) # Emits span spans = span_exporter.get_finished_spans() assert len(spans) == 1 assert spans[0].name == "chat gemini-2.5-pro" assert dict(spans[0].attributes) == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "gen_ai.response.finish_reasons": ("stop",), "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.usage.input_tokens": 128, "gen_ai.usage.output_tokens": 22, "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, } logs = log_exporter.get_finished_logs() assert len(logs) == 1 log = logs[0].log_record assert log.attributes == { "gen_ai.operation.name": "chat", "gen_ai.request.model": "gemini-2.5-pro", "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, "gen_ai.response.model": "gemini-2.5-pro", "gen_ai.response.finish_reasons": ("stop",), "gen_ai.usage.input_tokens": 128, "gen_ai.usage.output_tokens": 22, } def assert_fsspec_equal(path: str, value: Any) -> None: # Hide this function and its calls from traceback. __tracebackhide__ = True # pylint: disable=unused-variable with fsspec.open(path, "r") as file: assert json.load(file) == value @pytest.mark.vcr() def test_tool_events_with_completion_hook( span_exporter: InMemorySpanExporter, log_exporter: InMemoryLogRecordExporter, instrument_with_upload_hook: VertexAIInstrumentor, generate_content: callable, ): ask_about_weather_function_response(generate_content) # Emits span spans = span_exporter.get_finished_spans() assert len(spans) == 1 logs = log_exporter.get_finished_logs() assert len(logs) == 1 # File upload takes a few seconds sometimes. time.sleep(3) # Both log and span have the same reference attributes from upload hook for key in "gen_ai.input.messages_ref", "gen_ai.output.messages_ref": assert spans[0].attributes.get(key) assert logs[0].log_record.attributes.get(key) assert spans[0].attributes[key] == logs[0].log_record.attributes[key] assert_fsspec_equal( spans[0].attributes["gen_ai.output.messages_ref"], [ { "role": "model", "parts": [ { "content": "The weather in New Delhi is 35°C and in San Francisco is 25°C.", "type": "text", } ], "finish_reason": "stop", } ], ) assert_fsspec_equal( spans[0].attributes["gen_ai.input.messages_ref"], [ { "parts": [ { "content": "Get weather details in New Delhi and San Francisco?", "type": "text", } ], "role": "user", }, { "parts": [ { "arguments": {"location": "New Delhi"}, "id": "get_current_weather_0", "name": "get_current_weather", "type": "tool_call", }, { "arguments": {"location": "San Francisco"}, "id": "get_current_weather_1", "name": "get_current_weather", "type": "tool_call", }, ], "role": "model", }, { "parts": [ { "id": "get_current_weather_0", "response": {"content": '{"temperature": 35, "unit": "C"}'}, "type": "tool_call_response", }, { "id": "get_current_weather_1", "response": {"content": '{"temperature": 25, "unit": "C"}'}, "type": "tool_call_response", }, ], "role": "user", }, ], ) test_instrumentor.py000066400000000000000000000017711527070072300413260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import pytest from google.cloud.aiplatform_v1.services.prediction_service import client from google.cloud.aiplatform_v1beta1.services.prediction_service import ( client as client_v1beta1, ) from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor @pytest.fixture( name="client_class", params=[ pytest.param(client.PredictionServiceClient, id="v1"), pytest.param(client_v1beta1.PredictionServiceClient, id="v1beta1"), ], ) def fixture_client_class(request: pytest.FixtureRequest): return request.param def test_instruments(instrument_with_content: VertexAIInstrumentor, client_class): assert hasattr(client_class.generate_content, "__wrapped__") def test_uninstruments(instrument_with_content: VertexAIInstrumentor, client_class): instrument_with_content.uninstrument() assert not hasattr(client_class.generate_content, "__wrapped__") test_utils.py000066400000000000000000000030411527070072300377050ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from google.cloud.aiplatform_v1.types import ( content, ) from google.cloud.aiplatform_v1beta1.types import ( content as content_v1beta1, ) from opentelemetry.instrumentation.vertexai.utils import ( _map_finish_reason, get_server_attributes, ) def test_get_server_attributes() -> None: # without port assert get_server_attributes("us-central1-aiplatform.googleapis.com") == { "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, } # with port assert get_server_attributes("us-central1-aiplatform.googleapis.com:5432") == { "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 5432, } def test_map_finish_reason(): for Enum in ( content.Candidate.FinishReason, content_v1beta1.Candidate.FinishReason, ): for finish_reason, expect in [ # Handled mappings (Enum.FINISH_REASON_UNSPECIFIED, "error"), (Enum.OTHER, "error"), (Enum.STOP, "stop"), (Enum.MAX_TOKENS, "length"), # Preserve vertex enum value (Enum.BLOCKLIST, "BLOCKLIST"), (Enum.MALFORMED_FUNCTION_CALL, "MALFORMED_FUNCTION_CALL"), (Enum.PROHIBITED_CONTENT, "PROHIBITED_CONTENT"), (Enum.RECITATION, "RECITATION"), (Enum.SAFETY, "SAFETY"), (Enum.SPII, "SPII"), ]: assert _map_finish_reason(finish_reason) == expect opentelemetry-python-contrib-0.66b2/instrumentation/000077500000000000000000000000001527070072300230105ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/README.md000066400000000000000000000147141527070072300242760ustar00rootroot00000000000000 | Instrumentation | Supported Packages | Metrics support | Semconv status | | --------------- | ------------------ | --------------- | -------------- | | [opentelemetry-instrumentation-aio-pika](./opentelemetry-instrumentation-aio-pika) | aio_pika >= 7.2.0, < 11.0.0 | No | development | [opentelemetry-instrumentation-aiohttp-client](./opentelemetry-instrumentation-aiohttp-client) | aiohttp ~= 3.0 | Yes | migration | [opentelemetry-instrumentation-aiohttp-server](./opentelemetry-instrumentation-aiohttp-server) | aiohttp ~= 3.0 | Yes | migration | [opentelemetry-instrumentation-aiokafka](./opentelemetry-instrumentation-aiokafka) | aiokafka >= 0.8, < 1.0 | No | development | [opentelemetry-instrumentation-aiopg](./opentelemetry-instrumentation-aiopg) | aiopg >= 0.13.0, < 2.0.0 | Yes | migration | [opentelemetry-instrumentation-asgi](./opentelemetry-instrumentation-asgi) | asgiref ~= 3.0 | Yes | migration | [opentelemetry-instrumentation-asyncclick](./opentelemetry-instrumentation-asyncclick) | asyncclick ~= 8.0 | No | development | [opentelemetry-instrumentation-asyncio](./opentelemetry-instrumentation-asyncio) | asyncio | No | development | [opentelemetry-instrumentation-asyncpg](./opentelemetry-instrumentation-asyncpg) | asyncpg >= 0.12.0 | No | migration | [opentelemetry-instrumentation-aws-lambda](./opentelemetry-instrumentation-aws-lambda) | aws_lambda | No | development | [opentelemetry-instrumentation-boto3sqs](./opentelemetry-instrumentation-boto3sqs) | boto3 ~= 1.0 | No | development | [opentelemetry-instrumentation-botocore](./opentelemetry-instrumentation-botocore) | botocore~=1.0,aiobotocore>=2.0,<4.0 | No | development | [opentelemetry-instrumentation-cassandra](./opentelemetry-instrumentation-cassandra) | cassandra-driver ~= 3.25,scylla-driver ~= 3.25 | No | migration | [opentelemetry-instrumentation-celery](./opentelemetry-instrumentation-celery) | celery >= 4.0, < 6.0 | No | development | [opentelemetry-instrumentation-click](./opentelemetry-instrumentation-click) | click >= 8.1.3, < 9.0.0 | No | development | [opentelemetry-instrumentation-confluent-kafka](./opentelemetry-instrumentation-confluent-kafka) | confluent-kafka >= 1.8.2, < 3.0.0 | No | development | [opentelemetry-instrumentation-dbapi](./opentelemetry-instrumentation-dbapi) | dbapi | No | migration | [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 2.0 | Yes | development | [opentelemetry-instrumentation-exceptions](./opentelemetry-instrumentation-exceptions) | exceptions | No | development | [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 5.0.0 | Yes | migration | [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.92 | Yes | migration | [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0 | Yes | migration | [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio >= 1.42.0 | No | development | [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0,httpx2 >= 2.0.0 | Yes | migration | [opentelemetry-instrumentation-jinja2](./opentelemetry-instrumentation-jinja2) | jinja2 >= 2.7, < 4.0 | No | development | [opentelemetry-instrumentation-kafka-python](./opentelemetry-instrumentation-kafka-python) | kafka-python >= 2.0, < 4.0 | No | development | [opentelemetry-instrumentation-logging](./opentelemetry-instrumentation-logging) | logging | No | development | [opentelemetry-instrumentation-mysql](./opentelemetry-instrumentation-mysql) | mysql-connector-python >= 8.0 | No | migration | [opentelemetry-instrumentation-mysqlclient](./opentelemetry-instrumentation-mysqlclient) | mysqlclient < 3 | No | migration | [opentelemetry-instrumentation-pika](./opentelemetry-instrumentation-pika) | pika >= 0.12.0 | No | development | [opentelemetry-instrumentation-psycopg](./opentelemetry-instrumentation-psycopg) | psycopg >= 3.1.0 | No | migration | [opentelemetry-instrumentation-psycopg2](./opentelemetry-instrumentation-psycopg2) | psycopg2 >= 2.7.3.1,psycopg2-binary >= 2.7.3.1 | No | migration | [opentelemetry-instrumentation-pymemcache](./opentelemetry-instrumentation-pymemcache) | pymemcache >= 1.3.5, < 5 | No | migration | [opentelemetry-instrumentation-pymongo](./opentelemetry-instrumentation-pymongo) | pymongo >= 3.1, < 5.0 | No | migration | [opentelemetry-instrumentation-pymssql](./opentelemetry-instrumentation-pymssql) | pymssql >= 2.1.5, < 3 | No | migration | [opentelemetry-instrumentation-pymysql](./opentelemetry-instrumentation-pymysql) | PyMySQL < 2 | No | migration | [opentelemetry-instrumentation-pyramid](./opentelemetry-instrumentation-pyramid) | pyramid >= 1.7 | Yes | migration | [opentelemetry-instrumentation-redis](./opentelemetry-instrumentation-redis) | redis >= 2.6 | No | migration | [opentelemetry-instrumentation-remoulade](./opentelemetry-instrumentation-remoulade) | remoulade >= 0.50 | No | development | [opentelemetry-instrumentation-requests](./opentelemetry-instrumentation-requests) | requests ~= 2.0 | Yes | migration | [opentelemetry-instrumentation-sqlalchemy](./opentelemetry-instrumentation-sqlalchemy) | sqlalchemy >= 1.0.0, < 2.1.0 | Yes | migration | [opentelemetry-instrumentation-sqlite3](./opentelemetry-instrumentation-sqlite3) | sqlite3 | No | migration | [opentelemetry-instrumentation-starlette](./opentelemetry-instrumentation-starlette) | starlette >= 0.13 | Yes | development | [opentelemetry-instrumentation-structlog](./opentelemetry-instrumentation-structlog) | structlog >= 21.1 | No | development | [opentelemetry-instrumentation-system-metrics](./opentelemetry-instrumentation-system-metrics) | psutil >= 5 | No | development | [opentelemetry-instrumentation-threading](./opentelemetry-instrumentation-threading) | threading | No | development | [opentelemetry-instrumentation-tornado](./opentelemetry-instrumentation-tornado) | tornado >= 5.1.1 | Yes | migration | [opentelemetry-instrumentation-tortoiseorm](./opentelemetry-instrumentation-tortoiseorm) | tortoise-orm >= 0.17.0 | No | migration | [opentelemetry-instrumentation-urllib](./opentelemetry-instrumentation-urllib) | urllib | Yes | migration | [opentelemetry-instrumentation-urllib3](./opentelemetry-instrumentation-urllib3) | urllib3 >= 1.0.0, < 3.0.0 | Yes | migration | [opentelemetry-instrumentation-valkey-py](./opentelemetry-instrumentation-valkey-py) | valkey ~= 6.0 | Yes | development | [opentelemetry-instrumentation-wsgi](./opentelemetry-instrumentation-wsgi) | wsgi | Yes | migrationopentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/000077500000000000000000000000001527070072300326355ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/LICENSE000066400000000000000000000261351527070072300336510ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000013531527070072300342470ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pikaOpenTelemetry Aio-pika Instrumentation ====================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-aio-pika.svg :target: https://pypi.org/project/opentelemetry-instrumentation-aio-pika/ This library allows tracing requests made by the Aio-pika library. Installation ------------ :: pip install opentelemetry-instrumentation-aio-pika References ---------- * `OpenTelemetry Aio-pika instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000032101527070072300354660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-aio-pika" dynamic = ["version"] description = "OpenTelemetry Aio-pika instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.5", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [ "aio_pika >= 7.2.0, < 11.0.0", ] [project.entry-points.opentelemetry_instrumentor] aio-pika = "opentelemetry.instrumentation.aio_pika:AioPikaInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-aio-pika" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/aio_pika/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/src/000077500000000000000000000000001527070072300334245ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300362415ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/srcinstrumentation/000077500000000000000000000000001527070072300415045ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetryaio_pika/000077500000000000000000000000001527070072300432605ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation__init__.py000066400000000000000000000020141527070072300453660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Instrument aio_pika to trace RabbitMQ applications. Usage ----- Start broker backend .. code-block:: python docker run -p 5672:5672 rabbitmq Run instrumented task .. code-block:: python import asyncio from aio_pika import Message, connect from opentelemetry.instrumentation.aio_pika import AioPikaInstrumentor AioPikaInstrumentor().instrument() async def main() -> None: connection = await connect("amqp://guest:guest@localhost/") async with connection: channel = await connection.channel() queue = await channel.declare_queue("hello") await channel.default_exchange.publish(Message(b"Hello World!"), routing_key=queue.name) if __name__ == "__main__": asyncio.run(main()) API --- """ # pylint: disable=import-error from .aio_pika_instrumentor import AioPikaInstrumentor from .version import __version__ __all__ = ["AioPikaInstrumentor", "__version__"] aio_pika_instrumentor.py000066400000000000000000000050151527070072300502400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable, Collection from typing import Any import wrapt from aio_pika import Exchange, Queue from aio_pika.abc import AbstractIncomingMessage from opentelemetry import trace from opentelemetry.instrumentation.aio_pika.callback_decorator import ( CallbackDecorator, ) from opentelemetry.instrumentation.aio_pika.package import _instruments from opentelemetry.instrumentation.aio_pika.publish_decorator import ( PublishDecorator, ) from opentelemetry.instrumentation.aio_pika.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap from opentelemetry.trace import Tracer _INSTRUMENTATION_MODULE_NAME = "opentelemetry.instrumentation.aio_pika" class AioPikaInstrumentor(BaseInstrumentor): @staticmethod def _instrument_queue(tracer: Tracer): async def wrapper(wrapped, instance, args, kwargs): async def consume( callback: Callable[[AbstractIncomingMessage], Any], *fargs, **fkwargs, ): decorated_callback = CallbackDecorator(tracer, instance).decorate(callback) return await wrapped(decorated_callback, *fargs, **fkwargs) return await consume(*args, **kwargs) wrapt.wrap_function_wrapper(Queue, "consume", wrapper) @staticmethod def _instrument_exchange(tracer: Tracer): async def wrapper(wrapped, instance, args, kwargs): decorated_publish = PublishDecorator(tracer, instance).decorate(wrapped) return await decorated_publish(*args, **kwargs) wrapt.wrap_function_wrapper(Exchange, "publish", wrapper) def _instrument(self, **kwargs): tracer_provider = kwargs.get("tracer_provider", None) tracer = trace.get_tracer( _INSTRUMENTATION_MODULE_NAME, __version__, tracer_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) self._instrument_queue(tracer) self._instrument_exchange(tracer) @staticmethod def _uninstrument_queue(): unwrap(Queue, "consume") @staticmethod def _uninstrument_exchange(): unwrap(Exchange, "publish") def _uninstrument(self, **kwargs): self._uninstrument_queue() self._uninstrument_exchange() def instrumentation_dependencies(self) -> Collection[str]: return _instruments callback_decorator.py000066400000000000000000000035311527070072300474320ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable from typing import Any from aio_pika import Queue from aio_pika.abc import AbstractIncomingMessage from opentelemetry import context, propagate, trace from opentelemetry.instrumentation.aio_pika.span_builder import SpanBuilder from opentelemetry.instrumentation.utils import is_instrumentation_enabled from opentelemetry.semconv.trace import MessagingOperationValues from opentelemetry.trace import Span, Tracer class CallbackDecorator: def __init__(self, tracer: Tracer, queue: Queue): self._tracer = tracer self._queue = queue def _get_span(self, message: AbstractIncomingMessage) -> Span | None: builder = SpanBuilder(self._tracer) builder.set_as_consumer() builder.set_operation(MessagingOperationValues.RECEIVE) builder.set_destination(message.exchange or message.routing_key) builder.set_channel(self._queue.channel) builder.set_message(message) return builder.build() def decorate(self, callback: Callable[[AbstractIncomingMessage], Any]) -> Callable[[AbstractIncomingMessage], Any]: async def decorated(message: AbstractIncomingMessage): if not is_instrumentation_enabled(): return await callback(message) headers = message.headers or {} ctx = propagate.extract(headers) token = context.attach(ctx) span = self._get_span(message) if not span: return await callback(message) try: with trace.use_span(span, end_on_exit=True): return_value = await callback(message) finally: if token: context.detach(token) return return_value return decorated package.py000066400000000000000000000002651527070072300452300ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Collection _instruments: Collection[str] = ("aio_pika >= 7.2.0, < 11.0.0",) publish_decorator.py000066400000000000000000000027441527070072300473510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable import aiormq from aio_pika import Exchange from aio_pika.abc import AbstractMessage from opentelemetry import propagate, trace from opentelemetry.instrumentation.aio_pika.span_builder import SpanBuilder from opentelemetry.trace import Span, Tracer class PublishDecorator: def __init__(self, tracer: Tracer, exchange: Exchange): self._tracer = tracer self._exchange = exchange def _get_publish_span(self, message: AbstractMessage, routing_key: str) -> Span | None: builder = SpanBuilder(self._tracer) builder.set_as_producer() builder.set_destination(f"{self._exchange.name},{routing_key}") builder.set_channel(self._exchange.channel) builder.set_message(message) return builder.build() def decorate(self, publish: Callable) -> Callable: async def decorated_publish( message: AbstractMessage, routing_key: str, **kwargs ) -> aiormq.abc.ConfirmationFrameType | None: span = self._get_publish_span(message, routing_key) if not span: return await publish(message, routing_key, **kwargs) with trace.use_span(span, end_on_exit=True): propagate.inject(message.properties.headers) return_value = await publish(message, routing_key, **kwargs) return return_value return decorated_publish span_builder.py000066400000000000000000000066411527070072300463100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from aio_pika.abc import AbstractChannel, AbstractMessage from opentelemetry.instrumentation.utils import is_instrumentation_enabled from opentelemetry.semconv._incubating.attributes.messaging_attributes import ( MESSAGING_MESSAGE_ID, MESSAGING_OPERATION, MESSAGING_SYSTEM, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv.trace import ( MessagingOperationValues, SpanAttributes, ) from opentelemetry.trace import Span, SpanKind, Tracer _DEFAULT_ATTRIBUTES = {MESSAGING_SYSTEM: "rabbitmq"} class SpanBuilder: def __init__(self, tracer: Tracer): self._tracer = tracer self._attributes = _DEFAULT_ATTRIBUTES.copy() self._operation: MessagingOperationValues = None self._kind: SpanKind = None self._destination: str = None def set_as_producer(self): self._kind = SpanKind.PRODUCER def set_as_consumer(self): self._kind = SpanKind.CONSUMER def set_operation(self, operation: MessagingOperationValues): self._operation = operation def set_destination(self, destination: str): self._destination = destination # TODO: Update this implementation once the semantic conventions for messaging stabilize # See: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/registry/attributes/messaging.md self._attributes[SpanAttributes.MESSAGING_DESTINATION] = destination def set_channel(self, channel: AbstractChannel): if hasattr(channel, "_connection"): # aio_rmq 9.1 and above removed the connection attribute from the abstract listings connection = channel._connection else: # aio_rmq 9.0.5 and below connection = channel.connection if hasattr(connection, "connection"): # aio_rmq 7 url = connection.connection.url else: # aio_rmq 8 url = connection.url self._attributes.update( { NET_PEER_NAME: url.host, NET_PEER_PORT: url.port or 5672, } ) def set_message(self, message: AbstractMessage): properties = message.properties if properties.message_id: self._attributes[MESSAGING_MESSAGE_ID] = properties.message_id if properties.correlation_id: self._attributes[SpanAttributes.MESSAGING_CONVERSATION_ID] = properties.correlation_id def build(self) -> Span | None: if not is_instrumentation_enabled(): return None if self._operation: self._attributes[MESSAGING_OPERATION] = self._operation.value else: # TODO: Update this implementation once the semantic conventions for messaging stabilize # See: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/registry/attributes/messaging.md self._attributes[SpanAttributes.MESSAGING_TEMP_DESTINATION] = True span = self._tracer.start_span( self._generate_span_name(), kind=self._kind, attributes=self._attributes, ) return span def _generate_span_name(self) -> str: operation_value = self._operation.value if self._operation else "send" return f"{self._destination} {operation_value}" version.py000066400000000000000000000001441527070072300453160ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements-latest.txt000066400000000000000000000044411527070072300403140ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika# This file was autogenerated by uv via the following command: # uv pip compile --python 3.10 --universal --no-emit-package opentelemetry-api --no-emit-package opentelemetry-semantic-conventions -c dev-requirements.txt instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements.in -o instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-latest.txt -e instrumentation/opentelemetry-instrumentation-aio-pika # via -r instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements.in -e opentelemetry-instrumentation # via # -r instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements.in # opentelemetry-instrumentation-aio-pika aio-pika==9.6.2 ; python_full_version < '3.11' # via -r instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements.in aio-pika==10.0.1 ; python_full_version >= '3.11' # via -r instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements.in aiormq==6.9.4 ; python_full_version < '3.11' # via aio-pika aiormq==7.0.0 ; python_full_version >= '3.11' # via aio-pika colorama==0.4.6 ; sys_platform == 'win32' # via pytest exceptiongroup==1.3.1 ; python_full_version < '3.11' # via # aio-pika # pytest idna==3.19 # via yarl iniconfig==2.3.0 # via pytest multidict==6.7.1 # via yarl packaging==26.3 # via # opentelemetry-instrumentation # pytest pamqp==3.3.0 ; python_full_version < '3.11' # via aiormq pamqp==4.0.1 ; python_full_version >= '3.11' # via aiormq pluggy==1.6.0 # via pytest propcache==0.5.2 # via yarl pytest==7.4.4 # via # -c dev-requirements.txt # -r instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements.in tomli==2.4.1 ; python_full_version < '3.11' # via pytest typing-extensions==4.16.0 # via # exceptiongroup # multidict # opentelemetry-api # opentelemetry-semantic-conventions wrapt==2.4.0 # via # -r instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements.in # opentelemetry-instrumentation # opentelemetry-instrumentation-aio-pika yarl==1.24.5 # via # aio-pika # aiormq # The following packages were excluded from the output: # opentelemetry-api # opentelemetry-semantic-conventions test-requirements-oldest.txt000066400000000000000000000040031527070072300403040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika# This file was autogenerated by uv via the following command: # uv pip compile --python 3.10 --resolution lowest-direct --universal --no-emit-package opentelemetry-api --no-emit-package opentelemetry-semantic-conventions -c dev-requirements.txt instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements.in -o instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-oldest.txt -e instrumentation/opentelemetry-instrumentation-aio-pika # via -r instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements.in -e opentelemetry-instrumentation # via # -r instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements.in # opentelemetry-instrumentation-aio-pika aio-pika==7.2.0 # via -r instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements.in aiormq==6.2.3 # via aio-pika colorama==0.4.6 ; sys_platform == 'win32' # via pytest exceptiongroup==1.3.1 ; python_full_version < '3.11' # via pytest idna==3.19 # via yarl iniconfig==2.3.0 # via pytest multidict==6.8.0 # via yarl packaging==18.0 # via # opentelemetry-instrumentation # pytest pamqp==3.1.0 # via aiormq pluggy==1.6.0 # via pytest propcache==0.5.2 # via yarl pyparsing==3.3.2 # via packaging pytest==7.4.4 # via # -c dev-requirements.txt # -r instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements.in six==1.17.0 # via packaging tomli==2.4.1 ; python_full_version < '3.11' # via pytest typing-extensions==4.16.0 # via # exceptiongroup # multidict # opentelemetry-api # opentelemetry-semantic-conventions wrapt==1.16.0 # via # -r instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements.in # opentelemetry-instrumentation # opentelemetry-instrumentation-aio-pika yarl==1.24.5 # via # aio-pika # aiormq # The following packages were excluded from the output: # opentelemetry-api # opentelemetry-semantic-conventions test-requirements.in000066400000000000000000000002401527070072300366020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pikaaio-pika >= 7.2.0, < 11.0.0 wrapt >= 1.16.0, < 3.0.0 pytest >= 7.4.4 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-aio-pika opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/tests/000077500000000000000000000000001527070072300337775ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300360170ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/testsconsts.py000066400000000000000000000017371527070072300356130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from argparse import Namespace from aio_pika import __version__ as aiopika_version from yarl import URL AIOPIKA_VERSION_INFO = tuple(int(v) for v in aiopika_version.split(".")) MESSAGE_ID = "meesage_id" CORRELATION_ID = "correlation_id" MESSAGING_SYSTEM_VALUE = "rabbitmq" EXCHANGE_NAME = "exchange_name" QUEUE_NAME = "queue_name" ROUTING_KEY = "routing_key" SERVER_HOST = "localhost" SERVER_PORT = 1234 SERVER_USER = "guest" SERVER_PASS = "guest" SERVER_URL = URL(f"amqp://{SERVER_USER}:{SERVER_PASS}@{SERVER_HOST}:{SERVER_PORT}/") CONNECTION_7 = Namespace(connection=Namespace(url=SERVER_URL)) CONNECTION_8 = Namespace(url=SERVER_URL) CHANNEL_7 = Namespace(connection=CONNECTION_7, loop=None) CHANNEL_8 = Namespace(connection=CONNECTION_8, loop=None) MESSAGE = Namespace( properties=Namespace(message_id=MESSAGE_ID, correlation_id=CORRELATION_ID, headers={}), exchange=EXCHANGE_NAME, headers={}, ) test_aio_pika_instrumentation.py000066400000000000000000000013541527070072300424330ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase import wrapt from aio_pika import Exchange, Queue from opentelemetry.instrumentation.aio_pika import AioPikaInstrumentor class TestPika(TestCase): def test_instrument_api(self) -> None: instrumentation = AioPikaInstrumentor() instrumentation.instrument() self.assertTrue(isinstance(Queue.consume, wrapt.BoundFunctionWrapper)) self.assertTrue(isinstance(Exchange.publish, wrapt.BoundFunctionWrapper)) instrumentation.uninstrument() self.assertFalse(isinstance(Queue.consume, wrapt.BoundFunctionWrapper)) self.assertFalse(isinstance(Exchange.publish, wrapt.BoundFunctionWrapper)) test_callback_decorator.py000066400000000000000000000077471527070072300411460ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio from unittest import TestCase, mock, skipIf from aio_pika import Queue from opentelemetry.instrumentation.aio_pika.callback_decorator import ( CallbackDecorator, ) from opentelemetry.semconv._incubating.attributes.messaging_attributes import ( MESSAGING_MESSAGE_ID, MESSAGING_OPERATION, MESSAGING_SYSTEM, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import SpanKind, get_tracer from .consts import ( AIOPIKA_VERSION_INFO, CHANNEL_7, CHANNEL_8, CORRELATION_ID, EXCHANGE_NAME, MESSAGE, MESSAGE_ID, MESSAGING_SYSTEM_VALUE, QUEUE_NAME, SERVER_HOST, SERVER_PORT, ) @skipIf(AIOPIKA_VERSION_INFO >= (8, 0), "Only for aio_pika 7") class TestInstrumentedQueueAioRmq7(TestCase): EXPECTED_ATTRIBUTES = { MESSAGING_SYSTEM: MESSAGING_SYSTEM_VALUE, SpanAttributes.MESSAGING_DESTINATION: EXCHANGE_NAME, NET_PEER_NAME: SERVER_HOST, NET_PEER_PORT: SERVER_PORT, MESSAGING_MESSAGE_ID: MESSAGE_ID, SpanAttributes.MESSAGING_CONVERSATION_ID: CORRELATION_ID, MESSAGING_OPERATION: "receive", } def setUp(self): self.tracer = get_tracer(__name__) self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) def test_get_callback_span(self): queue = Queue(CHANNEL_7, QUEUE_NAME, False, False, False, None) tracer = mock.MagicMock() CallbackDecorator(tracer, queue)._get_span(MESSAGE) tracer.start_span.assert_called_once_with( f"{EXCHANGE_NAME} receive", kind=SpanKind.CONSUMER, attributes=self.EXPECTED_ATTRIBUTES, ) def test_decorate_callback(self): queue = Queue(CHANNEL_7, QUEUE_NAME, False, False, False, None) callback = mock.MagicMock(return_value=asyncio.sleep(0)) with mock.patch.object(CallbackDecorator, "_get_span") as mocked_get_callback_span: callback_decorator = CallbackDecorator(self.tracer, queue) decorated_callback = callback_decorator.decorate(callback) self.loop.run_until_complete(decorated_callback(MESSAGE)) mocked_get_callback_span.assert_called_once() callback.assert_called_once_with(MESSAGE) @skipIf(AIOPIKA_VERSION_INFO <= (8, 0), "Only for aio_pika 8") class TestInstrumentedQueueAioRmq8(TestCase): EXPECTED_ATTRIBUTES = { MESSAGING_SYSTEM: MESSAGING_SYSTEM_VALUE, SpanAttributes.MESSAGING_DESTINATION: EXCHANGE_NAME, NET_PEER_NAME: SERVER_HOST, NET_PEER_PORT: SERVER_PORT, MESSAGING_MESSAGE_ID: MESSAGE_ID, SpanAttributes.MESSAGING_CONVERSATION_ID: CORRELATION_ID, MESSAGING_OPERATION: "receive", } def setUp(self): self.tracer = get_tracer(__name__) self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) def test_get_callback_span(self): queue = Queue(CHANNEL_8, QUEUE_NAME, False, False, False, None) tracer = mock.MagicMock() CallbackDecorator(tracer, queue)._get_span(MESSAGE) tracer.start_span.assert_called_once_with( f"{EXCHANGE_NAME} receive", kind=SpanKind.CONSUMER, attributes=self.EXPECTED_ATTRIBUTES, ) def test_decorate_callback(self): queue = Queue(CHANNEL_8, QUEUE_NAME, False, False, False, None) callback = mock.MagicMock(return_value=asyncio.sleep(0)) with mock.patch.object(CallbackDecorator, "_get_span") as mocked_get_callback_span: callback_decorator = CallbackDecorator(self.tracer, queue) decorated_callback = callback_decorator.decorate(callback) self.loop.run_until_complete(decorated_callback(MESSAGE)) mocked_get_callback_span.assert_called_once() callback.assert_called_once_with(MESSAGE) test_publish_decorator.py000066400000000000000000000156541527070072300410540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio from unittest import TestCase, mock, skipIf from unittest.mock import MagicMock from aio_pika import Exchange, RobustExchange from opentelemetry.instrumentation.aio_pika.publish_decorator import ( PublishDecorator, ) from opentelemetry.semconv._incubating.attributes.messaging_attributes import ( MESSAGING_MESSAGE_ID, MESSAGING_SYSTEM, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import SpanKind, get_tracer from .consts import ( AIOPIKA_VERSION_INFO, CHANNEL_7, CHANNEL_8, CONNECTION_7, CONNECTION_8, CORRELATION_ID, EXCHANGE_NAME, MESSAGE, MESSAGE_ID, MESSAGING_SYSTEM_VALUE, ROUTING_KEY, SERVER_HOST, SERVER_PORT, ) @skipIf(AIOPIKA_VERSION_INFO >= (8, 0), "Only for aio_pika 7") class TestInstrumentedExchangeAioRmq7(TestCase): EXPECTED_ATTRIBUTES = { MESSAGING_SYSTEM: MESSAGING_SYSTEM_VALUE, SpanAttributes.MESSAGING_DESTINATION: f"{EXCHANGE_NAME},{ROUTING_KEY}", NET_PEER_NAME: SERVER_HOST, NET_PEER_PORT: SERVER_PORT, MESSAGING_MESSAGE_ID: MESSAGE_ID, SpanAttributes.MESSAGING_CONVERSATION_ID: CORRELATION_ID, SpanAttributes.MESSAGING_TEMP_DESTINATION: True, } def setUp(self): self.tracer = get_tracer(__name__) self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) def test_get_publish_span(self): exchange = Exchange(CONNECTION_7, CHANNEL_7, EXCHANGE_NAME) tracer = mock.MagicMock() PublishDecorator(tracer, exchange)._get_publish_span(MESSAGE, ROUTING_KEY) tracer.start_span.assert_called_once_with( f"{EXCHANGE_NAME},{ROUTING_KEY} send", kind=SpanKind.PRODUCER, attributes=self.EXPECTED_ATTRIBUTES, ) def _test_publish(self, exchange_type: type[Exchange]): exchange = exchange_type(CONNECTION_7, CHANNEL_7, EXCHANGE_NAME) with mock.patch.object(PublishDecorator, "_get_publish_span") as mock_get_publish_span: with mock.patch.object(Exchange, "publish") as mock_publish: decorated_publish = PublishDecorator(self.tracer, exchange).decorate(mock_publish) self.loop.run_until_complete(decorated_publish(MESSAGE, ROUTING_KEY)) mock_publish.assert_called_once() mock_get_publish_span.assert_called_once() def test_publish(self): self._test_publish(Exchange) def test_robust_publish(self): self._test_publish(RobustExchange) def _test_publish_works_with_not_recording_span(self, exchange_type): exchange = exchange_type(CONNECTION_7, CHANNEL_7, EXCHANGE_NAME) with mock.patch.object(PublishDecorator, "_get_publish_span") as mock_get_publish_span: mocked_not_recording_span = MagicMock() mocked_not_recording_span.is_recording.return_value = False mock_get_publish_span.return_value = mocked_not_recording_span with mock.patch.object(Exchange, "publish") as mock_publish: with mock.patch( "opentelemetry.instrumentation.aio_pika.publish_decorator.propagate.inject" ) as mock_inject: decorated_publish = PublishDecorator(self.tracer, exchange).decorate(mock_publish) self.loop.run_until_complete(decorated_publish(MESSAGE, ROUTING_KEY)) mock_publish.assert_called_once() mock_get_publish_span.assert_called_once() mock_inject.assert_called_once() def test_publish_works_with_not_recording_span(self): self._test_publish_works_with_not_recording_span(Exchange) def test_publish_works_with_not_recording_span_robust(self): self._test_publish_works_with_not_recording_span(RobustExchange) @skipIf(AIOPIKA_VERSION_INFO <= (8, 0), "Only for aio_pika 8") class TestInstrumentedExchangeAioRmq8(TestCase): EXPECTED_ATTRIBUTES = { MESSAGING_SYSTEM: MESSAGING_SYSTEM_VALUE, SpanAttributes.MESSAGING_DESTINATION: f"{EXCHANGE_NAME},{ROUTING_KEY}", NET_PEER_NAME: SERVER_HOST, NET_PEER_PORT: SERVER_PORT, MESSAGING_MESSAGE_ID: MESSAGE_ID, SpanAttributes.MESSAGING_CONVERSATION_ID: CORRELATION_ID, SpanAttributes.MESSAGING_TEMP_DESTINATION: True, } def setUp(self): self.tracer = get_tracer(__name__) self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) def test_get_publish_span(self): exchange = Exchange(CHANNEL_8, EXCHANGE_NAME) tracer = mock.MagicMock() PublishDecorator(tracer, exchange)._get_publish_span(MESSAGE, ROUTING_KEY) tracer.start_span.assert_called_once_with( f"{EXCHANGE_NAME},{ROUTING_KEY} send", kind=SpanKind.PRODUCER, attributes=self.EXPECTED_ATTRIBUTES, ) def _test_publish(self, exchange_type: type[Exchange]): exchange = exchange_type(CONNECTION_8, CHANNEL_8, EXCHANGE_NAME) with mock.patch.object(PublishDecorator, "_get_publish_span") as mock_get_publish_span: with mock.patch.object(Exchange, "publish") as mock_publish: decorated_publish = PublishDecorator(self.tracer, exchange).decorate(mock_publish) self.loop.run_until_complete(decorated_publish(MESSAGE, ROUTING_KEY)) mock_publish.assert_called_once() mock_get_publish_span.assert_called_once() def test_publish(self): self._test_publish(Exchange) def test_robust_publish(self): self._test_publish(RobustExchange) def _test_publish_works_with_not_recording_span(self, exchange_type): exchange = exchange_type(CONNECTION_7, CHANNEL_7, EXCHANGE_NAME) with mock.patch.object(PublishDecorator, "_get_publish_span") as mock_get_publish_span: mocked_not_recording_span = MagicMock() mocked_not_recording_span.is_recording.return_value = False mock_get_publish_span.return_value = mocked_not_recording_span with mock.patch.object(Exchange, "publish") as mock_publish: with mock.patch( "opentelemetry.instrumentation.aio_pika.publish_decorator.propagate.inject" ) as mock_inject: decorated_publish = PublishDecorator(self.tracer, exchange).decorate(mock_publish) self.loop.run_until_complete(decorated_publish(MESSAGE, ROUTING_KEY)) mock_publish.assert_called_once() mock_get_publish_span.assert_called_once() mock_inject.assert_called_once() def test_publish_works_with_not_recording_span(self): self._test_publish_works_with_not_recording_span(Exchange) def test_publish_works_with_not_recording_span_robust(self): self._test_publish_works_with_not_recording_span(RobustExchange) test_span_builder.py000066400000000000000000000007651527070072300400100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aio-pika/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from opentelemetry.instrumentation.aio_pika.span_builder import SpanBuilder from opentelemetry.trace import Span, get_tracer class TestBuilder(TestCase): def test_build(self): builder = SpanBuilder(get_tracer(__name__)) builder.set_as_consumer() builder.set_destination("destination") span = builder.build() self.assertTrue(isinstance(span, Span)) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-client/000077500000000000000000000000001527070072300340675ustar00rootroot00000000000000LICENSE000066400000000000000000000261351527070072300350240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-client Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000016561527070072300355070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-clientOpenTelemetry aiohttp client Integration ======================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-aiohttp-client.svg :target: https://pypi.org/project/opentelemetry-instrumentation-aiohttp-client/ This library allows tracing HTTP requests made by the `aiohttp client `_ library. Installation ------------ :: pip install opentelemetry-instrumentation-aiohttp-client References ---------- * `OpenTelemetry aiohttp client Instrumentation `_ * `aiohttp Tracing Reference `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000033151527070072300367260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-client[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-aiohttp-client" dynamic = ["version"] description = "OpenTelemetry aiohttp client instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-util-http == 0.66b2", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [ "aiohttp ~= 3.0", ] [project.entry-points.opentelemetry_instrumentor] aiohttp-client = "opentelemetry.instrumentation.aiohttp_client:AioHttpClientInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-aiohttp-client" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/aiohttp_client/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] src/000077500000000000000000000000001527070072300345775ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-clientopentelemetry/000077500000000000000000000000001527070072300374735ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-client/srcinstrumentation/000077500000000000000000000000001527070072300427365ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetryaiohttp_client/000077500000000000000000000000001527070072300457445ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation__init__.py000066400000000000000000000643461527070072300500720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The opentelemetry-instrumentation-aiohttp-client package allows tracing HTTP requests made by the aiohttp client library. Usage ----- Explicitly instrumenting a single client session: .. code:: python import asyncio import aiohttp from opentelemetry.instrumentation.aiohttp_client import create_trace_config import yarl def strip_query_params(url: yarl.URL) -> str: return str(url.with_query(None)) async def get(url): async with aiohttp.ClientSession(trace_configs=[create_trace_config( # Remove all query params from the URL attribute on the span. url_filter=strip_query_params, )]) as session: async with session.get(url) as response: await response.text() asyncio.run(get("https://example.com")) Instrumenting all client sessions: .. code:: python import asyncio import aiohttp from opentelemetry.instrumentation.aiohttp_client import ( AioHttpClientInstrumentor ) # Enable instrumentation AioHttpClientInstrumentor().instrument() # Create a session and make an HTTP get request async def get(url): async with aiohttp.ClientSession() as session: async with session.get(url) as response: await response.text() asyncio.run(get("https://example.com")) Configuration ------------- Request/Response hooks ********************** Utilize request/response hooks to execute custom logic to be performed before/after performing a request. .. code-block:: python def request_hook(span: Span, params: aiohttp.TraceRequestStartParams): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_request_hook", "some-value") def response_hook( span: Span, params: typing.Union[ aiohttp.TraceRequestEndParams, aiohttp.TraceRequestExceptionParams, ], ): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_response_hook", "some-value") AioHttpClientInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook) Exclude lists ************* To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_AIOHTTP_CLIENT_EXCLUDED_URLS`` (or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the URLs. For example, :: export OTEL_PYTHON_AIOHTTP_CLIENT_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. Capture HTTP request and response headers ***************************************** You can configure the agent to capture specified HTTP headers as span attributes, according to the `semantic conventions `_. Request headers *************** To capture HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST`` to a comma delimited list of HTTP header names. For example using the environment variable, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST="content-type,custom_request_header" will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. Request header names in aiohttp are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST="Accept.*,X-.*" Would match all request headers that start with ``Accept`` and ``X-``. To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST=".*" The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a single item list containing all the header values. For example: ``http.request.header.custom_request_header = ["", ""]`` Response headers **************** To capture HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE`` to a comma delimited list of HTTP header names. For example using the environment variable, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE="content-type,custom_response_header" will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. Response header names in aiohttp are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE="Content.*,X-.*" Would match all response headers that start with ``Content`` and ``X-``. To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE=".*" The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a list containing the header values. For example: ``http.response.header.custom_response_header = ["", ""]`` Sanitizing headers ****************** In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be matched in a case-insensitive manner. For example using the environment variable, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. Note: The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. API --- """ from __future__ import annotations import types import typing from collections.abc import Callable, Collection from timeit import default_timer from typing import ( TYPE_CHECKING, Any, TypedDict, cast, ) from urllib.parse import urlparse import aiohttp import wrapt import yarl from opentelemetry import context as context_api from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, _client_duration_attrs_new, _client_duration_attrs_old, _filter_semconv_duration_attrs, _get_schema_url, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _report_new, _report_old, _set_http_host_client, _set_http_method, _set_http_net_peer_name_client, _set_http_peer_port_client, _set_http_url, _set_status, _StabilityMode, ) from opentelemetry.instrumentation.aiohttp_client.package import _instruments from opentelemetry.instrumentation.aiohttp_client.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import ( is_http_instrumentation_enabled, unwrap, ) from opentelemetry.metrics import MeterProvider, get_meter from opentelemetry.propagate import inject from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.metrics import ( MetricInstruments, # type: ignore[reportDeprecated] ) from opentelemetry.semconv.metrics.http_metrics import ( HTTP_CLIENT_REQUEST_DURATION, ) from opentelemetry.trace import Span, SpanKind, TracerProvider, get_tracer from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, get_custom_header_attributes, get_custom_headers, get_excluded_urls, normalise_request_header_name, normalise_response_header_name, redact_url, sanitize_method, ) if TYPE_CHECKING: from typing_extensions import Unpack UrlFilterT = Callable[[yarl.URL], str] | None RequestHookT = Callable[[Span, aiohttp.TraceRequestStartParams], None] | None ResponseHookT = ( Callable[ [ Span, aiohttp.TraceRequestEndParams | aiohttp.TraceRequestExceptionParams, ], None, ] | None ) class ClientSessionInitKwargs(TypedDict, total=False): trace_configs: typing.Sequence[aiohttp.TraceConfig] class InstrumentKwargs(TypedDict, total=False): tracer_provider: trace.TracerProvider meter_provider: MeterProvider url_filter: UrlFilterT request_hook: RequestHookT response_hook: ResponseHookT trace_configs: typing.Sequence[aiohttp.TraceConfig] class UninstrumentKwargs(TypedDict, total=False): pass def _get_span_name(method: str) -> str: method = sanitize_method(method.strip()) if method == "_OTHER": method = "HTTP" return method def _set_http_status_code_attribute( span: Span, status_code: int, metric_attributes: dict[str, Any] | None = None, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ): status_code_str = str(status_code) try: status_code = int(status_code) except ValueError: status_code = -1 if metric_attributes is None: metric_attributes = {} # When we have durations we should set metrics only once # Also the decision to include status code on a histogram should # not be dependent on tracing decisions. _set_status( span, metric_attributes, status_code, status_code_str, server_span=False, sem_conv_opt_in_mode=sem_conv_opt_in_mode, ) # pylint: disable=too-many-locals # pylint: disable=too-many-statements def create_trace_config( url_filter: UrlFilterT = None, request_hook: RequestHookT = None, response_hook: ResponseHookT = None, tracer_provider: TracerProvider | None = None, meter_provider: MeterProvider | None = None, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, captured_request_headers: list[str] | None = None, captured_response_headers: list[str] | None = None, sensitive_headers: list[str] | None = None, ) -> aiohttp.TraceConfig: """Create an aiohttp-compatible trace configuration. One span is created for the entire HTTP request, including initial TCP/TLS setup if the connection doesn't exist. By default the span name is set to the HTTP request method. Example usage: .. code:: python import aiohttp from opentelemetry.instrumentation.aiohttp_client import create_trace_config async with aiohttp.ClientSession(trace_configs=[create_trace_config()]) as session: async with session.get(url) as response: await response.text() :param url_filter: A callback to process the requested URL prior to adding it as a span attribute. This can be useful to remove sensitive data such as API keys or user personal information. :param Callable request_hook: Optional callback that can modify span name and request params. :param Callable response_hook: Optional callback that can modify span name and response params. :param tracer_provider: optional TracerProvider from which to get a Tracer :param meter_provider: optional Meter provider to use :param captured_request_headers: List of HTTP request header regexes to capture as span attributes. Header names matching these patterns will be added as span attributes with the format ``http.request.header.``. :param captured_response_headers: List of HTTP response header regexes to capture as span attributes. Header names matching these patterns will be added as span attributes with the format ``http.response.header.``. :param sensitive_headers: List of HTTP header regexes whose values should be sanitized (redacted) when captured. Header values matching these patterns will be replaced with ``[REDACTED]``. :return: An object suitable for use with :py:class:`aiohttp.ClientSession`. :rtype: :py:class:`aiohttp.TraceConfig` """ # `aiohttp.TraceRequestStartParams` resolves to `aiohttp.tracing.TraceRequestStartParams` # which doesn't exist in the aiohttp intersphinx inventory. # Explicitly specify the type for the `request_hook` and `response_hook` param and rtype to work # around this issue. schema_url = _get_schema_url(sem_conv_opt_in_mode) tracer = get_tracer( __name__, __version__, tracer_provider, schema_url=schema_url, ) meter = get_meter( __name__, __version__, meter_provider, schema_url, ) duration_histogram_old = None if _report_old(sem_conv_opt_in_mode): duration_histogram_old = meter.create_histogram( name=MetricInstruments.HTTP_CLIENT_DURATION, # type: ignore[reportDeprecated] unit="ms", description="measures the duration of the outbound HTTP request", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, ) duration_histogram_new = None if _report_new(sem_conv_opt_in_mode): duration_histogram_new = meter.create_histogram( name=HTTP_CLIENT_REQUEST_DURATION, unit="s", description="Duration of HTTP client requests.", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) excluded_urls = get_excluded_urls("AIOHTTP_CLIENT") def _end_trace(trace_config_ctx: types.SimpleNamespace): elapsed_time = max(default_timer() - trace_config_ctx.start_time, 0) if trace_config_ctx.token: context_api.detach(trace_config_ctx.token) if trace_config_ctx.span: trace_config_ctx.span.end() if trace_config_ctx.duration_histogram_old is not None: duration_attrs_old = cast( dict[str, Any], _filter_semconv_duration_attrs( trace_config_ctx.metric_attributes, _client_duration_attrs_old, _client_duration_attrs_new, _StabilityMode.DEFAULT, ), ) trace_config_ctx.duration_histogram_old.record( max(round(elapsed_time * 1000), 0), attributes=duration_attrs_old, ) if trace_config_ctx.duration_histogram_new is not None: duration_attrs_new = cast( dict[str, Any], _filter_semconv_duration_attrs( trace_config_ctx.metric_attributes, _client_duration_attrs_old, _client_duration_attrs_new, _StabilityMode.HTTP, ), ) trace_config_ctx.duration_histogram_new.record(elapsed_time, attributes=duration_attrs_new) async def on_request_start( _session: aiohttp.ClientSession, trace_config_ctx: types.SimpleNamespace, params: aiohttp.TraceRequestStartParams, ): if not is_http_instrumentation_enabled() or trace_config_ctx.excluded_urls.url_disabled(str(params.url)): return trace_config_ctx.start_time = default_timer() method = params.method request_span_name = _get_span_name(method) request_url = ( redact_url(cast(Callable[[yarl.URL], str], trace_config_ctx.url_filter)(params.url)) if callable(trace_config_ctx.url_filter) else redact_url(str(params.url)) ) span_attributes: dict[str, Any] = {} _set_http_method( span_attributes, method, sanitize_method(method), sem_conv_opt_in_mode, ) _set_http_method( trace_config_ctx.metric_attributes, method, sanitize_method(method), sem_conv_opt_in_mode, ) _set_http_url(span_attributes, request_url, sem_conv_opt_in_mode) try: parsed_url = urlparse(request_url) if parsed_url.hostname: _set_http_host_client( trace_config_ctx.metric_attributes, parsed_url.hostname, sem_conv_opt_in_mode, ) _set_http_net_peer_name_client( trace_config_ctx.metric_attributes, parsed_url.hostname, sem_conv_opt_in_mode, ) if _report_new(sem_conv_opt_in_mode): _set_http_host_client( span_attributes, parsed_url.hostname, sem_conv_opt_in_mode, ) if parsed_url.port: _set_http_peer_port_client( trace_config_ctx.metric_attributes, parsed_url.port, sem_conv_opt_in_mode, ) if _report_new(sem_conv_opt_in_mode): _set_http_peer_port_client(span_attributes, parsed_url.port, sem_conv_opt_in_mode) except ValueError: pass span_attributes.update( get_custom_header_attributes( {key: params.headers.getall(key) for key in params.headers.keys()}, captured_request_headers, sensitive_headers, normalise_request_header_name, ) ) trace_config_ctx.span = trace_config_ctx.tracer.start_span( request_span_name, kind=SpanKind.CLIENT, attributes=span_attributes ) if callable(request_hook): request_hook(trace_config_ctx.span, params) trace_config_ctx.token = context_api.attach(trace.set_span_in_context(trace_config_ctx.span)) inject(params.headers) async def on_request_end( _session: aiohttp.ClientSession, trace_config_ctx: types.SimpleNamespace, params: aiohttp.TraceRequestEndParams, ): if trace_config_ctx.span is None: return if callable(response_hook): response_hook(trace_config_ctx.span, params) _set_http_status_code_attribute( trace_config_ctx.span, params.response.status, trace_config_ctx.metric_attributes, sem_conv_opt_in_mode, ) trace_config_ctx.span.set_attributes( get_custom_header_attributes( {key: params.response.headers.getall(key) for key in params.response.headers.keys()}, captured_response_headers, sensitive_headers, normalise_response_header_name, ) ) _end_trace(trace_config_ctx) async def on_request_exception( _session: aiohttp.ClientSession, trace_config_ctx: types.SimpleNamespace, params: aiohttp.TraceRequestExceptionParams, ): if trace_config_ctx.span is None: return if trace_config_ctx.span.is_recording() and params.exception: exc_type = type(params.exception).__qualname__ if _report_new(sem_conv_opt_in_mode): trace_config_ctx.span.set_attribute(ERROR_TYPE, exc_type) trace_config_ctx.metric_attributes[ERROR_TYPE] = exc_type trace_config_ctx.span.set_status(Status(StatusCode.ERROR, exc_type)) trace_config_ctx.span.record_exception(params.exception) if callable(response_hook): response_hook(trace_config_ctx.span, params) _end_trace(trace_config_ctx) def _trace_config_ctx_factory(**kwargs: Any) -> types.SimpleNamespace: kwargs.setdefault("trace_request_ctx", {}) return types.SimpleNamespace( tracer=tracer, span=None, token=None, duration_histogram_old=duration_histogram_old, duration_histogram_new=duration_histogram_new, metric_attributes={}, url_filter=url_filter, excluded_urls=excluded_urls, start_time=0, **kwargs, ) trace_config = aiohttp.TraceConfig( trace_config_ctx_factory=cast(type[types.SimpleNamespace], _trace_config_ctx_factory) ) trace_config.on_request_start.append(on_request_start) trace_config.on_request_end.append(on_request_end) trace_config.on_request_exception.append(on_request_exception) return trace_config def _instrument( tracer_provider: TracerProvider | None = None, meter_provider: MeterProvider | None = None, url_filter: UrlFilterT = None, request_hook: RequestHookT = None, response_hook: ResponseHookT = None, trace_configs: typing.Sequence[aiohttp.TraceConfig] | None = None, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, captured_request_headers: list[str] | None = None, captured_response_headers: list[str] | None = None, sensitive_headers: list[str] | None = None, ): """Enables tracing of all ClientSessions When a ClientSession gets created a TraceConfig is automatically added to the session's trace_configs. """ trace_configs = trace_configs or () # pylint:disable=unused-argument def instrumented_init( wrapped: Callable[..., None], _instance: aiohttp.ClientSession, args: tuple[Any, ...], kwargs: ClientSessionInitKwargs, ): client_trace_configs = list(kwargs.get("trace_configs") or []) client_trace_configs.extend(trace_configs) trace_config = create_trace_config( url_filter=url_filter, request_hook=request_hook, response_hook=response_hook, tracer_provider=tracer_provider, meter_provider=meter_provider, sem_conv_opt_in_mode=sem_conv_opt_in_mode, captured_request_headers=captured_request_headers, captured_response_headers=captured_response_headers, sensitive_headers=sensitive_headers, ) setattr(trace_config, "_is_instrumented_by_opentelemetry", True) client_trace_configs.append(trace_config) kwargs["trace_configs"] = client_trace_configs return wrapped(*args, **kwargs) wrapt.wrap_function_wrapper( # type: ignore[reportUnknownVariableType] aiohttp.ClientSession, "__init__", instrumented_init ) def _uninstrument(): """Disables instrumenting for all newly created ClientSessions""" unwrap(aiohttp.ClientSession, "__init__") def _uninstrument_session(client_session: aiohttp.ClientSession): """Disables instrumentation for the given ClientSession""" # pylint: disable=protected-access trace_configs = client_session._trace_configs client_session._trace_configs = [ trace_config for trace_config in trace_configs if not hasattr(trace_config, "_is_instrumented_by_opentelemetry") ] class AioHttpClientInstrumentor(BaseInstrumentor): """An instrumentor for aiohttp client sessions See `BaseInstrumentor` """ def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs: Unpack[InstrumentKwargs]): """Instruments aiohttp ClientSession Args: **kwargs: Optional arguments ``tracer_provider``: a TracerProvider, defaults to global ``meter_provider``: a MeterProvider, defaults to global ``url_filter``: A callback to process the requested URL prior to adding it as a span attribute. This can be useful to remove sensitive data such as API keys or user personal information. ``request_hook``: An optional callback that is invoked right after a span is created. ``response_hook``: An optional callback which is invoked right before the span is finished processing a response. ``trace_configs``: An optional list of aiohttp.TraceConfig items, allowing customize enrichment of spans based on aiohttp events (see specification: https://docs.aiohttp.org/en/stable/tracing_reference.html) """ _OpenTelemetrySemanticConventionStability._initialize() _sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) _instrument( tracer_provider=kwargs.get("tracer_provider"), meter_provider=kwargs.get("meter_provider"), url_filter=kwargs.get("url_filter"), request_hook=kwargs.get("request_hook"), response_hook=kwargs.get("response_hook"), trace_configs=kwargs.get("trace_configs"), sem_conv_opt_in_mode=_sem_conv_opt_in_mode, captured_request_headers=get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST), captured_response_headers=get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE), sensitive_headers=get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS), ) def _uninstrument(self, **kwargs: Unpack[UninstrumentKwargs]): _uninstrument() @staticmethod def uninstrument_session(client_session: aiohttp.ClientSession): """Disables instrumentation for the given session""" _uninstrument_session(client_session) package.py000066400000000000000000000002521527070072300477100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("aiohttp ~= 3.0",) _supports_metrics = True _semconv_status = "migration" py.typed000066400000000000000000000000001527070072300474310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_clientversion.py000066400000000000000000000001441527070072300500020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000011621527070072300402510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-clientaiohttp==3.10.2 aiosignal==1.3.1 asgiref==3.8.1 async-timeout==4.0.3 blinker==1.7.0 certifi==2024.7.4 charset-normalizer==3.3.2 click==8.1.7 Deprecated==1.2.14 Flask==3.0.2 frozenlist==1.4.1 http_server_mock==1.7 idna==3.7 iniconfig==2.0.0 itsdangerous==2.1.2 Jinja2==3.1.6 MarkupSafe==2.1.5 multidict==6.0.5 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 requests==2.33.1 tomli==2.0.1 typing_extensions==4.12.2 urllib3==2.7.0 Werkzeug==3.0.6 wrapt==1.16.0 yarl==1.9.4 zipp==3.19.2 -e opentelemetry-instrumentation -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-aiohttp-client tests/000077500000000000000000000000001527070072300351525ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-client__init__.py000066400000000000000000000000001527070072300372510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-client/teststest_aiohttp_client_integration.py000066400000000000000000001556241527070072300442110ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines import asyncio import contextlib import os import typing import unittest import urllib.parse from http import HTTPStatus from unittest import mock import aiohttp import aiohttp.test_utils import yarl from http_server_mock import HttpServerMock from opentelemetry import trace as trace_api from opentelemetry.instrumentation import aiohttp_client from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, _StabilityMode, ) from opentelemetry.instrumentation.aiohttp_client import ( AioHttpClientInstrumentor, ) from opentelemetry.instrumentation.utils import ( suppress_http_instrumentation, suppress_instrumentation, ) from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_HOST, HTTP_METHOD, HTTP_STATUS_CODE, HTTP_URL, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv._incubating.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_REQUEST_METHOD_ORIGINAL, HTTP_RESPONSE_STATUS_CODE, ) from opentelemetry.semconv.attributes.url_attributes import URL_FULL from opentelemetry.test.test_base import TestBase from opentelemetry.trace import Span, StatusCode from opentelemetry.util._importlib_metadata import entry_points def run_with_test_server(runnable: typing.Callable, url: str, handler: typing.Callable) -> tuple[str, int]: async def do_request(): app = aiohttp.web.Application() parsed_url = urllib.parse.urlparse(url) app.add_routes([aiohttp.web.get(parsed_url.path, handler)]) app.add_routes([aiohttp.web.post(parsed_url.path, handler)]) app.add_routes([aiohttp.web.patch(parsed_url.path, handler)]) with contextlib.suppress(aiohttp.ClientError): async with aiohttp.test_utils.TestServer(app) as server: netloc = (server.host, server.port) await server.start_server() await runnable(server) return netloc return asyncio.run(do_request()) SCOPE = "opentelemetry.instrumentation.aiohttp_client" # pylint: disable=too-many-public-methods class TestAioHttpIntegration(TestBase): _test_status_codes = ( (HTTPStatus.OK, StatusCode.UNSET), (HTTPStatus.TEMPORARY_REDIRECT, StatusCode.UNSET), (HTTPStatus.NOT_FOUND, StatusCode.ERROR), (HTTPStatus.BAD_REQUEST, StatusCode.ERROR), (HTTPStatus.SERVICE_UNAVAILABLE, StatusCode.ERROR), (HTTPStatus.GATEWAY_TIMEOUT, StatusCode.ERROR), ) def setUp(self): super().setUp() _OpenTelemetrySemanticConventionStability._initialized = False def _assert_spans(self, spans, num_spans=1): finished_spans = self.memory_exporter.get_finished_spans() self.assertEqual(num_spans, len(finished_spans)) self.assertEqual( [ ( span.name, (span.status.status_code, span.status.description), dict(span.attributes), ) for span in finished_spans ], spans, ) def _assert_metrics(self, num_metrics: int = 1): metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), num_metrics) return metrics def _assert_single_span(self): """Assert that exactly one span was created and return it.""" spans = self.memory_exporter.get_finished_spans() self.assertEqual(1, len(spans)) return spans[0] @staticmethod def _http_request( trace_config, url: str, method: str = "GET", status_code: int = HTTPStatus.OK, request_handler: typing.Callable = None, **kwargs, ) -> tuple[str, int]: """Helper to start an aiohttp test server and send an actual HTTP request to it.""" async def default_handler(request): assert "traceparent" in request.headers return aiohttp.web.Response(status=int(status_code)) async def client_request(server: aiohttp.test_utils.TestServer): async with aiohttp.test_utils.TestClient(server, trace_configs=[trace_config]) as client: await client.request(method, url, trace_request_ctx={}, **kwargs) handler = request_handler or default_handler return run_with_test_server(client_request, url, handler) def test_status_codes(self): index = 0 for status_code, span_status in self._test_status_codes: with self.subTest(status_code=status_code): path = "test-path?query=param#foobar" host, port = self._http_request( trace_config=aiohttp_client.create_trace_config(), url=f"/{path}", status_code=status_code, ) url = f"http://{host}:{port}/{path}" attributes = { HTTP_METHOD: "GET", HTTP_URL: url, HTTP_STATUS_CODE: status_code, } spans = [("GET", (span_status, None), attributes)] self._assert_spans(spans) self.memory_exporter.clear() metrics = self._assert_metrics(1) duration_data_point = metrics[0].data.data_points[index] self.assertEqual( dict(duration_data_point.attributes), { HTTP_STATUS_CODE: status_code, HTTP_METHOD: "GET", HTTP_HOST: host, NET_PEER_NAME: host, NET_PEER_PORT: port, }, ) self.assertEqual( duration_data_point.explicit_bounds, HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, ) index += 1 def test_status_codes_new_semconv(self): index = 0 for status_code, span_status in self._test_status_codes: with self.subTest(status_code=status_code): path = "test-path?query=param#foobar" host, port = self._http_request( trace_config=aiohttp_client.create_trace_config(sem_conv_opt_in_mode=_StabilityMode.HTTP), url=f"/{path}", status_code=status_code, ) url = f"http://{host}:{port}/{path}" attributes = { HTTP_REQUEST_METHOD: "GET", URL_FULL: url, HTTP_RESPONSE_STATUS_CODE: status_code, SERVER_ADDRESS: host, SERVER_PORT: port, } if status_code >= 400: attributes[ERROR_TYPE] = str(status_code.value) spans = [("GET", (span_status, None), attributes)] self._assert_spans(spans) self.memory_exporter.clear() metrics = self._assert_metrics(1) duration_data_point = metrics[0].data.data_points[index] self.assertEqual( duration_data_point.attributes.get(HTTP_RESPONSE_STATUS_CODE), status_code, ) self.assertEqual( duration_data_point.attributes.get(HTTP_REQUEST_METHOD), "GET", ) if status_code >= 400: self.assertEqual( duration_data_point.attributes.get(ERROR_TYPE), str(status_code.value), ) self.assertEqual( duration_data_point.explicit_bounds, HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) index += 1 def test_status_codes_both_semconv(self): index = 0 for status_code, span_status in self._test_status_codes: with self.subTest(status_code=status_code): path = "test-path?query=param#foobar" host, port = self._http_request( trace_config=aiohttp_client.create_trace_config(sem_conv_opt_in_mode=_StabilityMode.HTTP_DUP), url=f"/{path}", status_code=status_code, ) url = f"http://{host}:{port}/{path}" attributes = { HTTP_REQUEST_METHOD: "GET", HTTP_METHOD: "GET", HTTP_HOST: host, URL_FULL: url, HTTP_URL: url, HTTP_RESPONSE_STATUS_CODE: status_code, HTTP_STATUS_CODE: status_code, SERVER_ADDRESS: host, SERVER_PORT: port, NET_PEER_PORT: port, } if status_code >= 400: attributes[ERROR_TYPE] = str(status_code.value) spans = [("GET", (span_status, None), attributes)] self._assert_spans(spans, 1) self.memory_exporter.clear() metrics = self._assert_metrics(2) duration_data_point = metrics[0].data.data_points[index] self.assertEqual( duration_data_point.attributes.get(HTTP_STATUS_CODE), status_code, ) self.assertEqual( duration_data_point.attributes.get(HTTP_METHOD), "GET", ) self.assertEqual( duration_data_point.attributes.get(ERROR_TYPE), None, ) duration_data_point = metrics[1].data.data_points[index] self.assertEqual( duration_data_point.attributes.get(HTTP_RESPONSE_STATUS_CODE), status_code, ) self.assertEqual( duration_data_point.attributes.get(HTTP_REQUEST_METHOD), "GET", ) if status_code >= 400: self.assertEqual( duration_data_point.attributes.get(ERROR_TYPE), str(status_code.value), ) index += 1 def test_metrics(self): with self.subTest(status_code=200): host, port = self._http_request( trace_config=aiohttp_client.create_trace_config(), url="/test-path?query=param#foobar", status_code=200, ) metrics = self._assert_metrics(1) self.assertEqual(len(metrics[0].data.data_points), 1) duration_data_point = metrics[0].data.data_points[0] self.assertEqual( dict(metrics[0].data.data_points[0].attributes), { HTTP_STATUS_CODE: 200, HTTP_METHOD: "GET", HTTP_HOST: host, NET_PEER_NAME: host, NET_PEER_PORT: port, }, ) self.assertEqual(duration_data_point.count, 1) self.assertTrue(duration_data_point.min > 0) self.assertTrue(duration_data_point.max > 0) self.assertTrue(duration_data_point.sum > 0) def test_schema_url(self): with self.subTest(status_code=200): self._http_request( trace_config=aiohttp_client.create_trace_config(), url="/test-path?query=param#foobar", status_code=200, ) span = self.memory_exporter.get_finished_spans()[0] self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.11.0", ) self.memory_exporter.clear() def test_schema_url_new_semconv(self): with self.subTest(status_code=200): self._http_request( trace_config=aiohttp_client.create_trace_config(sem_conv_opt_in_mode=_StabilityMode.HTTP), url="/test-path?query=param#foobar", status_code=200, ) span = self.memory_exporter.get_finished_spans()[0] self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.21.0", ) self.memory_exporter.clear() def test_schema_url_both_semconv(self): with self.subTest(status_code=200): self._http_request( trace_config=aiohttp_client.create_trace_config(sem_conv_opt_in_mode=_StabilityMode.HTTP_DUP), url="/test-path?query=param#foobar", status_code=200, ) span = self.memory_exporter.get_finished_spans()[0] self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.21.0", ) self.memory_exporter.clear() def test_not_recording(self): mock_tracer = mock.Mock() mock_span = mock.Mock() mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span with mock.patch("opentelemetry.trace.get_tracer"): # pylint: disable=W0612 self._http_request( trace_config=aiohttp_client.create_trace_config(), url="/test-path?query=param#foobar", ) self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) def test_hooks(self): method = "PATCH" path = "/some/path" expected = "PATCH - /some/path" def request_hook(span: Span, params: aiohttp.TraceRequestStartParams): span.update_name(f"{params.method} - {params.url.path}") def response_hook( span: Span, params: aiohttp.TraceRequestEndParams | aiohttp.TraceRequestExceptionParams, ): span.set_attribute("response_hook_attr", "value") host, port = self._http_request( trace_config=aiohttp_client.create_trace_config( request_hook=request_hook, response_hook=response_hook, ), method=method, url=path, status_code=HTTPStatus.OK, ) for span in self.memory_exporter.get_finished_spans(): self.assertEqual(span.name, expected) self.assertEqual( (span.status.status_code, span.status.description), (StatusCode.UNSET, None), ) self.assertEqual(span.attributes[HTTP_METHOD], method) self.assertEqual( span.attributes[HTTP_URL], f"http://{host}:{port}{path}", ) self.assertEqual(span.attributes[HTTP_STATUS_CODE], HTTPStatus.OK) self.assertIn("response_hook_attr", span.attributes) self.assertEqual(span.attributes["response_hook_attr"], "value") self.memory_exporter.clear() def test_url_filter_option(self): # Strips all query params from URL before adding as a span attribute. def strip_query_params(url: yarl.URL) -> str: return str(url.with_query(None)) host, port = self._http_request( trace_config=aiohttp_client.create_trace_config(url_filter=strip_query_params), url="/some/path?query=param&other=param2", status_code=HTTPStatus.OK, ) self._assert_spans( [ ( "GET", (StatusCode.UNSET, None), { HTTP_METHOD: "GET", HTTP_URL: f"http://{host}:{port}/some/path", HTTP_STATUS_CODE: int(HTTPStatus.OK), }, ) ] ) def test_connection_errors(self): trace_configs = [aiohttp_client.create_trace_config()] for url, expected_status in ( ("http://this-is-unknown.local/", StatusCode.ERROR), ("http://127.0.0.1:1/", StatusCode.ERROR), ): with self.subTest(expected_status=expected_status): async def do_request(url): async with aiohttp.ClientSession( trace_configs=trace_configs, ) as session: async with session.get(url): pass with self.assertRaises(aiohttp.ClientConnectorError): asyncio.run(do_request(url)) self._assert_spans( [ ( "GET", (expected_status, "ClientConnectorError"), { HTTP_METHOD: "GET", HTTP_URL: url, }, ) ] ) self.memory_exporter.clear() def test_basic_exception(self): async def request_handler(request): assert "traceparent" in request.headers host, port = self._http_request( trace_config=aiohttp_client.create_trace_config(), url="/test", request_handler=request_handler, ) span = self.memory_exporter.get_finished_spans()[0] self.assertEqual(len(span.events), 1) self.assertEqual(span.events[0].name, "exception") self._assert_spans( [ ( "GET", (StatusCode.ERROR, "ServerDisconnectedError"), { HTTP_METHOD: "GET", HTTP_URL: f"http://{host}:{port}/test", }, ) ] ) metrics = self._assert_metrics(1) duration_data_point = metrics[0].data.data_points[0] self.assertEqual( dict(duration_data_point.attributes), { HTTP_METHOD: "GET", HTTP_HOST: host, NET_PEER_NAME: host, NET_PEER_PORT: port, }, ) def test_basic_exception_new_semconv(self): async def request_handler(request): assert "traceparent" in request.headers host, port = self._http_request( trace_config=aiohttp_client.create_trace_config(sem_conv_opt_in_mode=_StabilityMode.HTTP), url="/test", request_handler=request_handler, ) span = self.memory_exporter.get_finished_spans()[0] self.assertEqual(len(span.events), 1) self.assertEqual(span.events[0].name, "exception") self._assert_spans( [ ( "GET", (StatusCode.ERROR, "ServerDisconnectedError"), { HTTP_REQUEST_METHOD: "GET", URL_FULL: f"http://{host}:{port}/test", ERROR_TYPE: "ServerDisconnectedError", SERVER_ADDRESS: host, SERVER_PORT: port, }, ) ] ) metrics = self._assert_metrics(1) duration_data_point = metrics[0].data.data_points[0] self.assertEqual( dict(duration_data_point.attributes), { HTTP_REQUEST_METHOD: "GET", ERROR_TYPE: "ServerDisconnectedError", SERVER_ADDRESS: host, SERVER_PORT: port, }, ) def test_basic_exception_both_semconv(self): async def request_handler(request): assert "traceparent" in request.headers host, port = self._http_request( trace_config=aiohttp_client.create_trace_config(sem_conv_opt_in_mode=_StabilityMode.HTTP_DUP), url="/test", request_handler=request_handler, ) span = self.memory_exporter.get_finished_spans()[0] self.assertEqual(len(span.events), 1) self.assertEqual(span.events[0].name, "exception") self._assert_spans( [ ( "GET", (StatusCode.ERROR, "ServerDisconnectedError"), { HTTP_REQUEST_METHOD: "GET", URL_FULL: f"http://{host}:{port}/test", ERROR_TYPE: "ServerDisconnectedError", HTTP_METHOD: "GET", HTTP_URL: f"http://{host}:{port}/test", HTTP_HOST: host, SERVER_ADDRESS: host, SERVER_PORT: port, NET_PEER_PORT: port, }, ) ] ) metrics = self._assert_metrics(2) duration_data_point = metrics[0].data.data_points[0] self.assertEqual( dict(duration_data_point.attributes), { HTTP_METHOD: "GET", HTTP_HOST: host, NET_PEER_NAME: host, NET_PEER_PORT: port, }, ) duration_data_point = metrics[1].data.data_points[0] self.assertEqual( dict(duration_data_point.attributes), { HTTP_REQUEST_METHOD: "GET", ERROR_TYPE: "ServerDisconnectedError", SERVER_ADDRESS: host, SERVER_PORT: port, }, ) def test_timeout(self): async def request_handler(request): await asyncio.sleep(1) assert "traceparent" in request.headers return aiohttp.web.Response() host, port = self._http_request( trace_config=aiohttp_client.create_trace_config(), url="/test_timeout", request_handler=request_handler, timeout=aiohttp.ClientTimeout(sock_read=0.01), ) self._assert_spans( [ ( "GET", (StatusCode.ERROR, "SocketTimeoutError"), { HTTP_METHOD: "GET", HTTP_URL: f"http://{host}:{port}/test_timeout", }, ) ] ) def test_too_many_redirects(self): async def request_handler(request): # Create a redirect loop. location = request.url assert "traceparent" in request.headers raise aiohttp.web.HTTPFound(location=location) host, port = self._http_request( trace_config=aiohttp_client.create_trace_config(), url="/test_too_many_redirects", request_handler=request_handler, max_redirects=2, ) self._assert_spans( [ ( "GET", (StatusCode.ERROR, "TooManyRedirects"), { HTTP_METHOD: "GET", HTTP_URL: f"http://{host}:{port}/test_too_many_redirects", }, ) ] ) def test_nonstandard_http_method(self): trace_configs = [aiohttp_client.create_trace_config()] app = HttpServerMock("nonstandard_method") @app.route("/status/200", methods=["NONSTANDARD"]) def index(): return ("", 405, {}) url = "http://localhost:5000/status/200" with app.run("localhost", 5000): with self.subTest(url=url): async def do_request(url): async with aiohttp.ClientSession( trace_configs=trace_configs, ) as session: async with session.request("NONSTANDARD", url): pass asyncio.run(do_request(url)) self._assert_spans( [ ( "HTTP", (StatusCode.ERROR, None), { HTTP_METHOD: "_OTHER", HTTP_URL: url, HTTP_STATUS_CODE: int(HTTPStatus.METHOD_NOT_ALLOWED), }, ) ] ) self.memory_exporter.clear() def test_nonstandard_http_method_new_semconv(self): trace_configs = [aiohttp_client.create_trace_config(sem_conv_opt_in_mode=_StabilityMode.HTTP)] app = HttpServerMock("nonstandard_method") @app.route("/status/200", methods=["NONSTANDARD"]) def index(): return ("", 405, {}) url = "http://localhost:5000/status/200" with app.run("localhost", 5000): with self.subTest(url=url): async def do_request(url): async with aiohttp.ClientSession( trace_configs=trace_configs, ) as session: async with session.request("NONSTANDARD", url): pass asyncio.run(do_request(url)) self._assert_spans( [ ( "HTTP", (StatusCode.ERROR, None), { HTTP_REQUEST_METHOD: "_OTHER", URL_FULL: url, HTTP_RESPONSE_STATUS_CODE: int(HTTPStatus.METHOD_NOT_ALLOWED), HTTP_REQUEST_METHOD_ORIGINAL: "NONSTANDARD", ERROR_TYPE: "405", SERVER_ADDRESS: "localhost", SERVER_PORT: 5000, }, ) ] ) self.memory_exporter.clear() def test_remove_sensitive_params(self): trace_configs = [aiohttp_client.create_trace_config()] app = HttpServerMock("test_remove_sensitive_params") @app.route("/status/200") def index(): return "hello" url = "http://username:password@localhost:5000/status/200?Signature=secret" with app.run("localhost", 5000): with self.subTest(url=url): async def do_request(url): async with aiohttp.ClientSession( trace_configs=trace_configs, ) as session: async with session.get(url): pass asyncio.run(do_request(url)) self._assert_spans( [ ( "GET", (StatusCode.UNSET, None), { HTTP_METHOD: "GET", HTTP_URL: ("http://REDACTED:REDACTED@localhost:5000/status/200?Signature=REDACTED"), HTTP_STATUS_CODE: int(HTTPStatus.OK), }, ) ] ) self.memory_exporter.clear() def test_ignores_excluded_urls(self): async def request_handler(request): assert "traceparent" not in request.headers return aiohttp.web.Response(status=HTTPStatus.OK) for env_var in ( "OTEL_PYTHON_AIOHTTP_CLIENT_EXCLUDED_URLS", "OTEL_PYTHON_EXCLUDED_URLS", ): with self.subTest(env_var=env_var): with mock.patch.dict(os.environ, {env_var: "/some/path"}, clear=True): self._http_request( trace_config=aiohttp_client.create_trace_config(), request_handler=request_handler, url="/some/path?query=param&other=param2", status_code=HTTPStatus.OK, ) self._assert_spans([], 0) self._assert_metrics(0) def test_metric_attributes_isolation(self): async def success_handler(request): assert "traceparent" in request.headers return aiohttp.web.Response(status=HTTPStatus.OK) async def timeout_handler(request): await asyncio.sleep(60) assert "traceparent" in request.headers return aiohttp.web.Response(status=HTTPStatus.OK) trace_config: aiohttp.TraceConfig = aiohttp_client.create_trace_config() success_host, success_port = self._http_request( trace_config=trace_config, url="/success", request_handler=success_handler, ) timeout_host, timeout_port = self._http_request( trace_config=trace_config, url="/timeout", request_handler=timeout_handler, timeout=aiohttp.ClientTimeout(sock_read=0.01), ) metrics = self._assert_metrics(1) duration_dp_attributes = [dict(dp.attributes) for dp in metrics[0].data.data_points] self.assertEqual( [ { HTTP_METHOD: "GET", HTTP_HOST: success_host, HTTP_STATUS_CODE: int(HTTPStatus.OK), NET_PEER_NAME: success_host, NET_PEER_PORT: success_port, }, { HTTP_METHOD: "GET", HTTP_HOST: timeout_host, NET_PEER_NAME: timeout_host, NET_PEER_PORT: timeout_port, }, ], duration_dp_attributes, ) def test_custom_request_headers_captured(self): """Test that specified request headers are captured as span attributes.""" trace_config = aiohttp_client.create_trace_config( captured_request_headers=["X-Custom-Header", "X-Another-Header"] ) self._http_request( trace_config=trace_config, url="/test-path", status_code=200, headers={ "X-Custom-Header": "custom-value", "X-Another-Header": "another-value", "X-Excluded-Header": "excluded-value", }, ) span = self._assert_single_span() self.assertEqual( span.attributes["http.request.header.x_custom_header"], ("custom-value",), ) self.assertEqual( span.attributes["http.request.header.x_another_header"], ("another-value",), ) self.assertNotIn("http.request.header.x_excluded_header", span.attributes) def test_custom_response_headers_captured(self): """Test that specified response headers are captured as span attributes.""" trace_config = aiohttp_client.create_trace_config( captured_response_headers=["X-Custom-Header", "X-Another-Header"] ) def custom_handler(_request): headers = { "X-Custom-Header": "custom-value", "X-Another-Header": "another-value", "X-Excluded-Header": "excluded-value", } return aiohttp.web.Response(status=200, text="OK", headers=headers) self._http_request( trace_config=trace_config, url="/test-path", status_code=200, request_handler=custom_handler, ) span = self._assert_single_span() self.assertEqual( span.attributes["http.response.header.x_custom_header"], ("custom-value",), ) self.assertEqual( span.attributes["http.response.header.x_another_header"], ("another-value",), ) self.assertNotIn("http.response.header.x_excluded_header", span.attributes) def test_custom_headers_not_captured_when_not_configured(self): """Test that headers are not captured when env vars are not set.""" trace_config = aiohttp_client.create_trace_config() def custom_handler(_request): return aiohttp.web.Response( status=200, text="OK", headers={"X-Response-Header": "response-value"}, ) self._http_request( trace_config=trace_config, url="/test-path", status_code=200, request_handler=custom_handler, headers={"X-Request-Header": "request-value"}, ) span = self._assert_single_span() self.assertNotIn("http.request.header.x_request_header", span.attributes) self.assertNotIn("http.response.header.x_response_header", span.attributes) def test_sensitive_headers_sanitized(self): """Test that sensitive header values are redacted.""" trace_config = aiohttp_client.create_trace_config( captured_request_headers=["Authorization", "X-Api-Key"], captured_response_headers=["Set-Cookie", "X-Secret"], sensitive_headers=[ "Authorization", "X-Api-Key", "Set-Cookie", "X-Secret", ], ) def custom_handler(_request): response_headers = { "Set-Cookie": "session=abc123", "X-Secret": "secret", } return aiohttp.web.Response(status=200, text="OK", headers=response_headers) self._http_request( trace_config=trace_config, url="/test-path", status_code=200, request_handler=custom_handler, headers={ "Authorization": "Bearer secret-token", "X-Api-Key": "secret-key", }, ) span = self._assert_single_span() self.assertEqual( span.attributes["http.request.header.authorization"], ("[REDACTED]",), ) self.assertEqual( span.attributes["http.request.header.x_api_key"], ("[REDACTED]",), ) self.assertEqual( span.attributes["http.response.header.set_cookie"], ("[REDACTED]",), ) self.assertEqual( span.attributes["http.response.header.x_secret"], ("[REDACTED]",), ) def test_custom_headers_with_regex(self): """Test that header capture works with regex patterns.""" trace_config = aiohttp_client.create_trace_config( captured_request_headers=["X-Custom-Request-.*"], captured_response_headers=["X-Custom-Response-.*"], ) def custom_handler(_request): response_headers = { "X-Custom-Response-A": "value-A", "X-Custom-Response-B": "value-B", "X-Other-Response-Header": "other-value", } return aiohttp.web.Response(status=200, text="OK", headers=response_headers) self._http_request( trace_config=trace_config, url="/test-path", status_code=200, request_handler=custom_handler, headers={ "X-Custom-Request-One": "value-one", "X-Custom-Request-Two": "value-two", "X-Other-Request-Header": "other-value", }, ) span = self._assert_single_span() self.assertEqual( span.attributes["http.request.header.x_custom_request_one"], ("value-one",), ) self.assertEqual( span.attributes["http.request.header.x_custom_request_two"], ("value-two",), ) self.assertNotIn("http.request.header.x_other_request_header", span.attributes) self.assertEqual( span.attributes["http.response.header.x_custom_response_a"], ("value-A",), ) self.assertEqual( span.attributes["http.response.header.x_custom_response_b"], ("value-B",), ) self.assertNotIn("http.response.header.x_other_response_header", span.attributes) def test_custom_headers_case_insensitive(self): """Test that header capture is case-insensitive.""" trace_config = aiohttp_client.create_trace_config( captured_request_headers=["x-request-header"], captured_response_headers=["x-response-header"], ) def custom_handler(_request): response_headers = {"X-ReSPoNse-HeaDER": "custom-value"} return aiohttp.web.Response(status=200, text="OK", headers=response_headers) self._http_request( trace_config=trace_config, url="/test-path", status_code=200, request_handler=custom_handler, headers={"X-ReQuESt-HeaDER": "custom-value"}, ) span = self._assert_single_span() self.assertEqual( span.attributes["http.request.header.x_request_header"], ("custom-value",), ) self.assertEqual( span.attributes["http.response.header.x_response_header"], ("custom-value",), ) def test_multi_value_request_headers(self): """Test that headers with multiple values are captured correctly.""" trace_config = aiohttp_client.create_trace_config(captured_request_headers=["X-Multi-Value"]) self._http_request( trace_config=trace_config, url="/test-path", status_code=200, headers=(("X-Multi-Value", "value1"), ("X-Multi-Value", "value2")), ) span = self._assert_single_span() self.assertEqual( span.attributes["http.request.header.x_multi_value"], ("value1", "value2"), ) def test_standard_http_headers_captured(self): """Test that standard HTTP headers can be captured.""" trace_config = aiohttp_client.create_trace_config( captured_request_headers=["Content-Type", "Accept"], captured_response_headers=["Content-Type", "Server"], ) def custom_handler(_request): response_headers = { "Content-Type": "text/plain", "Server": "TestServer/1.0", } return aiohttp.web.Response(status=200, body=b"OK", headers=response_headers) self._http_request( trace_config=trace_config, url="/test-path", status_code=200, request_handler=custom_handler, headers={ "Content-Type": "application/json", "Accept": "application/json", }, ) span = self._assert_single_span() self.assertEqual( span.attributes["http.request.header.content_type"], ("application/json",), ) self.assertEqual( span.attributes["http.request.header.accept"], ("application/json",), ) self.assertEqual( span.attributes["http.response.header.content_type"], ("text/plain",), ) self.assertEqual( span.attributes["http.response.header.server"], ("TestServer/1.0",), ) def test_capture_all_request_headers(self): """Test that all request headers can be captured with .* pattern.""" trace_config = aiohttp_client.create_trace_config(captured_request_headers=[".*"]) def custom_handler(_request): return aiohttp.web.Response(status=200, text="OK") self._http_request( trace_config=trace_config, url="/test-path", status_code=200, request_handler=custom_handler, headers={ "X-Header-One": "value1", "X-Header-Two": "value2", "X-Header-Three": "value3", }, ) span = self._assert_single_span() self.assertEqual( span.attributes["http.request.header.x_header_one"], ("value1",), ) self.assertEqual( span.attributes["http.request.header.x_header_two"], ("value2",), ) self.assertEqual( span.attributes["http.request.header.x_header_three"], ("value3",), ) def test_capture_all_response_headers(self): """Test that all response headers can be captured with .* pattern.""" trace_config = aiohttp_client.create_trace_config(captured_response_headers=[".*"]) def custom_handler(_request): response_headers = { "X-Response-One": "value1", "X-Response-Two": "value2", "X-Response-Three": "value3", } return aiohttp.web.Response(status=200, text="OK", headers=response_headers) self._http_request( trace_config=trace_config, url="/test-path", status_code=200, request_handler=custom_handler, ) span = self._assert_single_span() self.assertEqual( span.attributes["http.response.header.x_response_one"], ("value1",), ) self.assertEqual( span.attributes["http.response.header.x_response_two"], ("value2",), ) self.assertEqual( span.attributes["http.response.header.x_response_three"], ("value3",), ) def test_sanitize_with_regex_pattern(self): """Test that sanitization works with regex patterns.""" trace_config = aiohttp_client.create_trace_config( captured_request_headers=["X-Test.*"], sensitive_headers=[".*secret.*"], ) self._http_request( trace_config=trace_config, url="/test-path", status_code=200, headers={ "X-Test": "normal-value", "X-Test-Secret": "secret-value", }, ) span = self._assert_single_span() self.assertEqual( span.attributes["http.request.header.x_test"], ("normal-value",), ) self.assertEqual( span.attributes["http.request.header.x_test_secret"], ("[REDACTED]",), ) class TestAioHttpClientInstrumentor(TestBase): URL = "/test-path" def setUp(self): super().setUp() AioHttpClientInstrumentor().instrument() _OpenTelemetrySemanticConventionStability._initialized = False def tearDown(self): super().tearDown() AioHttpClientInstrumentor().uninstrument() @staticmethod # pylint:disable=unused-argument async def default_handler(request): return aiohttp.web.Response(status=200) @staticmethod def get_default_request(url: str = URL): async def default_request(server: aiohttp.test_utils.TestServer): async with aiohttp.test_utils.TestClient(server) as session: await session.get(url) return default_request def _assert_spans(self, num_spans: int): finished_spans = self.memory_exporter.get_finished_spans() self.assertEqual(num_spans, len(finished_spans)) if num_spans == 0: return None if num_spans == 1: return finished_spans[0] return finished_spans def _assert_metrics(self, num_metrics: int = 1): metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), num_metrics) return metrics def test_instrument(self): host, port = run_with_test_server(self.get_default_request(), self.URL, self.default_handler) span = self._assert_spans(1) self.assertEqual("GET", span.name) self.assertEqual("GET", span.attributes[HTTP_METHOD]) self.assertEqual( f"http://{host}:{port}/test-path", span.attributes[HTTP_URL], ) self.assertEqual(200, span.attributes[HTTP_STATUS_CODE]) metrics = self._assert_metrics(1) duration_data_point = metrics[0].data.data_points[0] self.assertEqual(duration_data_point.count, 1) self.assertEqual( dict(duration_data_point.attributes), { HTTP_HOST: host, HTTP_STATUS_CODE: 200, HTTP_METHOD: "GET", NET_PEER_NAME: host, NET_PEER_PORT: port, }, ) def test_instrument_new_semconv(self): AioHttpClientInstrumentor().uninstrument() with mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "http"}): AioHttpClientInstrumentor().instrument() host, port = run_with_test_server(self.get_default_request(), self.URL, self.default_handler) span = self._assert_spans(1) self.assertEqual("GET", span.name) self.assertEqual("GET", span.attributes[HTTP_REQUEST_METHOD]) self.assertEqual( f"http://{host}:{port}/test-path", span.attributes[URL_FULL], ) self.assertEqual(200, span.attributes[HTTP_RESPONSE_STATUS_CODE]) metrics = self._assert_metrics(1) duration_data_point = metrics[0].data.data_points[0] self.assertEqual(duration_data_point.count, 1) self.assertEqual( dict(duration_data_point.attributes), { HTTP_RESPONSE_STATUS_CODE: 200, HTTP_REQUEST_METHOD: "GET", SERVER_ADDRESS: host, SERVER_PORT: port, }, ) def test_instrument_both_semconv(self): AioHttpClientInstrumentor().uninstrument() with mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "http/dup"}): AioHttpClientInstrumentor().instrument() host, port = run_with_test_server(self.get_default_request(), self.URL, self.default_handler) url = f"http://{host}:{port}/test-path" span = self._assert_spans(1) self.assertEqual("GET", span.name) self.assertEqual( dict(span.attributes), { HTTP_REQUEST_METHOD: "GET", HTTP_METHOD: "GET", HTTP_HOST: host, URL_FULL: url, HTTP_URL: url, HTTP_RESPONSE_STATUS_CODE: 200, HTTP_STATUS_CODE: 200, SERVER_ADDRESS: host, SERVER_PORT: port, NET_PEER_PORT: port, }, ) metrics = self._assert_metrics(2) duration_data_point = metrics[0].data.data_points[0] self.assertEqual(duration_data_point.count, 1) self.assertEqual( dict(duration_data_point.attributes), { HTTP_STATUS_CODE: 200, HTTP_METHOD: "GET", HTTP_HOST: host, NET_PEER_NAME: host, NET_PEER_PORT: port, }, ) duration_data_point = metrics[1].data.data_points[0] self.assertEqual(duration_data_point.count, 1) self.assertEqual( dict(duration_data_point.attributes), { HTTP_RESPONSE_STATUS_CODE: 200, HTTP_REQUEST_METHOD: "GET", SERVER_ADDRESS: host, SERVER_PORT: port, }, ) def test_instrument_with_custom_trace_config(self): trace_config = aiohttp.TraceConfig() AioHttpClientInstrumentor().uninstrument() AioHttpClientInstrumentor().instrument(trace_configs=[trace_config]) async def make_request(server: aiohttp.test_utils.TestServer): async with aiohttp.test_utils.TestClient(server) as client: trace_configs = client.session._trace_configs self.assertEqual(2, len(trace_configs)) self.assertTrue(trace_config in trace_configs) async with client as session: await session.get(TestAioHttpClientInstrumentor.URL) run_with_test_server(make_request, self.URL, self.default_handler) self._assert_spans(1) def test_every_request_by_new_session_creates_one_span(self): async def make_request(server: aiohttp.test_utils.TestServer): async with aiohttp.test_utils.TestClient(server) as client: async with client as session: await session.get(TestAioHttpClientInstrumentor.URL) for request_no in range(3): self.memory_exporter.clear() with self.subTest(request_no=request_no): run_with_test_server(make_request, self.URL, self.default_handler) self._assert_spans(1) def test_instrument_with_existing_trace_config(self): trace_config = aiohttp.TraceConfig() async def create_session(server: aiohttp.test_utils.TestServer): async with aiohttp.test_utils.TestClient(server, trace_configs=[trace_config]) as client: # pylint:disable=protected-access trace_configs = client.session._trace_configs self.assertEqual(2, len(trace_configs)) self.assertTrue(trace_config in trace_configs) async with client as session: await session.get(TestAioHttpClientInstrumentor.URL) run_with_test_server(create_session, self.URL, self.default_handler) self._assert_spans(1) def test_no_op_tracer_provider(self): AioHttpClientInstrumentor().uninstrument() AioHttpClientInstrumentor().instrument(tracer_provider=trace_api.NoOpTracerProvider()) run_with_test_server(self.get_default_request(), self.URL, self.default_handler) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) def test_uninstrument(self): AioHttpClientInstrumentor().uninstrument() run_with_test_server(self.get_default_request(), self.URL, self.default_handler) self._assert_spans(0) AioHttpClientInstrumentor().instrument() run_with_test_server(self.get_default_request(), self.URL, self.default_handler) self._assert_spans(1) def test_uninstrument_session(self): async def uninstrument_request(server: aiohttp.test_utils.TestServer): client = aiohttp.test_utils.TestClient(server) AioHttpClientInstrumentor().uninstrument_session(client.session) async with client as session: await session.get(self.URL) run_with_test_server(uninstrument_request, self.URL, self.default_handler) self._assert_spans(0) run_with_test_server(self.get_default_request(), self.URL, self.default_handler) self._assert_spans(1) def test_suppress_instrumentation(self): for suppress_ctx in ( suppress_instrumentation, suppress_http_instrumentation, ): with self.subTest(suppress_ctx=suppress_ctx.__name__): with suppress_ctx(): run_with_test_server( self.get_default_request(), self.URL, self.default_handler, ) self._assert_spans(0) self._assert_metrics(0) @staticmethod def make_suppressed_request(suppress_ctx): async def suppressed_request(server: aiohttp.test_utils.TestServer): async with aiohttp.test_utils.TestClient(server) as client: with suppress_ctx(): await client.get(TestAioHttpClientInstrumentor.URL) return suppressed_request def test_suppress_instrumentation_after_creation(self): for suppress_ctx in ( suppress_instrumentation, suppress_http_instrumentation, ): with self.subTest(suppress_ctx=suppress_ctx.__name__): run_with_test_server( self.make_suppressed_request(suppress_ctx), self.URL, self.default_handler, ) self._assert_spans(0) self._assert_metrics(0) def test_suppress_instrumentation_with_server_exception(self): # pylint:disable=unused-argument async def raising_handler(request): raise aiohttp.web.HTTPFound(location=self.URL) for suppress_ctx in ( suppress_instrumentation, suppress_http_instrumentation, ): with self.subTest(suppress_ctx=suppress_ctx.__name__): run_with_test_server( self.make_suppressed_request(suppress_ctx), self.URL, raising_handler, ) self._assert_spans(0) self._assert_metrics(0) def test_url_filter(self): def strip_query_params(url: yarl.URL) -> str: return str(url.with_query(None)) AioHttpClientInstrumentor().uninstrument() AioHttpClientInstrumentor().instrument(url_filter=strip_query_params) url = "/test-path?query=params" host, port = run_with_test_server(self.get_default_request(url), url, self.default_handler) span = self._assert_spans(1) self.assertEqual( f"http://{host}:{port}/test-path", span.attributes[HTTP_URL], ) def test_hooks(self): def request_hook(span: Span, params: aiohttp.TraceRequestStartParams): span.update_name(f"{params.method} - {params.url.path}") def response_hook( span: Span, params: aiohttp.TraceRequestEndParams | aiohttp.TraceRequestExceptionParams, ): span.set_attribute("response_hook_attr", "value") AioHttpClientInstrumentor().uninstrument() AioHttpClientInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook) url = "/test-path" run_with_test_server(self.get_default_request(url), url, self.default_handler) span = self._assert_spans(1) self.assertEqual("GET - /test-path", span.name) self.assertIn("response_hook_attr", span.attributes) self.assertEqual(span.attributes["response_hook_attr"], "value") @mock.patch.dict(os.environ, {"OTEL_PYTHON_AIOHTTP_CLIENT_EXCLUDED_URLS": "/test-path"}) def test_ignores_excluded_urls(self): # need the env var set at instrument time AioHttpClientInstrumentor().uninstrument() AioHttpClientInstrumentor().instrument() url = "/test-path?query=params" run_with_test_server(self.get_default_request(url), url, self.default_handler) self._assert_spans(0) self._assert_metrics(0) class TestLoadingAioHttpInstrumentor(unittest.TestCase): def test_loading_instrumentor(self): (entry_point,) = entry_points(group="opentelemetry_instrumentor", name="aiohttp-client") instrumentor = entry_point.load()() self.assertIsInstance(instrumentor, AioHttpClientInstrumentor) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-server/000077500000000000000000000000001527070072300341175ustar00rootroot00000000000000README.rst000066400000000000000000000016561527070072300355370ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-serverOpenTelemetry aiohttp server Integration ======================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-aiohttp-server.svg :target: https://pypi.org/project/opentelemetry-instrumentation-aiohttp-server/ This library allows tracing HTTP requests made by the `aiohttp server `_ library. Installation ------------ :: pip install opentelemetry-instrumentation-aiohttp-server References ---------- * `OpenTelemetry aiohttp server Instrumentation `_ * `aiohttp Tracing Reference `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000033331527070072300367560ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-server[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-aiohttp-server" dynamic = ["version"] description = "Aiohttp server instrumentation for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io"} ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-util-http == 0.66b2", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [ "aiohttp ~= 3.0", ] [project.entry-points.opentelemetry_instrumentor] aiohttp-server = "opentelemetry.instrumentation.aiohttp_server:AioHttpServerInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-aiohttp-server" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/aiohttp_server/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] src/000077500000000000000000000000001527070072300346275ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-serveropentelemetry/000077500000000000000000000000001527070072300375235ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-server/srcinstrumentation/000077500000000000000000000000001527070072300427665ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetryaiohttp_server/000077500000000000000000000000001527070072300460245ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation__init__.py000066400000000000000000000500451527070072300501410ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The opentelemetry-instrumentation-aiohttp-server package allows tracing HTTP requests made by the aiohttp server library. Usage ----- .. code:: python from aiohttp import web from opentelemetry.instrumentation.aiohttp_server import ( AioHttpServerInstrumentor ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased # Optional: configure non-default TracerProvider, resource, sampler resource = Resource(attributes={"service.name": "my-aiohttp-service"}) sampler = ParentBased(root=TraceIdRatioBased(rate=0.25)) # sample 25% of traces AioHttpServerInstrumentor().instrument(tracer_provider=TracerProvider(resource=resource, sampler=sampler)) async def hello(request): return web.Response(text="Hello, world") app = web.Application() app.add_routes([web.get('/', hello)]) web.run_app(app) Configuration ------------- Exclude lists ************* To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_AIOHTTP_SERVER_EXCLUDED_URLS`` (or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the URLs. For example, :: export OTEL_PYTHON_AIOHTTP_SERVER_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. Capture HTTP request and response headers ***************************************** You can configure the agent to capture specified HTTP headers as span attributes, according to the `semantic conventions `_. Request headers *************** To capture HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. Request header names in aiohttp are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" Would match all request headers that start with ``Accept`` and ``X-``. To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*" The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a list containing the header values. For example: ``http.request.header.custom_request_header = [", "]`` Response headers **************** To capture HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. Response header names in aiohttp are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" Would match all response headers that start with ``Content`` and ``X-``. To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*" The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a list containing the header values. For example: ``http.response.header.custom_response_header = [", "]`` Sanitizing headers ****************** In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be matched in a case-insensitive manner. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. Note: The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. API --- """ from __future__ import annotations from timeit import default_timer from aiohttp import web from multidict import CIMultiDictProxy from opentelemetry import metrics, trace from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, _filter_semconv_active_request_count_attr, _filter_semconv_duration_attrs, _get_schema_url, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _report_new, _report_old, _server_active_requests_count_attrs_new, _server_active_requests_count_attrs_old, _server_duration_attrs_new, _server_duration_attrs_old, _set_http_flavor_version, _set_http_method, _set_http_net_host, _set_http_net_host_port, _set_http_scheme, _set_http_target, _set_http_user_agent, _set_status, _StabilityMode, ) from opentelemetry.instrumentation.aiohttp_server.package import _instruments from opentelemetry.instrumentation.aiohttp_server.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import ( is_http_instrumentation_enabled, ) from opentelemetry.propagate import extract from opentelemetry.propagators.textmap import Getter from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_HOST, HTTP_ROUTE, HTTP_SERVER_NAME, HTTP_URL, ) from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.semconv.metrics.http_metrics import ( HTTP_SERVER_REQUEST_DURATION, ) from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, SanitizeValue, _parse_url_query, get_custom_headers, get_excluded_urls, normalise_request_header_name, normalise_response_header_name, redact_query_parameters, redact_url, sanitize_method, ) tracer = None meter_old = None meter_new = None duration_histogram_old = None duration_histogram_new = None active_requests_counter = None _excluded_urls = None _sem_conv_opt_in_mode = _StabilityMode.DEFAULT def _parse_active_request_count_attrs(req_attrs, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT): return _filter_semconv_active_request_count_attr( req_attrs, _server_active_requests_count_attrs_old, _server_active_requests_count_attrs_new, sem_conv_opt_in_mode, ) def _parse_duration_attrs( req_attrs: dict[str, str | None], sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ): return _filter_semconv_duration_attrs( req_attrs, _server_duration_attrs_old, _server_duration_attrs_new, sem_conv_opt_in_mode, ) def get_default_span_name(request: web.Request) -> str: """Returns the span name. Args: request: the request object itself. Returns: The span name as "{method} {canonical_name}" of a resource if possible or just "{method}". """ path = _get_canonical_path(request) method = sanitize_method(request.method) if method == "_OTHER": method = "HTTP" if path: return f"{method} {path}" return f"{method}" def _get_canonical_path(request: web.Request) -> str: """Returns the canonical path from the request handler. Args: request: the request object itself. Returns: a string containing the canonical path """ try: resource = request.match_info.route.resource path = resource.canonical except AttributeError: path = "" return path def collect_request_attributes( request: web.Request, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ) -> dict: """Collects HTTP request attributes from the aiohttp request context and returns a dictionary to be used as span creation attributes.""" result: dict[str, str | None] = {} _set_http_method( result, request.method, sanitize_method(request.method), sem_conv_opt_in_mode, ) _set_http_scheme( result, request.scheme, sem_conv_opt_in_mode, ) server_host = request.url.host port = request.url.port if server_host: _set_http_net_host(result, server_host, sem_conv_opt_in_mode) if _report_old(sem_conv_opt_in_mode): result[HTTP_HOST] = server_host if port: _set_http_net_host_port(result, port, sem_conv_opt_in_mode) if request.path_qs: redacted_target = redact_query_parameters(request.path_qs) _, redacted_query = _parse_url_query(redacted_target) _set_http_target( result, redacted_target, request.path, redacted_query, sem_conv_opt_in_mode, ) # old semconv v1.20.0 - always set HTTP_URL when reporting old semconv if _report_old(sem_conv_opt_in_mode): result[HTTP_URL] = redact_url(str(request.url)) if user_agent := request.headers.get("user-agent"): _set_http_user_agent(result, user_agent, sem_conv_opt_in_mode) flavor = f"{request.version.major}.{request.version.minor}" _set_http_flavor_version(result, flavor, sem_conv_opt_in_mode) # http.route for both old and new result[HTTP_ROUTE] = _get_canonical_path(request) if _report_old(sem_conv_opt_in_mode): http_host_value_list = [request.host] if not isinstance(request.host, list) else request.host if http_host_value_list: result[HTTP_SERVER_NAME] = ",".join(http_host_value_list) return result def collect_request_headers_attributes( request: web.Request, ) -> dict[str, list[str]]: sanitize = SanitizeValue(get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS)) return sanitize.sanitize_header_values( request.headers, get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST), normalise_request_header_name, ) def collect_response_headers_attributes( response: web.Response, ) -> dict[str, list[str]]: sanitize = SanitizeValue(get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS)) return sanitize.sanitize_header_values( response.headers, get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE), normalise_response_header_name, ) def set_status_code( span: trace.Span, status_code: int, duration_attrs: dict[str, str | None] | None = None, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ) -> None: """Adds HTTP response attributes to span using the status_code argument.""" if duration_attrs is None: duration_attrs = {} status_code_str = str(status_code) try: status_code_int = int(status_code) except ValueError: status_code_int = -1 _set_status( span, duration_attrs, status_code_int, status_code_str, server_span=True, sem_conv_opt_in_mode=sem_conv_opt_in_mode, ) class AiohttpGetter(Getter): """Extract current trace from headers""" def get(self, carrier, key: str) -> list | None: """Getter implementation to retrieve an HTTP header value from the aiohttp request context. Args: carrier: aiohttp request context object key: header name in request context Returns: A list of all header values matching the key, or None if the key does not match any header. """ headers: CIMultiDictProxy = carrier.headers if not headers: return None return headers.getall(key, None) def keys(self, carrier: dict) -> list: return list(carrier.keys()) getter = AiohttpGetter() def create_aiohttp_middleware( tracer_provider: trace.TracerProvider | None = None, ): _tracer = tracer_provider.get_tracer(__name__, __version__) if tracer_provider else tracer @web.middleware async def _middleware(request, handler): """Middleware for aiohttp implementing tracing logic""" if not is_http_instrumentation_enabled() or _excluded_urls.url_disabled(request.url.path): return await handler(request) span_name = get_default_span_name(request) request_attrs = collect_request_attributes(request, _sem_conv_opt_in_mode) active_requests_count_attrs = _parse_active_request_count_attrs( request_attrs, _sem_conv_opt_in_mode, ) with _tracer.start_as_current_span( span_name, context=extract(request, getter=getter), kind=trace.SpanKind.SERVER, attributes=request_attrs, set_status_on_exception=False, record_exception=False, ) as span: if span.is_recording(): span.set_attributes(collect_request_headers_attributes(request)) start = default_timer() active_requests_counter.add(1, active_requests_count_attrs) try: resp = await handler(request) set_status_code( span, resp.status, request_attrs, _sem_conv_opt_in_mode, ) if span.is_recording(): response_headers_attributes = collect_response_headers_attributes(resp) span.set_attributes(response_headers_attributes) except web.HTTPServerError as ex: set_status_code( span, ex.status_code, request_attrs, _sem_conv_opt_in_mode, ) span.record_exception(ex) raise except web.HTTPException as ex: set_status_code( span, ex.status_code, request_attrs, _sem_conv_opt_in_mode, ) raise except Exception as ex: set_status_code( span, type(ex).__qualname__, request_attrs, _sem_conv_opt_in_mode, ) span.record_exception(ex) raise finally: duration_s = default_timer() - start if duration_histogram_old: duration_attrs_old = _parse_duration_attrs(request_attrs, _StabilityMode.DEFAULT) duration_histogram_old.record(max(round(duration_s * 1000), 0), duration_attrs_old) if duration_histogram_new: duration_attrs_new = _parse_duration_attrs(request_attrs, _StabilityMode.HTTP) duration_histogram_new.record(max(duration_s, 0), duration_attrs_new) active_requests_counter.add(-1, active_requests_count_attrs) return resp return _middleware middleware = create_aiohttp_middleware() # for backwards compatibility def create_instrumented_application( tracer_provider: trace.TracerProvider | None = None, ): _middleware = create_aiohttp_middleware(tracer_provider=tracer_provider) class _InstrumentedApplication(web.Application): """Insert tracing middleware""" def __init__(self, *args, **kwargs): middlewares = kwargs.pop("middlewares", []) middlewares.insert(0, _middleware) kwargs["middlewares"] = middlewares super().__init__(*args, **kwargs) return _InstrumentedApplication class AioHttpServerInstrumentor(BaseInstrumentor): # pylint: disable=protected-access """An instrumentor for aiohttp.web.Application See `BaseInstrumentor` """ def _instrument(self, **kwargs): _OpenTelemetrySemanticConventionStability._initialize() sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) tracer_provider = kwargs.get("tracer_provider", None) # update global values at instrument time so we can test them global _excluded_urls # pylint: disable=global-statement _excluded_urls = get_excluded_urls("AIOHTTP_SERVER") global _sem_conv_opt_in_mode # pylint: disable=global-statement _sem_conv_opt_in_mode = sem_conv_opt_in_mode global tracer # pylint: disable=global-statement tracer = trace.get_tracer( __name__, __version__, schema_url=_get_schema_url(sem_conv_opt_in_mode), ) global meter_old # pylint: disable=global-statement global meter_new # pylint: disable=global-statement global duration_histogram_old # pylint: disable=global-statement global duration_histogram_new # pylint: disable=global-statement global active_requests_counter # pylint: disable=global-statement if _report_old(sem_conv_opt_in_mode): meter_old = metrics.get_meter( __name__, __version__, schema_url=_get_schema_url(_StabilityMode.DEFAULT), ) duration_histogram_old = meter_old.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", description="Measures the duration of inbound HTTP requests.", ) if _report_new(sem_conv_opt_in_mode): meter_new = metrics.get_meter( __name__, __version__, schema_url=_get_schema_url(_StabilityMode.HTTP), ) duration_histogram_new = meter_new.create_histogram( name=HTTP_SERVER_REQUEST_DURATION, unit="s", description="Duration of HTTP server requests.", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) meter_for_counter = meter_new or meter_old active_requests_counter = meter_for_counter.create_up_down_counter( name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, unit="{request}", description="Number of active HTTP server requests.", ) self._original_app = web.Application _InstrumentedApplication = create_instrumented_application(tracer_provider=tracer_provider) web.Application = _InstrumentedApplication def _uninstrument(self, **kwargs): web.Application = self._original_app def instrumentation_dependencies(self): return _instruments package.py000066400000000000000000000002521527070072300477700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("aiohttp ~= 3.0",) _supports_metrics = True _semconv_status = "migration" version.py000066400000000000000000000001441527070072300500620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000007201527070072300403000ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-serveraiohttp==3.10.2 aiosignal==1.3.1 asgiref==3.8.1 async-timeout==4.0.3 Deprecated==1.2.14 frozenlist==1.4.1 idna==3.7 iniconfig==2.0.0 multidict==6.0.5 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 pytest-aiohttp==1.0.5 pytest-asyncio==0.23.5 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 yarl==1.9.4 zipp==3.19.2 -e opentelemetry-instrumentation -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-aiohttp-server tests/000077500000000000000000000000001527070072300352025ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-server__init__.py000066400000000000000000000000001527070072300373010ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-server/teststest_aiohttp_server_integration.py000066400000000000000000001071471527070072300442660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines from enum import Enum from http import HTTPStatus import aiohttp import pytest import pytest_asyncio from multidict import CIMultiDict from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, _server_active_requests_count_attrs_new, _server_active_requests_count_attrs_old, _server_duration_attrs_new, _server_duration_attrs_old, _StabilityMode, ) from opentelemetry.instrumentation.aiohttp_server import ( AioHttpServerInstrumentor, ) from opentelemetry.instrumentation.utils import suppress_http_instrumentation from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, ) from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_FLAVOR, HTTP_METHOD, HTTP_ROUTE, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_TARGET, HTTP_URL, HTTP_USER_AGENT, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_HOST_NAME, NET_HOST_PORT, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, ) from opentelemetry.semconv.attributes.network_attributes import ( NETWORK_PROTOCOL_VERSION, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.semconv.attributes.url_attributes import ( URL_PATH, URL_QUERY, URL_SCHEME, ) from opentelemetry.semconv.attributes.user_agent_attributes import ( USER_AGENT_ORIGINAL, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import StatusCode from opentelemetry.util._importlib_metadata import entry_points from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, ) class HTTPMethod(Enum): """HTTP methods and descriptions""" def __repr__(self): return f"{self.value}" CONNECT = "CONNECT" DELETE = "DELETE" GET = "GET" HEAD = "HEAD" OPTIONS = "OPTIONS" PATCH = "PATCH" POST = "POST" PUT = "PUT" TRACE = "TRACE" SCOPE = "opentelemetry.instrumentation.aiohttp_server" @pytest.fixture(name="test_base", scope="function") def fixture_test_base(): test_base = TestBase() test_base.setUp() _OpenTelemetrySemanticConventionStability._initialized = False try: yield test_base finally: # Reset semconv stability state otherwise tests can interfere with each other _OpenTelemetrySemanticConventionStability._initialized = False test_base.tearDown() @pytest.fixture(name="tracer", scope="function") def fixture_tracer(test_base: TestBase): return test_base.tracer_provider, test_base.memory_exporter @pytest.fixture(name="meter", scope="function") def fixture_meter(test_base: TestBase): return test_base.meter_provider, test_base.memory_metrics_reader async def default_handler(request, status=200): return aiohttp.web.Response(status=status) @pytest.fixture(name="suppress") def fixture_suppress(): return False @pytest_asyncio.fixture(name="server_fixture") async def fixture_server_fixture(tracer, aiohttp_server, suppress): tracer_provider, _ = tracer AioHttpServerInstrumentor().instrument(tracer_provider=tracer_provider) app = aiohttp.web.Application() app.add_routes( [ aiohttp.web.get("/test-path", default_handler), aiohttp.web.get("/test-path/{url_param}", default_handler), aiohttp.web.get("/object/{object_id}/action/{another_param}", default_handler), ] ) if suppress: with suppress_http_instrumentation(): server = await aiohttp_server(app) else: server = await aiohttp_server(app) yield server, app AioHttpServerInstrumentor().uninstrument() def test_checking_instrumentor_pkg_installed(): (instrumentor_entrypoint,) = entry_points(group="opentelemetry_instrumentor", name="aiohttp-server") instrumentor = instrumentor_entrypoint.load()() assert isinstance(instrumentor, AioHttpServerInstrumentor) @pytest.mark.asyncio @pytest.mark.parametrize( "url, expected_method, expected_status_code", [ ("/test-path", HTTPMethod.GET, HTTPStatus.OK), ("/not-found", HTTPMethod.GET, HTTPStatus.NOT_FOUND), ], ) async def test_status_code_instrumentation( test_base: TestBase, server_fixture, aiohttp_client, url, expected_method, expected_status_code, ): server, _ = server_fixture assert len(test_base.get_finished_spans()) == 0 metrics = test_base.get_sorted_metrics(SCOPE) assert len(metrics) == 0 client = await aiohttp_client(server) await client.get(url) assert len(test_base.get_finished_spans()) == 1 metrics = test_base.get_sorted_metrics(SCOPE) assert len(metrics) == 2 [span] = test_base.get_finished_spans() assert expected_method.value == span.attributes[HTTP_METHOD] assert expected_status_code == span.attributes[HTTP_STATUS_CODE] assert url == span.attributes[HTTP_TARGET] @pytest.mark.asyncio @pytest.mark.parametrize( "span_name, example_paths", [ ( "GET /test-path/{url_param}", ( "/test-path/foo", "/test-path/bar", ), ), ( "GET /object/{object_id}/action/{another_param}", ( "/object/1/action/bar", "/object/234/action/baz", ), ), ( "GET", ( "/i/dont/exist", "/me-neither", ), ), ], ) async def test_url_params_instrumentation( tracer, server_fixture, aiohttp_client, span_name, example_paths, ): _, memory_exporter = tracer server, _ = server_fixture assert len(memory_exporter.get_finished_spans()) == 0 client = await aiohttp_client(server) for path in example_paths: await client.get(path) assert len(memory_exporter.get_finished_spans()) == 2 for request_path, span in zip(example_paths, memory_exporter.get_finished_spans()): assert span_name == span.name assert request_path == span.attributes[HTTP_TARGET] full_url = f"http://{server.host}:{server.port}{request_path}" assert full_url == span.attributes[HTTP_URL] @pytest.mark.asyncio async def test_span_name_handler_unknown_method( tracer, server_fixture, ): _, memory_exporter = tracer server, _ = server_fixture assert len(memory_exporter.get_finished_spans()) == 0 async with aiohttp.ClientSession(base_url=f"http://{server.host}:{server.port}") as session: # PURGE is accepted by aiohttp but not recognized by sanitize_method. async with session.request("PURGE", "/test-path") as response: assert response.status == HTTPStatus.METHOD_NOT_ALLOWED spans = memory_exporter.get_finished_spans() assert len(spans) == 1 assert "_OTHER" == spans[0].attributes[HTTP_METHOD] assert "HTTP" == spans[0].name @pytest.mark.asyncio @pytest.mark.parametrize("suppress", [True]) async def test_suppress_instrumentation(test_base: TestBase, server_fixture, aiohttp_client): server, _ = server_fixture assert len(test_base.get_finished_spans()) == 0 client = await aiohttp_client(server) await client.get("/test-path") assert len(test_base.get_finished_spans()) == 0 @pytest.mark.asyncio async def test_remove_sensitive_params(test_base: TestBase, aiohttp_server, monkeypatch): """Test that sensitive information in URLs is properly redacted.""" # Use old semconv to test HTTP_URL redaction monkeypatch.setenv(OTEL_SEMCONV_STABILITY_OPT_IN, _StabilityMode.DEFAULT.value) # Set up instrumentation AioHttpServerInstrumentor().instrument() # Create app with test route app = aiohttp.web.Application() async def handler(request): return aiohttp.web.Response(text="hello") app.router.add_get("/status/200", handler) # Start the server server = await aiohttp_server(app) # Make request with sensitive data in URL url = f"http://username:password@{server.host}:{server.port}/status/200?Signature=secret" async with aiohttp.ClientSession() as session: async with session.get(url) as response: assert response.status == 200 assert await response.text() == "hello" # Verify redaction in span attributes spans = test_base.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.attributes[HTTP_METHOD] == "GET" assert span.attributes[HTTP_STATUS_CODE] == 200 assert span.attributes[HTTP_URL] == f"http://{server.host}:{server.port}/status/200?Signature=REDACTED" # Clean up AioHttpServerInstrumentor().uninstrument() @pytest.mark.asyncio async def test_remove_sensitive_params_new(test_base: TestBase, aiohttp_server, monkeypatch): """Test URL handling with new semantic conventions (no redaction for URL_PATH/URL_QUERY).""" # Use new semconv monkeypatch.setenv(OTEL_SEMCONV_STABILITY_OPT_IN, _StabilityMode.HTTP.value) # Set up instrumentation AioHttpServerInstrumentor().instrument() # Create app with test route app = aiohttp.web.Application() async def handler(request): return aiohttp.web.Response(text="hello") app.router.add_get("/status/200", handler) # Start the server server = await aiohttp_server(app) # Make request with sensitive data in URL url = f"http://username:password@{server.host}:{server.port}/status/200?Signature=secret" async with aiohttp.ClientSession() as session: async with session.get(url) as response: assert response.status == 200 assert await response.text() == "hello" # Verify span attributes with new semconv spans = test_base.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.attributes[HTTP_REQUEST_METHOD] == "GET" assert span.attributes[HTTP_RESPONSE_STATUS_CODE] == 200 assert span.attributes[URL_PATH] == "/status/200" assert span.attributes[URL_QUERY] == "Signature=REDACTED" assert HTTP_URL not in span.attributes # Clean up AioHttpServerInstrumentor().uninstrument() @pytest.mark.asyncio @pytest.mark.parametrize( "env_var", ["OTEL_PYTHON_AIOHTTP_SERVER_EXCLUDED_URLS", "OTEL_PYTHON_EXCLUDED_URLS"], ) async def test_excluded_urls(test_base: TestBase, aiohttp_server, monkeypatch, env_var): """Test that excluded env vars are taken into account.""" monkeypatch.setenv(env_var, "/status/200") AioHttpServerInstrumentor().instrument() app = aiohttp.web.Application() async def handler(request): return aiohttp.web.Response(text="hello") app.router.add_get("/status/200", handler) server = await aiohttp_server(app) url = f"http://{server.host}:{server.port}/status/200" async with aiohttp.ClientSession() as session: async with session.get(url) as response: assert response.status == 200 assert await response.text() == "hello" spans = test_base.get_finished_spans() assert len(spans) == 0 metrics = test_base.get_sorted_metrics(SCOPE) assert len(metrics) == 0 AioHttpServerInstrumentor().uninstrument() @pytest.mark.asyncio @pytest.mark.parametrize( "tracer", [TestBase().create_tracer_provider(sampler=ParentBased(TraceIdRatioBased(0.05)))], ) async def test_non_global_tracer_provider( tracer, server_fixture, aiohttp_client, ): n_requests = 1000 collection_ratio = 0.05 n_expected_trace_ids = n_requests * collection_ratio _, memory_exporter = tracer server, _ = server_fixture assert len(memory_exporter.get_finished_spans()) == 0 client = await aiohttp_client(server) for _ in range(n_requests): await client.get("/test-path") trace_ids = {span.context.trace_id for span in memory_exporter.get_finished_spans() if span.context is not None} assert 0.5 * n_expected_trace_ids <= len(trace_ids) <= 1.5 * n_expected_trace_ids @pytest.mark.asyncio async def test_custom_request_headers(test_base: TestBase, aiohttp_server, monkeypatch): # pylint: disable=too-many-locals monkeypatch.setenv( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, ".*my-secret.*", ) monkeypatch.setenv( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", ) AioHttpServerInstrumentor().instrument() app = aiohttp.web.Application() async def handler(request): return aiohttp.web.Response(text="hello") app.router.add_get("/status/200", handler) server = await aiohttp_server(app) url = f"http://{server.host}:{server.port}/status/200" async with aiohttp.ClientSession() as session: headers = { "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", "Regex-Test-Header-1": "Regex Test Value 1", "regex-test-header-2": "RegexTestValue2,RegexTestValue3", "My-Secret-Header": "My Secret Value", } async with session.get(url, headers=headers) as response: assert response.status == 200 assert await response.text() == "hello" spans = test_base.get_finished_spans() assert len(spans) == 1 span = spans[0] expected = { "http.request.header.custom_test_header_1": ("test-header-value-1",), "http.request.header.custom_test_header_2": ("test-header-value-2",), "http.request.header.regex_test_header_1": ("Regex Test Value 1",), "http.request.header.regex_test_header_2": ("RegexTestValue2,RegexTestValue3",), "http.request.header.my_secret_header": ("[REDACTED]",), } for attribute, value in expected.items(): assert span.attributes.get(attribute) == value assert "http.request.header.custom_test_header_3" not in span.attributes AioHttpServerInstrumentor().uninstrument() @pytest.mark.asyncio async def test_custom_response_headers(test_base: TestBase, aiohttp_server, monkeypatch): monkeypatch.setenv( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, ".*my-secret.*", ) monkeypatch.setenv( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", ) AioHttpServerInstrumentor().instrument() app = aiohttp.web.Application() async def handler(request): headers = CIMultiDict( **{ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", "my-custom-regex-header-1": "my-custom-regex-value-1,my-custom-regex-value-2", "My-Custom-Regex-Header-2": "my-custom-regex-value-3,my-custom-regex-value-4", "My-Secret-Header": "My Secret Value", } ) return aiohttp.web.Response(text="hello", headers=headers) app.router.add_get("/status/200", handler) server = await aiohttp_server(app) url = f"http://{server.host}:{server.port}/status/200" async with aiohttp.ClientSession() as session: async with session.get(url) as response: assert response.status == 200 assert await response.text() == "hello" spans = test_base.get_finished_spans() assert len(spans) == 1 span = spans[0] expected = { "http.response.header.custom_test_header_1": ("test-header-value-1",), "http.response.header.custom_test_header_2": ("test-header-value-2",), "http.response.header.my_custom_regex_header_1": ("my-custom-regex-value-1,my-custom-regex-value-2",), "http.response.header.my_custom_regex_header_2": ("my-custom-regex-value-3,my-custom-regex-value-4",), "http.response.header.my_secret_header": ("[REDACTED]",), } for attribute, value in expected.items(): assert span.attributes.get(attribute) == value assert "http.response.header.custom_test_header_3" not in span.attributes AioHttpServerInstrumentor().uninstrument() # pylint: disable=too-many-locals @pytest.mark.asyncio async def test_semantic_conventions_metrics_old_default(test_base: TestBase, aiohttp_server, monkeypatch): monkeypatch.setenv(OTEL_SEMCONV_STABILITY_OPT_IN, _StabilityMode.DEFAULT.value) AioHttpServerInstrumentor().instrument() app = aiohttp.web.Application() app.router.add_get("/test-path/{param}", default_handler) server = await aiohttp_server(app) client_session = aiohttp.ClientSession() try: url = f"http://{server.host}:{server.port}/test-path/test-param?query=test" async with client_session.get( # pylint: disable=not-async-context-manager url, headers={"User-Agent": "test-agent"} ) as response: assert response.status == 200 spans = test_base.get_finished_spans() assert len(spans) == 1 span = spans[0] # Verify old semconv schema URL assert span.instrumentation_scope.schema_url == "https://opentelemetry.io/schemas/1.11.0" # Old semconv span attributes present assert span.attributes.get(HTTP_METHOD) == "GET" assert span.attributes.get(HTTP_SCHEME) == "http" assert span.attributes.get(NET_HOST_NAME) == server.host assert span.attributes.get(NET_HOST_PORT) == server.port assert span.attributes.get(HTTP_TARGET) == "/test-path/test-param?query=test" assert span.attributes.get(HTTP_USER_AGENT) == "test-agent" assert span.attributes.get(HTTP_FLAVOR) == "1.1" assert span.attributes.get(HTTP_STATUS_CODE) == 200 assert span.attributes.get(HTTP_ROUTE) == "/test-path/{param}" # New semconv span attributes NOT present assert HTTP_REQUEST_METHOD not in span.attributes assert URL_SCHEME not in span.attributes assert SERVER_ADDRESS not in span.attributes assert SERVER_PORT not in span.attributes assert URL_PATH not in span.attributes assert URL_QUERY not in span.attributes assert USER_AGENT_ORIGINAL not in span.attributes assert NETWORK_PROTOCOL_VERSION not in span.attributes assert HTTP_RESPONSE_STATUS_CODE not in span.attributes metrics = test_base.get_sorted_metrics(SCOPE) expected_metric_names = [ "http.server.active_requests", "http.server.duration", ] recommended_metrics_attrs = { "http.server.active_requests": _server_active_requests_count_attrs_old, "http.server.duration": _server_duration_attrs_old, } for metric in metrics: assert metric.name in expected_metric_names if metric.name == "http.server.duration": assert metric.unit == "ms" for point in metric.data.data_points: for attr in point.attributes: assert attr in recommended_metrics_attrs[metric.name] finally: await client_session.close() AioHttpServerInstrumentor().uninstrument() # pylint: disable=too-many-locals @pytest.mark.asyncio async def test_semantic_conventions_metrics_new(test_base: TestBase, meter, aiohttp_server, monkeypatch): monkeypatch.setenv(OTEL_SEMCONV_STABILITY_OPT_IN, _StabilityMode.HTTP.value) AioHttpServerInstrumentor().instrument() app = aiohttp.web.Application() app.router.add_get("/test-path/{param}", default_handler) server = await aiohttp_server(app) client_session = aiohttp.ClientSession() try: url = f"http://{server.host}:{server.port}/test-path/test-param?query=test" async with client_session.get( # pylint: disable=not-async-context-manager url, headers={"User-Agent": "test-agent"} ) as response: assert response.status == 200 spans = test_base.get_finished_spans() assert len(spans) == 1 span = spans[0] # Verify new semconv schema URL assert span.instrumentation_scope.schema_url == "https://opentelemetry.io/schemas/1.21.0" # New semconv span attributes present assert span.attributes.get(HTTP_REQUEST_METHOD) == "GET" assert span.attributes.get(URL_SCHEME) == "http" assert span.attributes.get(SERVER_ADDRESS) == server.host assert span.attributes.get(SERVER_PORT) == server.port assert span.attributes.get(URL_PATH) == "/test-path/test-param" assert span.attributes.get(URL_QUERY) == "query=test" assert span.attributes.get(USER_AGENT_ORIGINAL) == "test-agent" assert span.attributes.get(NETWORK_PROTOCOL_VERSION) == "1.1" assert span.attributes.get(HTTP_RESPONSE_STATUS_CODE) == 200 assert span.attributes.get(HTTP_ROUTE) == "/test-path/{param}" # Old semconv span attributes NOT present assert HTTP_METHOD not in span.attributes assert HTTP_SCHEME not in span.attributes assert NET_HOST_NAME not in span.attributes assert NET_HOST_PORT not in span.attributes assert HTTP_TARGET not in span.attributes assert HTTP_USER_AGENT not in span.attributes assert HTTP_FLAVOR not in span.attributes assert HTTP_STATUS_CODE not in span.attributes metrics = test_base.get_sorted_metrics(SCOPE) expected_metric_names = [ "http.server.active_requests", "http.server.request.duration", ] recommended_metrics_attrs = { "http.server.active_requests": _server_active_requests_count_attrs_new, "http.server.request.duration": _server_duration_attrs_new, } for metric in metrics: assert metric.name in expected_metric_names if metric.name == "http.server.request.duration": assert metric.unit == "s" for point in metric.data.data_points: if isinstance(point, HistogramDataPoint) and metric.name == "http.server.request.duration": assert point.explicit_bounds == HTTP_DURATION_HISTOGRAM_BUCKETS_NEW for attr in point.attributes: assert attr in recommended_metrics_attrs[metric.name] finally: await client_session.close() AioHttpServerInstrumentor().uninstrument() # pylint: disable=too-many-locals # pylint: disable=too-many-statements @pytest.mark.asyncio async def test_semantic_conventions_metrics_both(test_base: TestBase, meter, aiohttp_server, monkeypatch): monkeypatch.setenv(OTEL_SEMCONV_STABILITY_OPT_IN, _StabilityMode.HTTP_DUP.value) AioHttpServerInstrumentor().instrument() app = aiohttp.web.Application() app.router.add_get("/test-path", default_handler) server = await aiohttp_server(app) client_session = aiohttp.ClientSession() try: url = f"http://{server.host}:{server.port}/test-path?query=test" async with client_session.get( # pylint: disable=not-async-context-manager url, headers={"User-Agent": "test-agent"} ) as response: assert response.status == 200 spans = test_base.get_finished_spans() assert len(spans) == 1 span = spans[0] # Verify new semconv schema URL (both mode uses new schema) assert span.instrumentation_scope.schema_url == "https://opentelemetry.io/schemas/1.21.0" # Both old and new semconv span attributes present assert span.attributes.get(HTTP_METHOD) == "GET" assert span.attributes.get(HTTP_REQUEST_METHOD) == "GET" assert span.attributes.get(HTTP_SCHEME) == "http" assert span.attributes.get(URL_SCHEME) == "http" assert span.attributes.get(NET_HOST_NAME) == server.host assert span.attributes.get(SERVER_ADDRESS) == server.host assert span.attributes.get(NET_HOST_PORT) == server.port assert span.attributes.get(SERVER_PORT) == server.port assert span.attributes.get(HTTP_TARGET) == "/test-path?query=test" assert span.attributes.get(URL_PATH) == "/test-path" assert span.attributes.get(URL_QUERY) == "query=test" assert span.attributes.get(HTTP_USER_AGENT) == "test-agent" assert span.attributes.get(USER_AGENT_ORIGINAL) == "test-agent" assert span.attributes.get(HTTP_FLAVOR) == "1.1" assert span.attributes.get(NETWORK_PROTOCOL_VERSION) == "1.1" assert span.attributes.get(HTTP_STATUS_CODE) == 200 assert span.attributes.get(HTTP_RESPONSE_STATUS_CODE) == 200 assert span.attributes.get(HTTP_ROUTE) == "/test-path" metrics = test_base.get_sorted_metrics(SCOPE) assert len(metrics) == 3 # Both duration metrics + active requests server_active_requests_count_attrs_both = list(_server_active_requests_count_attrs_old) server_active_requests_count_attrs_both.extend(_server_active_requests_count_attrs_new) recommended_metrics_attrs = { "http.server.active_requests": server_active_requests_count_attrs_both, "http.server.duration": _server_duration_attrs_old, "http.server.request.duration": _server_duration_attrs_new, } for metric in metrics: if metric.unit == "ms": assert metric.name == "http.server.duration" elif metric.unit == "s": assert metric.name == "http.server.request.duration" else: assert metric.name == "http.server.active_requests" for point in metric.data.data_points: if isinstance(point, HistogramDataPoint) and metric.name == "http.server.request.duration": assert point.explicit_bounds == HTTP_DURATION_HISTOGRAM_BUCKETS_NEW for attr in point.attributes: assert attr in recommended_metrics_attrs[metric.name] finally: await client_session.close() AioHttpServerInstrumentor().uninstrument() @pytest.mark.asyncio @pytest.mark.parametrize( "exception_class,expected_status", [ (aiohttp.web.HTTPOk, 200), (aiohttp.web.HTTPCreated, 201), ], ) async def test_http_successful_no_error(test_base: TestBase, aiohttp_server, exception_class, expected_status): AioHttpServerInstrumentor().instrument() app = aiohttp.web.Application() async def handler(request): raise exception_class() app.router.add_get("/test", handler) server = await aiohttp_server(app) url = f"http://{server.host}:{server.port}/test" async with aiohttp.ClientSession() as session: async with session.get(url) as response: assert response.status == expected_status spans = test_base.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.attributes.get(HTTP_STATUS_CODE) == expected_status assert span.status.status_code == StatusCode.UNSET assert len(span.events) == 0 AioHttpServerInstrumentor().uninstrument() @pytest.mark.asyncio @pytest.mark.parametrize( "exception_class,expected_status,kwargs", [ (aiohttp.web.HTTPMovedPermanently, 301, {"location": "/destination"}), (aiohttp.web.HTTPFound, 302, {"location": "/destination"}), ], ) async def test_http_redirection_no_error( test_base: TestBase, aiohttp_server, exception_class, expected_status, kwargs, ): AioHttpServerInstrumentor().instrument() app = aiohttp.web.Application() async def handler(request): raise exception_class(**kwargs) app.router.add_get("/test", handler) server = await aiohttp_server(app) url = f"http://{server.host}:{server.port}/test" async with aiohttp.ClientSession() as session: async with session.get(url, allow_redirects=False) as response: assert response.status == expected_status spans = test_base.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.attributes.get(HTTP_STATUS_CODE) == expected_status assert span.status.status_code == StatusCode.UNSET assert len(span.events) == 0 AioHttpServerInstrumentor().uninstrument() @pytest.mark.asyncio @pytest.mark.parametrize( "exception_class,expected_status,kwargs", [ (aiohttp.web.HTTPMovedPermanently, 301, {"location": "/destination"}), (aiohttp.web.HTTPFound, 302, {"location": "/destination"}), ], ) async def test_http_redirection_no_error_new_semconv( test_base: TestBase, aiohttp_server, monkeypatch, exception_class, expected_status, kwargs, ): # Use new semconv monkeypatch.setenv(OTEL_SEMCONV_STABILITY_OPT_IN, _StabilityMode.HTTP.value) AioHttpServerInstrumentor().instrument() app = aiohttp.web.Application() async def handler(request): raise exception_class(**kwargs) app.router.add_get("/test", handler) server = await aiohttp_server(app) url = f"http://{server.host}:{server.port}/test" async with aiohttp.ClientSession() as session: async with session.get(url, allow_redirects=False) as response: assert response.status == expected_status spans = test_base.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.attributes.get(HTTP_RESPONSE_STATUS_CODE) == expected_status assert span.status.status_code == StatusCode.UNSET assert len(span.events) == 0 AioHttpServerInstrumentor().uninstrument() @pytest.mark.asyncio @pytest.mark.parametrize( "exception_class,expected_status", [ (aiohttp.web.HTTPBadRequest, 400), (aiohttp.web.HTTPNotFound, 404), ], ) async def test_http_client_error_no_error(test_base: TestBase, aiohttp_server, exception_class, expected_status): AioHttpServerInstrumentor().instrument() app = aiohttp.web.Application() async def handler(request): raise exception_class() app.router.add_get("/test", handler) server = await aiohttp_server(app) url = f"http://{server.host}:{server.port}/test" async with aiohttp.ClientSession() as session: async with session.get(url) as response: assert response.status == expected_status spans = test_base.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.attributes.get(HTTP_STATUS_CODE) == expected_status assert span.status.status_code == StatusCode.UNSET assert len(span.events) == 0 AioHttpServerInstrumentor().uninstrument() @pytest.mark.asyncio @pytest.mark.parametrize( "exception_class,expected_status", [ (aiohttp.web.HTTPInternalServerError, 500), (aiohttp.web.HTTPBadGateway, 502), ], ) async def test_http_server_error_records_error(test_base: TestBase, aiohttp_server, exception_class, expected_status): AioHttpServerInstrumentor().instrument() app = aiohttp.web.Application() async def handler(request): raise exception_class() app.router.add_get("/test", handler) server = await aiohttp_server(app) url = f"http://{server.host}:{server.port}/test" async with aiohttp.ClientSession() as session: async with session.get(url) as response: assert response.status == expected_status spans = test_base.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.attributes.get(HTTP_STATUS_CODE) == expected_status assert span.status.status_code == StatusCode.ERROR assert len(span.events) == 1 assert span.events[0].name == "exception" assert span.events[0].attributes["exception.type"] == f"aiohttp.web_exceptions.{exception_class.__name__}" AioHttpServerInstrumentor().uninstrument() @pytest.mark.asyncio @pytest.mark.parametrize( "exception_class,message", [ (ValueError, "Something went wrong"), (RuntimeError, "Unexpected runtime error"), ], ) async def test_generic_exception_records_error(test_base: TestBase, aiohttp_server, exception_class, message): AioHttpServerInstrumentor().instrument() app = aiohttp.web.Application() async def handler(request): raise exception_class(message) app.router.add_get("/test", handler) server = await aiohttp_server(app) url = f"http://{server.host}:{server.port}/test" async with aiohttp.ClientSession() as session: async with session.get(url) as response: assert response.status == 500 spans = test_base.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.status.status_code == StatusCode.ERROR assert len(span.events) == 1 assert span.events[0].name == "exception" assert span.events[0].attributes["exception.type"] == exception_class.__qualname__ assert span.events[0].attributes["exception.message"] == message AioHttpServerInstrumentor().uninstrument() @pytest.mark.asyncio @pytest.mark.parametrize( "exception_class,expected_status", [ (aiohttp.web.HTTPInternalServerError, 500), (aiohttp.web.HTTPBadGateway, 502), ], ) async def test_http_server_error_records_error_new_semconv( test_base: TestBase, aiohttp_server, monkeypatch, exception_class, expected_status, ): # Use new semconv monkeypatch.setenv(OTEL_SEMCONV_STABILITY_OPT_IN, _StabilityMode.HTTP.value) AioHttpServerInstrumentor().instrument() app = aiohttp.web.Application() async def handler(request): raise exception_class() app.router.add_get("/test", handler) server = await aiohttp_server(app) url = f"http://{server.host}:{server.port}/test" async with aiohttp.ClientSession() as session: async with session.get(url) as response: assert response.status == expected_status spans = test_base.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.status.status_code == StatusCode.ERROR assert len(span.events) == 1 assert span.attributes[ERROR_TYPE] == str(expected_status) assert span.events[0].name == "exception" assert span.events[0].attributes["exception.type"] == f"aiohttp.web_exceptions.{exception_class.__name__}" AioHttpServerInstrumentor().uninstrument() @pytest.mark.asyncio @pytest.mark.parametrize( "exception_class,message", [ (ValueError, "Something went wrong"), (RuntimeError, "Unexpected runtime error"), ], ) async def test_generic_exception_records_error_new_semconv( test_base: TestBase, aiohttp_server, monkeypatch, exception_class, message ): # Use new semconv monkeypatch.setenv(OTEL_SEMCONV_STABILITY_OPT_IN, _StabilityMode.HTTP.value) AioHttpServerInstrumentor().instrument() app = aiohttp.web.Application() async def handler(request): raise exception_class(message) app.router.add_get("/test", handler) server = await aiohttp_server(app) url = f"http://{server.host}:{server.port}/test" async with aiohttp.ClientSession() as session: async with session.get(url) as response: assert response.status == 500 spans = test_base.get_finished_spans() assert len(spans) == 1 span = spans[0] print(span.to_json()) assert span.status.status_code == StatusCode.ERROR assert len(span.events) == 1 assert span.attributes[ERROR_TYPE] == exception_class.__qualname__ assert span.events[0].name == "exception" assert span.events[0].attributes["exception.type"] == exception_class.__qualname__ AioHttpServerInstrumentor().uninstrument() opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiokafka/000077500000000000000000000000001527070072300327115ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiokafka/LICENSE000066400000000000000000000261351527070072300337250ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000012511527070072300343200ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiokafkaOpenTelemetry aiokafka instrumentation ======================================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-aiokafka.svg :target: https://pypi.org/project/opentelemetry-instrumentation-aiokafka/ Installation ------------ :: pip install opentelemetry-instrumentation-aiokafka References ---------- * `OpenTelemetry aiokafka Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031721527070072300355510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiokafka[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-aiokafka" dynamic = ["version"] description = "OpenTelemetry aiokafka instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.27", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "typing_extensions ~= 4.1", ] [project.optional-dependencies] instruments = ["aiokafka >= 0.8, < 1.0"] [project.entry-points.opentelemetry_instrumentor] aiokafka = "opentelemetry.instrumentation.aiokafka:AIOKafkaInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-aiokafka" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/aiokafka/version.py" [tool.hatch.build.targets.sdist] include = ["/src", "/tests"] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiokafka/src/000077500000000000000000000000001527070072300335005ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300363155ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiokafka/srcinstrumentation/000077500000000000000000000000001527070072300415605ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiokafka/src/opentelemetryaiokafka/000077500000000000000000000000001527070072300433265ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiokafka/src/opentelemetry/instrumentation__init__.py000066400000000000000000000136021527070072300454410ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiokafka/src/opentelemetry/instrumentation/aiokafka# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Instrument aiokafka to report instrumentation-kafka produced and consumed messages Usage ----- .. code:: python import asyncio from opentelemetry.instrumentation.aiokafka import AIOKafkaInstrumentor from aiokafka import AIOKafkaProducer, AIOKafkaConsumer # Instrument kafka AIOKafkaInstrumentor().instrument() # report a span of type producer with the default settings async def produce(): producer = AIOKafkaProducer(bootstrap_servers=['localhost:9092']) await producer.start() try: await producer.send_and_wait('my-topic', b'raw_bytes') finally: await producer.stop() # report a span of type consumer with the default settings async def consume(): consumer = AIOKafkaConsumer('my-topic', group_id='my-group', bootstrap_servers=['localhost:9092']) await consumer.start() try: async for message in consumer: # process message print(message) finally: await consumer.stop() asyncio.run(produce()) asyncio.run(consume()) The ``_instrument()`` method accepts the following keyword args: - **tracer_provider** (TracerProvider) - an optional tracer provider - **async_produce_hook** (Callable) - a function with extra user-defined logic to be performed before sending the message Function signature: .. code:: python async def async_produce_hook(span: Span, args, kwargs): ... - **async_consume_hook** (Callable) - a function with extra user-defined logic to be performed after consuming a message Function signature: .. code:: python async def async_consume_hook(span: Span, record: kafka.record.ABCRecord, args, kwargs): ... For example: .. code:: python import asyncio from opentelemetry.instrumentation.aiokafka import AIOKafkaInstrumentor from aiokafka import AIOKafkaProducer, AIOKafkaConsumer async def async_produce_hook(span, args, kwargs): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_async_response_hook", "some-value") async def async_consume_hook(span, record, args, kwargs): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_consume_hook", "some-value") # instrument kafka with produce and consume hooks AIOKafkaInstrumentor().instrument(async_produce_hook=async_produce_hook, async_consume_hook=async_consume_hook) # Using kafka as normal now will automatically generate spans, # including user custom attributes added from the hooks async def produce(): producer = AIOKafkaProducer(bootstrap_servers=['localhost:9092']) await producer.start() try: await producer.send_and_wait('my-topic', b'raw_bytes') finally: await producer.stop() asyncio.run(produce()) API --- """ from __future__ import annotations from collections.abc import Collection from inspect import iscoroutinefunction from typing import TYPE_CHECKING import aiokafka from wrapt import ( wrap_function_wrapper, # type: ignore[reportUnknownVariableType] ) from opentelemetry import trace from opentelemetry.instrumentation.aiokafka.package import _instruments from opentelemetry.instrumentation.aiokafka.utils import ( _wrap_getmany, _wrap_getone, _wrap_send, ) from opentelemetry.instrumentation.aiokafka.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap from opentelemetry.semconv.schemas import Schemas if TYPE_CHECKING: from typing import TypedDict from typing_extensions import Unpack from .utils import ConsumeHookT, ProduceHookT class InstrumentKwargs(TypedDict, total=False): tracer_provider: trace.TracerProvider async_produce_hook: ProduceHookT async_consume_hook: ConsumeHookT class UninstrumentKwargs(TypedDict, total=False): pass class AIOKafkaInstrumentor(BaseInstrumentor): """An instrumentor for kafka module See `BaseInstrumentor` """ def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs: Unpack[InstrumentKwargs]): """Instruments the kafka module Args: **kwargs: Optional arguments ``tracer_provider``: a TracerProvider, defaults to global. ``async_produce_hook``: a callable to be executed just before producing a message ``async_consume_hook``: a callable to be executed just after consuming a message """ tracer_provider = kwargs.get("tracer_provider") async_produce_hook = kwargs.get("async_produce_hook") if not iscoroutinefunction(async_produce_hook): async_produce_hook = None async_consume_hook = kwargs.get("async_consume_hook") if not iscoroutinefunction(async_consume_hook): async_consume_hook = None tracer = trace.get_tracer( __name__, __version__, tracer_provider=tracer_provider, schema_url=Schemas.V1_27_0.value, ) wrap_function_wrapper( aiokafka.AIOKafkaProducer, "send", _wrap_send(tracer, async_produce_hook), ) wrap_function_wrapper( aiokafka.AIOKafkaConsumer, "getone", _wrap_getone(tracer, async_consume_hook), ) wrap_function_wrapper( aiokafka.AIOKafkaConsumer, "getmany", _wrap_getmany(tracer, async_consume_hook), ) def _uninstrument(self, **kwargs: Unpack[UninstrumentKwargs]): unwrap(aiokafka.AIOKafkaProducer, "send") unwrap(aiokafka.AIOKafkaConsumer, "getone") unwrap(aiokafka.AIOKafkaConsumer, "getmany") package.py000066400000000000000000000001711527070072300452720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiokafka/src/opentelemetry/instrumentation/aiokafka# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("aiokafka >= 0.8, < 1.0",) py.typed000066400000000000000000000000001527070072300450130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiokafka/src/opentelemetry/instrumentation/aiokafkautils.py000066400000000000000000000442531527070072300450500ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiokafka/src/opentelemetry/instrumentation/aiokafka# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import asyncio import contextlib import json from collections.abc import Awaitable, Callable, MutableSequence, Sequence from logging import getLogger from typing import ( TYPE_CHECKING, Any, Protocol, cast, ) import aiokafka from opentelemetry import context, propagate, trace from opentelemetry.context import Context from opentelemetry.propagators import textmap from opentelemetry.semconv._incubating.attributes import messaging_attributes from opentelemetry.semconv.attributes import server_attributes from opentelemetry.trace import Tracer from opentelemetry.trace.span import Span if TYPE_CHECKING: from aiokafka.structs import RecordMetadata class AIOKafkaGetOneProto(Protocol): async def __call__(self, *partitions: aiokafka.TopicPartition) -> aiokafka.ConsumerRecord[object, object]: ... class AIOKafkaGetManyProto(Protocol): async def __call__( self, *partitions: aiokafka.TopicPartition, timeout_ms: int = 0, max_records: int | None = None, ) -> dict[ aiokafka.TopicPartition, list[aiokafka.ConsumerRecord[object, object]], ]: ... class AIOKafkaSendProto(Protocol): async def __call__( self, topic: str, value: object | None = None, key: object | None = None, partition: int | None = None, timestamp_ms: int | None = None, headers: HeadersT | None = None, ) -> asyncio.Future[RecordMetadata]: ... ProduceHookT = Callable[[Span, tuple[Any, ...], dict[str, Any]], Awaitable[None]] ConsumeHookT = Callable[ [ Span, aiokafka.ConsumerRecord[object, object], tuple[aiokafka.TopicPartition, ...], dict[str, Any], ], Awaitable[None], ] HeadersT = Sequence[tuple[str, bytes | None]] _LOG = getLogger(__name__) def _extract_bootstrap_servers( client: aiokafka.AIOKafkaClient, ) -> str | list[str]: return client._bootstrap_servers def _extract_client_id(client: aiokafka.AIOKafkaClient) -> str: return client._client_id def _extract_consumer_group( consumer: aiokafka.AIOKafkaConsumer, ) -> str | None: return consumer._group_id # type: ignore[reportUnknownVariableType] def _extract_argument( key: str, position: int, default_value: Any, args: tuple[Any, ...], kwargs: dict[str, Any], ) -> Any: if len(args) > position: return args[position] return kwargs.get(key, default_value) def _extract_send_topic(args: tuple[Any, ...], kwargs: dict[str, Any]) -> str: """extract topic from `send` method arguments in AIOKafkaProducer class""" return _extract_argument("topic", 0, "unknown", args, kwargs) def _extract_send_value(args: tuple[Any, ...], kwargs: dict[str, Any]) -> object | None: """extract value from `send` method arguments in AIOKafkaProducer class""" return _extract_argument("value", 1, None, args, kwargs) def _extract_send_key(args: tuple[Any, ...], kwargs: dict[str, Any]) -> object | None: """extract key from `send` method arguments in AIOKafkaProducer class""" return _extract_argument("key", 2, None, args, kwargs) def _extract_send_headers(args: tuple[Any, ...], kwargs: dict[str, Any]) -> HeadersT | None: """extract headers from `send` method arguments in AIOKafkaProducer class""" return _extract_argument("headers", 5, None, args, kwargs) def _move_headers_to_kwargs(args: tuple[Any], kwargs: dict[str, Any]) -> tuple[tuple[Any], dict[str, Any]]: """Move headers from args to kwargs""" if len(args) > 5: kwargs["headers"] = args[5] return args[:5], kwargs def _deserialize_key(key: object | None) -> str | None: if key is None: return None if isinstance(key, bytes): with contextlib.suppress(UnicodeDecodeError): return key.decode() return str(key) async def _extract_send_partition( instance: aiokafka.AIOKafkaProducer, args: tuple[Any, ...], kwargs: dict[str, Any], ) -> int | None: """extract partition `send` method arguments, using the `_partition` method in AIOKafkaProducer class""" try: topic = _extract_send_topic(args, kwargs) key = _extract_send_key(args, kwargs) value = _extract_send_value(args, kwargs) partition = _extract_argument("partition", 3, None, args, kwargs) key_bytes = cast( "bytes | None", instance._key_serializer(key) # type: ignore[reportUnknownMemberType] if instance._key_serializer # type: ignore[reportUnknownMemberType] else key, ) value_bytes = cast( "bytes | None", instance._value_serializer(value) # type: ignore[reportUnknownMemberType] if instance._value_serializer # type: ignore[reportUnknownMemberType] else value, ) valid_types = (bytes, bytearray, memoryview, type(None)) if type(key_bytes) not in valid_types or type(value_bytes) not in valid_types: return None await instance.client._wait_on_metadata(topic) # type: ignore[reportUnknownMemberType] return instance._partition( # type: ignore[reportUnknownMemberType] topic, partition, key, value, key_bytes, value_bytes ) except Exception as exception: # pylint: disable=W0703 _LOG.debug("Unable to extract partition: %s", exception) return None class AIOKafkaContextGetter(textmap.Getter["HeadersT"]): def get(self, carrier: HeadersT, key: str) -> list[str] | None: for item_key, value in carrier: if item_key == key: if value is not None: return [value.decode()] return None def keys(self, carrier: HeadersT) -> list[str]: return [key for (key, _) in carrier] class AIOKafkaContextSetter(textmap.Setter["HeadersT"]): def set(self, carrier: HeadersT, key: str | None, value: str | None) -> None: if key is None: return if not isinstance(carrier, MutableSequence): _LOG.warning("Unable to set context in headers. Headers is immutable") return if value is not None: carrier.append((key, value.encode())) else: carrier.append((key, value)) _aiokafka_getter = AIOKafkaContextGetter() _aiokafka_setter = AIOKafkaContextSetter() def _enrich_base_span( span: Span, *, bootstrap_servers: str | list[str], client_id: str, topic: str, partition: int | None, key: str | None, ) -> None: span.set_attribute( messaging_attributes.MESSAGING_SYSTEM, messaging_attributes.MessagingSystemValues.KAFKA.value, ) span.set_attribute(server_attributes.SERVER_ADDRESS, json.dumps(bootstrap_servers)) span.set_attribute(messaging_attributes.MESSAGING_CLIENT_ID, client_id) span.set_attribute(messaging_attributes.MESSAGING_DESTINATION_NAME, topic) if partition is not None: span.set_attribute( messaging_attributes.MESSAGING_DESTINATION_PARTITION_ID, str(partition), ) if key is not None: span.set_attribute(messaging_attributes.MESSAGING_KAFKA_MESSAGE_KEY, key) def _enrich_send_span( span: Span, *, bootstrap_servers: str | list[str], client_id: str, topic: str, partition: int | None, key: str | None, ) -> None: if not span.is_recording(): return _enrich_base_span( span, bootstrap_servers=bootstrap_servers, client_id=client_id, topic=topic, partition=partition, key=key, ) span.set_attribute(messaging_attributes.MESSAGING_OPERATION_NAME, "send") span.set_attribute( messaging_attributes.MESSAGING_OPERATION_TYPE, messaging_attributes.MessagingOperationTypeValues.PUBLISH.value, ) def _enrich_getone_span( span: Span, *, bootstrap_servers: str | list[str], client_id: str, consumer_group: str | None, topic: str, partition: int | None, key: str | None, offset: int, ) -> None: if not span.is_recording(): return _enrich_base_span( span, bootstrap_servers=bootstrap_servers, client_id=client_id, topic=topic, partition=partition, key=key, ) if consumer_group is not None: span.set_attribute(messaging_attributes.MESSAGING_CONSUMER_GROUP_NAME, consumer_group) span.set_attribute(messaging_attributes.MESSAGING_OPERATION_NAME, "receive") span.set_attribute( messaging_attributes.MESSAGING_OPERATION_TYPE, messaging_attributes.MessagingOperationTypeValues.RECEIVE.value, ) span.set_attribute(messaging_attributes.MESSAGING_KAFKA_MESSAGE_OFFSET, offset) # https://stackoverflow.com/questions/65935155/identify-and-find-specific-message-in-kafka-topic # A message within Kafka is uniquely defined by its topic name, topic partition and offset. if partition is not None: span.set_attribute( messaging_attributes.MESSAGING_MESSAGE_ID, f"{topic}.{partition}.{offset}", ) def _enrich_getmany_poll_span( span: Span, *, bootstrap_servers: str | list[str], client_id: str, consumer_group: str | None, message_count: int, ) -> None: if not span.is_recording(): return span.set_attribute( messaging_attributes.MESSAGING_SYSTEM, messaging_attributes.MessagingSystemValues.KAFKA.value, ) span.set_attribute(server_attributes.SERVER_ADDRESS, json.dumps(bootstrap_servers)) span.set_attribute(messaging_attributes.MESSAGING_CLIENT_ID, client_id) if consumer_group is not None: span.set_attribute(messaging_attributes.MESSAGING_CONSUMER_GROUP_NAME, consumer_group) span.set_attribute(messaging_attributes.MESSAGING_BATCH_MESSAGE_COUNT, message_count) span.set_attribute(messaging_attributes.MESSAGING_OPERATION_NAME, "receive") span.set_attribute( messaging_attributes.MESSAGING_OPERATION_TYPE, messaging_attributes.MessagingOperationTypeValues.RECEIVE.value, ) def _enrich_getmany_topic_span( span: Span, *, bootstrap_servers: str | list[str], client_id: str, consumer_group: str | None, topic: str, partition: int, message_count: int, ) -> None: if not span.is_recording(): return _enrich_base_span( span, bootstrap_servers=bootstrap_servers, client_id=client_id, topic=topic, partition=partition, key=None, ) if consumer_group is not None: span.set_attribute(messaging_attributes.MESSAGING_CONSUMER_GROUP_NAME, consumer_group) span.set_attribute(messaging_attributes.MESSAGING_BATCH_MESSAGE_COUNT, message_count) span.set_attribute(messaging_attributes.MESSAGING_OPERATION_NAME, "receive") span.set_attribute( messaging_attributes.MESSAGING_OPERATION_TYPE, messaging_attributes.MessagingOperationTypeValues.RECEIVE.value, ) def _get_span_name(operation: str, topic: str): return f"{topic} {operation}" def _wrap_send( # type: ignore[reportUnusedFunction] tracer: Tracer, async_produce_hook: ProduceHookT | None ) -> Callable[..., Awaitable[asyncio.Future[RecordMetadata]]]: async def _traced_send( func: AIOKafkaSendProto, instance: aiokafka.AIOKafkaProducer, args: tuple[Any, ...], kwargs: dict[str, Any], ) -> asyncio.Future[RecordMetadata]: args, kwargs = _move_headers_to_kwargs(args, kwargs) headers: HeadersT | None = _extract_send_headers(args, kwargs) if headers is None: headers = [] kwargs["headers"] = headers topic = _extract_send_topic(args, kwargs) bootstrap_servers = _extract_bootstrap_servers(instance.client) client_id = _extract_client_id(instance.client) key = _deserialize_key(_extract_send_key(args, kwargs)) partition = await _extract_send_partition(instance, args, kwargs) span_name = _get_span_name("send", topic) with tracer.start_as_current_span(span_name, kind=trace.SpanKind.PRODUCER) as span: _enrich_send_span( span, bootstrap_servers=bootstrap_servers, client_id=client_id, topic=topic, partition=partition, key=key, ) propagate.inject( headers, context=trace.set_span_in_context(span), setter=_aiokafka_setter, ) try: if async_produce_hook is not None: await async_produce_hook(span, args, kwargs) except Exception as hook_exception: # pylint: disable=W0703 _LOG.exception(hook_exception) return await func(*args, **kwargs) return _traced_send async def _create_consumer_span( tracer: Tracer, async_consume_hook: ConsumeHookT | None, record: aiokafka.ConsumerRecord[object, object], extracted_context: Context, bootstrap_servers: str | list[str], client_id: str, consumer_group: str | None, args: tuple[aiokafka.TopicPartition, ...], kwargs: dict[str, Any], ) -> trace.Span: span_name = _get_span_name("receive", record.topic) with tracer.start_as_current_span( span_name, context=extracted_context, kind=trace.SpanKind.CONSUMER, ) as span: new_context = trace.set_span_in_context(span, extracted_context) token = context.attach(new_context) _enrich_getone_span( span, bootstrap_servers=bootstrap_servers, client_id=client_id, consumer_group=consumer_group, topic=record.topic, partition=record.partition, key=_deserialize_key(record.key), offset=record.offset, ) try: if async_consume_hook is not None: await async_consume_hook(span, record, args, kwargs) except Exception as hook_exception: # pylint: disable=W0703 _LOG.exception(hook_exception) if token: context.detach(token) return span def _wrap_getone( # type: ignore[reportUnusedFunction] tracer: Tracer, async_consume_hook: ConsumeHookT | None ) -> Callable[..., Awaitable[aiokafka.ConsumerRecord[object, object]]]: async def _traced_getone( func: AIOKafkaGetOneProto, instance: aiokafka.AIOKafkaConsumer, args: tuple[aiokafka.TopicPartition, ...], kwargs: dict[str, Any], ) -> aiokafka.ConsumerRecord[object, object]: record = await func(*args, **kwargs) if record: bootstrap_servers = _extract_bootstrap_servers(instance._client) client_id = _extract_client_id(instance._client) consumer_group = _extract_consumer_group(instance) extracted_context = propagate.extract(record.headers, getter=_aiokafka_getter) await _create_consumer_span( tracer, async_consume_hook, record, extracted_context, bootstrap_servers, client_id, consumer_group, args, kwargs, ) return record return _traced_getone def _wrap_getmany( # type: ignore[reportUnusedFunction] tracer: Tracer, async_consume_hook: ConsumeHookT | None ) -> Callable[ ..., Awaitable[ dict[ aiokafka.TopicPartition, list[aiokafka.ConsumerRecord[object, object]], ] ], ]: async def _traced_getmany( func: AIOKafkaGetManyProto, instance: aiokafka.AIOKafkaConsumer, args: tuple[aiokafka.TopicPartition, ...], kwargs: dict[str, Any], ) -> dict[aiokafka.TopicPartition, list[aiokafka.ConsumerRecord[object, object]]]: records = await func(*args, **kwargs) if records: bootstrap_servers = _extract_bootstrap_servers(instance._client) client_id = _extract_client_id(instance._client) consumer_group = _extract_consumer_group(instance) span_name = _get_span_name( "receive", ", ".join(sorted({topic.topic for topic in records.keys()})), ) with tracer.start_as_current_span(span_name, kind=trace.SpanKind.CONSUMER) as poll_span: _enrich_getmany_poll_span( poll_span, bootstrap_servers=bootstrap_servers, client_id=client_id, consumer_group=consumer_group, message_count=sum(len(r) for r in records.values()), ) for topic, topic_records in records.items(): span_name = _get_span_name("receive", topic.topic) with tracer.start_as_current_span(span_name, kind=trace.SpanKind.CONSUMER) as topic_span: _enrich_getmany_topic_span( topic_span, bootstrap_servers=bootstrap_servers, client_id=client_id, consumer_group=consumer_group, topic=topic.topic, partition=topic.partition, message_count=len(topic_records), ) for record in topic_records: extracted_context = propagate.extract(record.headers, getter=_aiokafka_getter) record_span = await _create_consumer_span( tracer, async_consume_hook, record, extracted_context, bootstrap_servers, client_id, consumer_group, args, kwargs, ) topic_span.add_link(record_span.get_span_context()) return records return _traced_getmany version.py000066400000000000000000000001441527070072300453640ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiokafka/src/opentelemetry/instrumentation/aiokafka# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000002761527070072300371000ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiokafkaaiokafka==0.13.0; python_version >= "3.10" aiokafka==0.12.0; python_version < "3.10" pytest==7.4.4 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-aiokafka opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiokafka/tests/000077500000000000000000000000001527070072300340535ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300360730ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiokafka/teststest_instrumentation.py000066400000000000000000000465351527070072300406650ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiokafka/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import uuid from collections.abc import Sequence from typing import Any, cast from unittest import IsolatedAsyncioTestCase, TestCase, mock import aiokafka from aiokafka import ( AIOKafkaConsumer, AIOKafkaProducer, ConsumerRecord, TopicPartition, ) from wrapt import BoundFunctionWrapper from opentelemetry import baggage, context from opentelemetry.instrumentation.aiokafka import AIOKafkaInstrumentor from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.semconv._incubating.attributes import messaging_attributes from opentelemetry.semconv.attributes import server_attributes from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind, format_trace_id, set_span_in_context class TestAIOKafkaInstrumentor(TestCase): def test_instrument_api(self) -> None: instrumentation = AIOKafkaInstrumentor() instrumentation.instrument() self.assertTrue(isinstance(AIOKafkaProducer.send, BoundFunctionWrapper)) self.assertTrue(isinstance(AIOKafkaConsumer.getone, BoundFunctionWrapper)) self.assertTrue(isinstance(AIOKafkaConsumer.getmany, BoundFunctionWrapper)) instrumentation.uninstrument() self.assertFalse(isinstance(AIOKafkaProducer.send, BoundFunctionWrapper)) self.assertFalse(isinstance(AIOKafkaConsumer.getone, BoundFunctionWrapper)) self.assertFalse(isinstance(AIOKafkaConsumer.getmany, BoundFunctionWrapper)) class TestAIOKafkaInstrumentation(TestBase, IsolatedAsyncioTestCase): @staticmethod def consumer_record_factory(number: int, headers: tuple[tuple[str, bytes], ...]) -> ConsumerRecord: return ConsumerRecord( f"topic_{number}", number, number, number, number, f"key_{number}".encode(), f"value_{number}".encode(), None, number, number, headers=headers, ) @staticmethod def consumer_batch_factory( *headers: tuple[tuple[str, bytes], ...], ) -> dict[aiokafka.TopicPartition, list[aiokafka.ConsumerRecord]]: records = {} for number, record_headers in enumerate(headers, start=1): records[aiokafka.TopicPartition(topic=f"topic_{number}", partition=number)] = [ ConsumerRecord( f"topic_{number}", number, number, number, number, f"key_{number}".encode(), f"value_{number}".encode(), None, number, number, headers=record_headers, ) ] return records @staticmethod async def consumer_factory(**consumer_kwargs: Any) -> AIOKafkaConsumer: consumer = AIOKafkaConsumer(**consumer_kwargs) consumer._client.bootstrap = mock.AsyncMock() consumer._client._wait_on_metadata = mock.AsyncMock() await consumer.start() consumer._fetcher.next_record = mock.AsyncMock() consumer._fetcher.fetched_records = mock.AsyncMock() return consumer @staticmethod async def producer_factory() -> AIOKafkaProducer: producer = AIOKafkaProducer() producer.client._wait_on_metadata = mock.AsyncMock() producer.client.bootstrap = mock.AsyncMock() producer._message_accumulator.add_message = mock.AsyncMock() producer._sender.start = mock.AsyncMock() producer._partition = mock.Mock(return_value=1) await producer.start() return producer def setUp(self): super().setUp() AIOKafkaInstrumentor().instrument(tracer_provider=self.tracer_provider) def tearDown(self): super().tearDown() AIOKafkaInstrumentor().uninstrument() async def test_getone(self) -> None: client_id = str(uuid.uuid4()) group_id = str(uuid.uuid4()) consumer = await self.consumer_factory(client_id=client_id, group_id=group_id) self.addAsyncCleanup(consumer.stop) next_record_mock = cast(mock.AsyncMock, consumer._fetcher.next_record) expected_spans = [ { "name": "topic_1 receive", "kind": SpanKind.CONSUMER, "attributes": { messaging_attributes.MESSAGING_SYSTEM: messaging_attributes.MessagingSystemValues.KAFKA.value, server_attributes.SERVER_ADDRESS: '"localhost"', messaging_attributes.MESSAGING_CLIENT_ID: client_id, messaging_attributes.MESSAGING_DESTINATION_NAME: "topic_1", messaging_attributes.MESSAGING_DESTINATION_PARTITION_ID: "1", messaging_attributes.MESSAGING_KAFKA_MESSAGE_KEY: "key_1", messaging_attributes.MESSAGING_CONSUMER_GROUP_NAME: group_id, messaging_attributes.MESSAGING_OPERATION_NAME: "receive", messaging_attributes.MESSAGING_OPERATION_TYPE: messaging_attributes.MessagingOperationTypeValues.RECEIVE.value, messaging_attributes.MESSAGING_KAFKA_MESSAGE_OFFSET: 1, messaging_attributes.MESSAGING_MESSAGE_ID: "topic_1.1.1", }, }, { "name": "topic_2 receive", "kind": SpanKind.CONSUMER, "attributes": { messaging_attributes.MESSAGING_SYSTEM: messaging_attributes.MessagingSystemValues.KAFKA.value, server_attributes.SERVER_ADDRESS: '"localhost"', messaging_attributes.MESSAGING_CLIENT_ID: client_id, messaging_attributes.MESSAGING_DESTINATION_NAME: "topic_2", messaging_attributes.MESSAGING_DESTINATION_PARTITION_ID: "2", messaging_attributes.MESSAGING_KAFKA_MESSAGE_KEY: "key_2", messaging_attributes.MESSAGING_CONSUMER_GROUP_NAME: group_id, messaging_attributes.MESSAGING_OPERATION_NAME: "receive", messaging_attributes.MESSAGING_OPERATION_TYPE: messaging_attributes.MessagingOperationTypeValues.RECEIVE.value, messaging_attributes.MESSAGING_KAFKA_MESSAGE_OFFSET: 2, messaging_attributes.MESSAGING_MESSAGE_ID: "topic_2.2.2", }, }, ] self.memory_exporter.clear() next_record_mock.side_effect = [ self.consumer_record_factory( 1, headers=( ( "traceparent", b"00-03afa25236b8cd948fa853d67038ac79-405ff022e8247c46-01", ), ), ), self.consumer_record_factory(2, headers=()), ] await consumer.getone() next_record_mock.assert_awaited_with(()) first_span = self.memory_exporter.get_finished_spans()[0] self.assertEqual( format_trace_id(first_span.get_span_context().trace_id), "03afa25236b8cd948fa853d67038ac79", ) await consumer.getone() next_record_mock.assert_awaited_with(()) span_list = self.memory_exporter.get_finished_spans() self._compare_spans(span_list, expected_spans) async def test_getone_baggage(self) -> None: received_baggage = None async def async_consume_hook(span, *_) -> None: nonlocal received_baggage received_baggage = baggage.get_all(set_span_in_context(span)) AIOKafkaInstrumentor().uninstrument() AIOKafkaInstrumentor().instrument( tracer_provider=self.tracer_provider, async_consume_hook=async_consume_hook, ) consumer = await self.consumer_factory() self.addAsyncCleanup(consumer.stop) next_record_mock = cast(mock.AsyncMock, consumer._fetcher.next_record) self.memory_exporter.clear() next_record_mock.side_effect = [ self.consumer_record_factory( 1, headers=( ( "traceparent", b"00-03afa25236b8cd948fa853d67038ac79-405ff022e8247c46-01", ), ("baggage", b"foo=bar"), ), ), ] await consumer.getone() next_record_mock.assert_awaited_with(()) self.assertEqual(received_baggage, {"foo": "bar"}) async def test_getone_consume_hook(self) -> None: async_consume_hook_mock = mock.AsyncMock() AIOKafkaInstrumentor().uninstrument() AIOKafkaInstrumentor().instrument( tracer_provider=self.tracer_provider, async_consume_hook=async_consume_hook_mock, ) consumer = await self.consumer_factory() self.addAsyncCleanup(consumer.stop) next_record_mock = cast(mock.AsyncMock, consumer._fetcher.next_record) next_record_mock.side_effect = [self.consumer_record_factory(1, headers=())] await consumer.getone() async_consume_hook_mock.assert_awaited_once() async def test_getmany(self) -> None: client_id = str(uuid.uuid4()) group_id = str(uuid.uuid4()) consumer = await self.consumer_factory(client_id=client_id, group_id=group_id) self.addAsyncCleanup(consumer.stop) fetched_records_mock = cast(mock.AsyncMock, consumer._fetcher.fetched_records) expected_spans = [ { "name": "topic_1 receive", "kind": SpanKind.CONSUMER, "attributes": { messaging_attributes.MESSAGING_SYSTEM: messaging_attributes.MessagingSystemValues.KAFKA.value, server_attributes.SERVER_ADDRESS: '"localhost"', messaging_attributes.MESSAGING_CLIENT_ID: client_id, messaging_attributes.MESSAGING_DESTINATION_NAME: "topic_1", messaging_attributes.MESSAGING_DESTINATION_PARTITION_ID: "1", messaging_attributes.MESSAGING_KAFKA_MESSAGE_KEY: "key_1", messaging_attributes.MESSAGING_CONSUMER_GROUP_NAME: group_id, messaging_attributes.MESSAGING_OPERATION_NAME: "receive", messaging_attributes.MESSAGING_OPERATION_TYPE: messaging_attributes.MessagingOperationTypeValues.RECEIVE.value, messaging_attributes.MESSAGING_KAFKA_MESSAGE_OFFSET: 1, messaging_attributes.MESSAGING_MESSAGE_ID: "topic_1.1.1", }, }, { "name": "topic_1 receive", "kind": SpanKind.CONSUMER, "attributes": { messaging_attributes.MESSAGING_SYSTEM: messaging_attributes.MessagingSystemValues.KAFKA.value, server_attributes.SERVER_ADDRESS: '"localhost"', messaging_attributes.MESSAGING_CLIENT_ID: client_id, messaging_attributes.MESSAGING_DESTINATION_NAME: "topic_1", messaging_attributes.MESSAGING_DESTINATION_PARTITION_ID: "1", messaging_attributes.MESSAGING_CONSUMER_GROUP_NAME: group_id, messaging_attributes.MESSAGING_OPERATION_NAME: "receive", messaging_attributes.MESSAGING_OPERATION_TYPE: messaging_attributes.MessagingOperationTypeValues.RECEIVE.value, messaging_attributes.MESSAGING_BATCH_MESSAGE_COUNT: 1, }, }, { "name": "topic_2 receive", "kind": SpanKind.CONSUMER, "attributes": { messaging_attributes.MESSAGING_SYSTEM: messaging_attributes.MessagingSystemValues.KAFKA.value, server_attributes.SERVER_ADDRESS: '"localhost"', messaging_attributes.MESSAGING_CLIENT_ID: client_id, messaging_attributes.MESSAGING_DESTINATION_NAME: "topic_2", messaging_attributes.MESSAGING_DESTINATION_PARTITION_ID: "2", messaging_attributes.MESSAGING_KAFKA_MESSAGE_KEY: "key_2", messaging_attributes.MESSAGING_CONSUMER_GROUP_NAME: group_id, messaging_attributes.MESSAGING_OPERATION_NAME: "receive", messaging_attributes.MESSAGING_OPERATION_TYPE: messaging_attributes.MessagingOperationTypeValues.RECEIVE.value, messaging_attributes.MESSAGING_KAFKA_MESSAGE_OFFSET: 2, messaging_attributes.MESSAGING_MESSAGE_ID: "topic_2.2.2", }, }, { "name": "topic_2 receive", "kind": SpanKind.CONSUMER, "attributes": { messaging_attributes.MESSAGING_SYSTEM: messaging_attributes.MessagingSystemValues.KAFKA.value, server_attributes.SERVER_ADDRESS: '"localhost"', messaging_attributes.MESSAGING_CLIENT_ID: client_id, messaging_attributes.MESSAGING_DESTINATION_NAME: "topic_2", messaging_attributes.MESSAGING_DESTINATION_PARTITION_ID: "2", messaging_attributes.MESSAGING_CONSUMER_GROUP_NAME: group_id, messaging_attributes.MESSAGING_OPERATION_NAME: "receive", messaging_attributes.MESSAGING_OPERATION_TYPE: messaging_attributes.MessagingOperationTypeValues.RECEIVE.value, messaging_attributes.MESSAGING_BATCH_MESSAGE_COUNT: 1, }, }, { "name": "topic_1, topic_2 receive", "kind": SpanKind.CONSUMER, "attributes": { messaging_attributes.MESSAGING_SYSTEM: messaging_attributes.MessagingSystemValues.KAFKA.value, server_attributes.SERVER_ADDRESS: '"localhost"', messaging_attributes.MESSAGING_CLIENT_ID: client_id, messaging_attributes.MESSAGING_CONSUMER_GROUP_NAME: group_id, messaging_attributes.MESSAGING_OPERATION_NAME: "receive", messaging_attributes.MESSAGING_OPERATION_TYPE: messaging_attributes.MessagingOperationTypeValues.RECEIVE.value, messaging_attributes.MESSAGING_BATCH_MESSAGE_COUNT: 2, }, }, ] self.memory_exporter.clear() fetched_records_mock.side_effect = [ self.consumer_batch_factory( ( ( "traceparent", b"00-03afa25236b8cd948fa853d67038ac79-405ff022e8247c46-01", ), ), (), ), ] await consumer.getmany() fetched_records_mock.assert_awaited_with((), 0.0, max_records=None) span_list = self.memory_exporter.get_finished_spans() self._compare_spans(span_list, expected_spans) async def test_send(self) -> None: producer = await self.producer_factory() self.addAsyncCleanup(producer.stop) add_message_mock = cast(mock.AsyncMock, producer._message_accumulator.add_message) tracer = self.tracer_provider.get_tracer(__name__) with tracer.start_as_current_span("test_span") as span: await producer.send("topic_1", b"value_1") add_message_mock.assert_awaited_with( TopicPartition(topic="topic_1", partition=1), None, b"value_1", 40.0, timestamp_ms=None, headers=[("traceparent", mock.ANY)], ) add_message_mock.call_args_list[0].kwargs["headers"][0][1].startswith( f"00-{format_trace_id(span.get_span_context().trace_id)}-".encode() ) await producer.send("topic_2", b"value_2") add_message_mock.assert_awaited_with( TopicPartition(topic="topic_2", partition=1), None, b"value_2", 40.0, timestamp_ms=None, headers=[("traceparent", mock.ANY)], ) async def test_send_baggage(self) -> None: producer = await self.producer_factory() self.addAsyncCleanup(producer.stop) add_message_mock = cast(mock.AsyncMock, producer._message_accumulator.add_message) tracer = self.tracer_provider.get_tracer(__name__) ctx = baggage.set_baggage("foo", "bar") context.attach(ctx) with tracer.start_as_current_span("test_span", context=ctx): await producer.send("topic_1", b"value_1") add_message_mock.assert_awaited_with( TopicPartition(topic="topic_1", partition=1), None, b"value_1", 40.0, timestamp_ms=None, headers=[("traceparent", mock.ANY), ("baggage", b"foo=bar")], ) async def test_send_produce_hook(self) -> None: async_produce_hook_mock = mock.AsyncMock() AIOKafkaInstrumentor().uninstrument() AIOKafkaInstrumentor().instrument( tracer_provider=self.tracer_provider, async_produce_hook=async_produce_hook_mock, ) producer = await self.producer_factory() self.addAsyncCleanup(producer.stop) await producer.send("topic_1", b"value_1") async_produce_hook_mock.assert_awaited_once() def _compare_spans(self, spans: Sequence[ReadableSpan], expected_spans: list[dict]) -> None: self.assertEqual(len(spans), len(expected_spans)) for span, expected_span in zip(spans, expected_spans): self.assertEqual(expected_span["name"], span.name, msg=span.name) self.assertEqual(expected_span["kind"], span.kind, msg=span.name) self.assertEqual( expected_span["attributes"], dict(span.attributes), msg=span.name, ) async def test_send_and_wait(self) -> None: AIOKafkaInstrumentor().uninstrument() AIOKafkaInstrumentor().instrument(tracer_provider=self.tracer_provider) producer = await self.producer_factory() try: add_message_mock: mock.AsyncMock = producer._message_accumulator.add_message add_message_mock.side_effect = [ mock.AsyncMock()(), mock.AsyncMock()(), ] tracer = self.tracer_provider.get_tracer(__name__) with tracer.start_as_current_span("test_span") as span: await producer.send_and_wait("topic_1", b"value_1") add_message_mock.assert_awaited_with( TopicPartition(topic="topic_1", partition=1), None, b"value_1", 40.0, timestamp_ms=None, headers=[("traceparent", mock.ANY)], ) assert ( add_message_mock.call_args_list[0] .kwargs["headers"][0][1] .startswith(f"00-{format_trace_id(span.get_span_context().trace_id)}-".encode()) ) await producer.send_and_wait("topic_2", b"value_2") add_message_mock.assert_awaited_with( TopicPartition(topic="topic_2", partition=1), None, b"value_2", 40.0, timestamp_ms=None, headers=[("traceparent", mock.ANY)], ) finally: await producer.stop() test_utils.py000066400000000000000000000275621527070072300365610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiokafka/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=unnecessary-dunder-call from __future__ import annotations from unittest import IsolatedAsyncioTestCase, mock import aiokafka from opentelemetry.instrumentation.aiokafka.utils import ( AIOKafkaContextGetter, AIOKafkaContextSetter, _aiokafka_getter, _aiokafka_setter, _create_consumer_span, _extract_send_partition, _get_span_name, _wrap_getmany, _wrap_getone, _wrap_send, ) from opentelemetry.trace import SpanKind class TestUtils(IsolatedAsyncioTestCase): def setUp(self) -> None: super().setUp() self.topic_name = "test_topic" self.args = [self.topic_name] self.headers = [] self.kwargs = {"partition": 0, "headers": self.headers} def test_context_setter(self) -> None: context_setter = AIOKafkaContextSetter() carrier_list = [("key1", b"val1")] context_setter.set(carrier_list, "key2", "val2") self.assertTrue(("key2", b"val2") in carrier_list) def test_context_getter(self) -> None: context_setter = AIOKafkaContextSetter() context_getter = AIOKafkaContextGetter() carrier_list = [] context_setter.set(carrier_list, "key1", "val1") self.assertEqual(context_getter.get(carrier_list, "key1"), ["val1"]) self.assertEqual(["key1"], context_getter.keys(carrier_list)) @mock.patch("opentelemetry.instrumentation.aiokafka.utils._extract_bootstrap_servers") @mock.patch("opentelemetry.instrumentation.aiokafka.utils._extract_send_partition") @mock.patch("opentelemetry.instrumentation.aiokafka.utils._enrich_send_span") @mock.patch("opentelemetry.trace.set_span_in_context") @mock.patch("opentelemetry.propagate.inject") async def test_wrap_send_with_topic_as_arg( self, inject: mock.MagicMock, set_span_in_context: mock.MagicMock, enrich_span: mock.MagicMock, extract_send_partition: mock.MagicMock, extract_bootstrap_servers: mock.MagicMock, ) -> None: await self.wrap_send_helper( inject, set_span_in_context, enrich_span, extract_send_partition, extract_bootstrap_servers, ) @mock.patch("opentelemetry.instrumentation.aiokafka.utils._extract_bootstrap_servers") @mock.patch("opentelemetry.instrumentation.aiokafka.utils._extract_send_partition") @mock.patch("opentelemetry.instrumentation.aiokafka.utils._enrich_send_span") @mock.patch("opentelemetry.trace.set_span_in_context") @mock.patch("opentelemetry.propagate.inject") async def test_wrap_send_with_topic_as_kwarg( self, inject: mock.MagicMock, set_span_in_context: mock.MagicMock, enrich_span: mock.MagicMock, extract_send_partition: mock.AsyncMock, extract_bootstrap_servers: mock.MagicMock, ) -> None: self.args = [] self.kwargs["topic"] = self.topic_name await self.wrap_send_helper( inject, set_span_in_context, enrich_span, extract_send_partition, extract_bootstrap_servers, ) async def wrap_send_helper( self, inject: mock.MagicMock, set_span_in_context: mock.MagicMock, enrich_span: mock.MagicMock, extract_send_partition: mock.AsyncMock, extract_bootstrap_servers: mock.MagicMock, ) -> None: tracer = mock.MagicMock() produce_hook = mock.AsyncMock() original_send_callback = mock.AsyncMock() kafka_producer = mock.MagicMock() expected_span_name = _get_span_name("send", self.topic_name) wrapped_send = _wrap_send(tracer, produce_hook) retval = await wrapped_send(original_send_callback, kafka_producer, self.args, self.kwargs) extract_bootstrap_servers.assert_called_once_with(kafka_producer.client) extract_send_partition.assert_awaited_once_with(kafka_producer, self.args, self.kwargs) tracer.start_as_current_span.assert_called_once_with(expected_span_name, kind=SpanKind.PRODUCER) span = tracer.start_as_current_span().__enter__.return_value enrich_span.assert_called_once_with( span, bootstrap_servers=extract_bootstrap_servers.return_value, client_id=kafka_producer.client._client_id, topic=self.topic_name, partition=extract_send_partition.return_value, key=None, ) set_span_in_context.assert_called_once_with(span) context = set_span_in_context.return_value inject.assert_called_once_with(self.headers, context=context, setter=_aiokafka_setter) produce_hook.assert_awaited_once_with(span, self.args, self.kwargs) original_send_callback.assert_awaited_once_with(*self.args, **self.kwargs) self.assertEqual(retval, original_send_callback.return_value) @mock.patch("opentelemetry.propagate.extract") @mock.patch("opentelemetry.instrumentation.aiokafka.utils._create_consumer_span") @mock.patch("opentelemetry.instrumentation.aiokafka.utils._extract_bootstrap_servers") @mock.patch("opentelemetry.instrumentation.aiokafka.utils._extract_client_id") @mock.patch("opentelemetry.instrumentation.aiokafka.utils._extract_consumer_group") async def test_wrap_getone( self, extract_consumer_group: mock.MagicMock, extract_client_id: mock.MagicMock, extract_bootstrap_servers: mock.MagicMock, _create_consumer_span: mock.MagicMock, extract: mock.MagicMock, ) -> None: tracer = mock.MagicMock() consume_hook = mock.AsyncMock() original_getone_callback = mock.AsyncMock() kafka_consumer = mock.MagicMock() wrapped_getone = _wrap_getone(tracer, consume_hook) record = await wrapped_getone(original_getone_callback, kafka_consumer, self.args, self.kwargs) extract_bootstrap_servers.assert_called_once_with(kafka_consumer._client) bootstrap_servers = extract_bootstrap_servers.return_value extract_client_id.assert_called_once_with(kafka_consumer._client) client_id = extract_client_id.return_value extract_consumer_group.assert_called_once_with(kafka_consumer) consumer_group = extract_consumer_group.return_value original_getone_callback.assert_awaited_once_with(*self.args, **self.kwargs) self.assertEqual(record, original_getone_callback.return_value) extract.assert_called_once_with(record.headers, getter=_aiokafka_getter) context = extract.return_value _create_consumer_span.assert_called_once_with( tracer, consume_hook, record, context, bootstrap_servers, client_id, consumer_group, self.args, self.kwargs, ) @mock.patch("opentelemetry.propagate.extract") @mock.patch("opentelemetry.instrumentation.aiokafka.utils._create_consumer_span") @mock.patch("opentelemetry.instrumentation.aiokafka.utils._enrich_getmany_topic_span") @mock.patch("opentelemetry.instrumentation.aiokafka.utils._enrich_getmany_poll_span") @mock.patch("opentelemetry.instrumentation.aiokafka.utils._extract_bootstrap_servers") @mock.patch("opentelemetry.instrumentation.aiokafka.utils._extract_client_id") @mock.patch("opentelemetry.instrumentation.aiokafka.utils._extract_consumer_group") # pylint: disable=too-many-locals async def test_wrap_getmany( self, extract_consumer_group: mock.MagicMock, extract_client_id: mock.MagicMock, extract_bootstrap_servers: mock.MagicMock, _enrich_getmany_poll_span: mock.MagicMock, _enrich_getmany_topic_span: mock.MagicMock, _create_consumer_span: mock.AsyncMock, extract: mock.MagicMock, ) -> None: tracer = mock.MagicMock() consume_hook = mock.AsyncMock() record_mock = mock.MagicMock() original_getmany_callback = mock.AsyncMock( return_value={aiokafka.TopicPartition(topic="topic_1", partition=0): [record_mock]} ) kafka_consumer = mock.MagicMock() _create_consumer_span.return_value = mock.MagicMock() wrapped_getmany = _wrap_getmany(tracer, consume_hook) records = await wrapped_getmany(original_getmany_callback, kafka_consumer, self.args, self.kwargs) extract_bootstrap_servers.assert_called_once_with(kafka_consumer._client) bootstrap_servers = extract_bootstrap_servers.return_value extract_client_id.assert_called_once_with(kafka_consumer._client) client_id = extract_client_id.return_value extract_consumer_group.assert_called_once_with(kafka_consumer) consumer_group = extract_consumer_group.return_value original_getmany_callback.assert_awaited_once_with(*self.args, **self.kwargs) self.assertEqual(records, original_getmany_callback.return_value) extract.assert_called_once_with(record_mock.headers, getter=_aiokafka_getter) context = extract.return_value _create_consumer_span.assert_called_once_with( tracer, consume_hook, record_mock, context, bootstrap_servers, client_id, consumer_group, self.args, self.kwargs, ) @mock.patch("opentelemetry.trace.set_span_in_context") @mock.patch("opentelemetry.context.attach") @mock.patch("opentelemetry.instrumentation.aiokafka.utils._enrich_getone_span") @mock.patch("opentelemetry.context.detach") async def test_create_consumer_span( self, detach: mock.MagicMock, enrich_span: mock.MagicMock, attach: mock.MagicMock, set_span_in_context: mock.MagicMock, ) -> None: tracer = mock.MagicMock() consume_hook = mock.AsyncMock() bootstrap_servers = mock.MagicMock() extracted_context = mock.MagicMock() record = mock.MagicMock() client_id = mock.MagicMock() consumer_group = mock.MagicMock() await _create_consumer_span( tracer, consume_hook, record, extracted_context, bootstrap_servers, client_id, consumer_group, self.args, self.kwargs, ) expected_span_name = _get_span_name("receive", record.topic) tracer.start_as_current_span.assert_called_once_with( expected_span_name, context=extracted_context, kind=SpanKind.CONSUMER, ) span = tracer.start_as_current_span.return_value.__enter__() set_span_in_context.assert_called_once_with(span, extracted_context) attach.assert_called_once_with(set_span_in_context.return_value) enrich_span.assert_called_once_with( span, bootstrap_servers=bootstrap_servers, client_id=client_id, consumer_group=consumer_group, topic=record.topic, partition=record.partition, key=str(record.key), offset=record.offset, ) consume_hook.assert_awaited_once_with(span, record, self.args, self.kwargs) detach.assert_called_once_with(attach.return_value) async def test_kafka_properties_extractor(self): aiokafka_instance_mock = mock.Mock() aiokafka_instance_mock._key_serializer = None aiokafka_instance_mock._value_serializer = None aiokafka_instance_mock._partition.return_value = "partition" aiokafka_instance_mock.client._wait_on_metadata = mock.AsyncMock() assert await _extract_send_partition(aiokafka_instance_mock, self.args, self.kwargs) == "partition" aiokafka_instance_mock.client._wait_on_metadata.side_effect = Exception("mocked error") assert await _extract_send_partition(aiokafka_instance_mock, self.args, self.kwargs) is None opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopg/000077500000000000000000000000001527070072300322425ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopg/LICENSE000066400000000000000000000261351527070072300332560ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopg/README.rst000066400000000000000000000012201527070072300337240ustar00rootroot00000000000000OpenTelemetry aiopg instrumentation =================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-aiopg.svg :target: https://pypi.org/project/opentelemetry-instrumentation-aiopg/ Installation ------------ :: pip install opentelemetry-instrumentation-aiopg References ---------- * `OpenTelemetry aiopg Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031631527070072300351020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopg[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-aiopg" dynamic = ["version"] description = "OpenTelemetry aiopg instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-instrumentation-dbapi == 0.66b2", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [ "aiopg >= 0.13.0, < 2.0.0", ] [project.entry-points.opentelemetry_instrumentor] aiopg = "opentelemetry.instrumentation.aiopg:AiopgInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-aiopg" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/aiopg/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopg/src/000077500000000000000000000000001527070072300330315ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300356465ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopg/srcinstrumentation/000077500000000000000000000000001527070072300411115ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetryaiopg/000077500000000000000000000000001527070072300422105ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation__init__.py000066400000000000000000000112371527070072300443250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The integration with PostgreSQL supports the aiopg library, it can be enabled by using ``AiopgInstrumentor``. .. aiopg: https://github.com/aio-libs/aiopg Usage ----- .. code-block:: python import asyncio import aiopg from opentelemetry.instrumentation.aiopg import AiopgInstrumentor # Call instrument() to wrap all database connections AiopgInstrumentor().instrument() dsn = "user=user password=password host=127.0.0.1" async def connect(): cnx = await aiopg.connect(dsn) cursor = await cnx.cursor() await cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)") await cursor.execute("INSERT INTO test (testField) VALUES (123)") cursor.close() cnx.close() async def create_pool(): pool = await aiopg.create_pool(dsn) cnx = await pool.acquire() cursor = await cnx.cursor() await cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)") await cursor.execute("INSERT INTO test (testField) VALUES (123)") cursor.close() cnx.close() asyncio.run(connect()) asyncio.run(create_pool()) .. code-block:: python import asyncio import aiopg from opentelemetry.instrumentation.aiopg import AiopgInstrumentor dsn = "user=user password=password host=127.0.0.1" # Alternatively, use instrument_connection for an individual connection async def go(): cnx = await aiopg.connect(dsn) instrumented_cnx = AiopgInstrumentor().instrument_connection(cnx) cursor = await instrumented_cnx.cursor() await cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)") await cursor.execute("INSERT INTO test (testField) VALUES (123)") cursor.close() instrumented_cnx.close() asyncio.run(go()) API --- """ from collections.abc import Collection from opentelemetry.instrumentation.aiopg import wrappers from opentelemetry.instrumentation.aiopg.package import _instruments from opentelemetry.instrumentation.aiopg.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor class AiopgInstrumentor(BaseInstrumentor): _CONNECTION_ATTRIBUTES = { "database": "info.dbname", "port": "info.port", "host": "info.host", "user": "info.user", } _DATABASE_SYSTEM = "postgresql" def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): """Integrate with PostgreSQL aiopg library. aiopg: https://github.com/aio-libs/aiopg """ tracer_provider = kwargs.get("tracer_provider") meter_provider = kwargs.get("meter_provider") wrappers.wrap_connect( __name__, self._DATABASE_SYSTEM, self._CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, meter_provider=meter_provider, ) wrappers.wrap_create_pool( __name__, self._DATABASE_SYSTEM, self._CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, meter_provider=meter_provider, ) # pylint:disable=no-self-use def _uninstrument(self, **kwargs): """ "Disable aiopg instrumentation""" wrappers.unwrap_connect() wrappers.unwrap_create_pool() # pylint:disable=no-self-use def instrument_connection(self, connection, tracer_provider=None, meter_provider=None): """Enable instrumentation in a aiopg connection. Args: connection: The connection to instrument. tracer_provider: The optional tracer provider to use. If omitted the current globally configured one is used. meter_provider: The optional meter provider to use. If omitted the current globally configured one is used. Returns: An instrumented connection. """ return wrappers.instrument_connection( __name__, connection, self._DATABASE_SYSTEM, self._CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, meter_provider=meter_provider, ) def uninstrument_connection(self, connection): """Disable instrumentation in a aiopg connection. Args: connection: The connection to uninstrument. Returns: An uninstrumented connection. """ return wrappers.uninstrument_connection(connection) aiopg_integration.py000066400000000000000000000166351527070072300462770ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import inspect import time import typing from collections.abc import Coroutine import wrapt from opentelemetry.instrumentation._semconv import _report_new from opentelemetry.instrumentation.dbapi import ( CursorTracer, DatabaseApiIntegration, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.trace import SpanKind # pylint: disable=abstract-method,no-member class AsyncProxyObject(wrapt.ObjectProxy): def __aiter__(self): return self.__wrapped__.__aiter__() async def __anext__(self): result = await self.__wrapped__.__anext__() return result async def __aenter__(self): return await self.__wrapped__.__aenter__() async def __aexit__(self, exc_type, exc_val, exc_tb): return await self.__wrapped__.__aexit__(exc_type, exc_val, exc_tb) def __await__(self): return self.__wrapped__.__await__() class AiopgIntegration(DatabaseApiIntegration): async def wrapped_connection( self, connect_method: typing.Callable[..., typing.Any], args: tuple[typing.Any, typing.Any], kwargs: dict[typing.Any, typing.Any], ): """Add object proxy to connection object.""" connection = await connect_method(*args, **kwargs) # pylint: disable=protected-access self.get_connection_attributes(connection._conn) return get_traced_connection_proxy(connection, self) async def wrapped_pool(self, create_pool_method, args, kwargs): pool = await create_pool_method(*args, **kwargs) async with pool.acquire() as connection: # pylint: disable=protected-access self.get_connection_attributes(connection._conn) return get_traced_pool_proxy(pool, self) def get_traced_connection_proxy(connection, db_api_integration, *args, **kwargs): # pylint: disable=abstract-method,no-member class TracedConnectionProxy(AsyncProxyObject): # pylint: disable=unused-argument def __init__(self, connection, *args, **kwargs): super().__init__(connection) def cursor(self, *args, **kwargs): coro = self._cursor(*args, **kwargs) return _ContextManager(coro) async def _cursor(self, *args, **kwargs): # pylint: disable=protected-access cursor = await self.__wrapped__._cursor(*args, **kwargs) return get_traced_cursor_proxy(cursor, db_api_integration) return TracedConnectionProxy(connection, *args, **kwargs) def get_traced_pool_proxy(pool, db_api_integration, *args, **kwargs): # pylint: disable=abstract-method,no-member class TracedPoolProxy(AsyncProxyObject): # pylint: disable=unused-argument def __init__(self, pool, *args, **kwargs): super().__init__(pool) def acquire(self): """Acquire free connection from the pool.""" coro = self._acquire() return _PoolAcquireContextManager(coro, self) async def _acquire(self): # pylint: disable=protected-access connection = await self.__wrapped__._acquire() if not isinstance(connection, AsyncProxyObject): connection = get_traced_connection_proxy(connection, db_api_integration, *args, **kwargs) return connection return TracedPoolProxy(pool, *args, **kwargs) class AsyncCursorTracer(CursorTracer): async def traced_execution( self, cursor, query_method: typing.Callable[..., typing.Any], *args: tuple[typing.Any, typing.Any], **kwargs: dict[typing.Any, typing.Any], ): operation_name = self.get_operation_name(cursor, args) span_name = operation_name if not span_name: span_name = ( self._db_api_integration.database if self._db_api_integration.database else self._db_api_integration.name ) with self._db_api_integration._tracer.start_as_current_span(span_name, kind=SpanKind.CLIENT) as span: if span.is_recording(): self._populate_span(span, cursor, *args) start_time = time.perf_counter() error = None try: return await query_method(*args, **kwargs) except Exception as exc: error = exc if span.is_recording() and _report_new(self._db_api_integration._sem_conv_opt_in_mode_db): span.set_attribute(ERROR_TYPE, type(exc).__qualname__) raise finally: self._record_metrics(cursor, operation_name, start_time, error) def get_traced_cursor_proxy(cursor, db_api_integration, *args, **kwargs): _traced_cursor = AsyncCursorTracer(db_api_integration) # pylint: disable=abstract-method,no-member class AsyncCursorTracerProxy(AsyncProxyObject): # pylint: disable=unused-argument def __init__(self, cursor, *args, **kwargs): super().__init__(cursor) async def execute(self, *args, **kwargs): result = await _traced_cursor.traced_execution(self, self.__wrapped__.execute, *args, **kwargs) return result async def executemany(self, *args, **kwargs): result = await _traced_cursor.traced_execution(self, self.__wrapped__.executemany, *args, **kwargs) return result async def callproc(self, *args, **kwargs): result = await _traced_cursor.traced_execution(self, self.__wrapped__.callproc, *args, **kwargs) return result return AsyncCursorTracerProxy(cursor, *args, **kwargs) class _ContextManager(Coroutine): __slots__ = ("_coro", "_obj") def __init__(self, coro): self._coro = coro self._obj = None def send(self, value): return self._coro.send(value) def throw(self, typ, val=None, tb=None): if val is None: return self._coro.throw(typ) if tb is None: return self._coro.throw(typ, val) return self._coro.throw(typ, val, tb) def close(self): return self._coro.close() @property def gi_frame(self): return self._coro.gi_frame @property def gi_running(self): return self._coro.gi_running @property def gi_code(self): return self._coro.gi_code def __next__(self): return self.send(None) def __await__(self): resp = self._coro.__await__() return resp async def __aenter__(self): self._obj = await self._coro return self._obj async def __aexit__(self, exc_type, exc, t_b): try: if inspect.iscoroutinefunction(self._obj.close): await self._obj.close() else: self._obj.close() finally: self._obj = None class _PoolContextManager(_ContextManager): __slots__ = () async def __aexit__(self, exc_type, exc, tb): self._obj.close() await self._obj.wait_closed() self._obj = None class _PoolAcquireContextManager(_ContextManager): # pylint: disable=redefined-slots-in-subclass __slots__ = ("_coro", "_obj", "_pool") def __init__(self, coro, pool): super().__init__(coro) self._pool = pool async def __aexit__(self, exc_type, exc, tb): await self._pool.release(self._obj) self._pool = None self._obj = None package.py000066400000000000000000000002631527070072300441560ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("aiopg >= 0.13.0, < 2.0.0",) _supports_metrics = True _semconv_status = "migration" version.py000066400000000000000000000001441527070072300442460ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" wrappers.py000066400000000000000000000156361527070072300444400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The trace integration with aiopg based on dbapi integration, where replaced sync wrap methods to async Usage ----- .. code-block:: python from opentelemetry import trace from opentelemetry.instrumentation.aiopg import trace_integration trace_integration(aiopg.connection, "_connect", "postgresql") API --- """ import logging import typing import aiopg import wrapt from opentelemetry.instrumentation.aiopg.aiopg_integration import ( AiopgIntegration, AsyncProxyObject, _ContextManager, _PoolContextManager, get_traced_connection_proxy, ) from opentelemetry.instrumentation.aiopg.version import __version__ from opentelemetry.instrumentation.utils import unwrap from opentelemetry.metrics import MeterProvider from opentelemetry.trace import TracerProvider logger = logging.getLogger(__name__) def trace_integration( database_system: str, connection_attributes: dict = None, tracer_provider: TracerProvider | None = None, meter_provider: MeterProvider | None = None, ): """Integrate with aiopg library. based on dbapi integration, where replaced sync wrap methods to async Args: database_system: An identifier for the database management system (DBMS) product being used. connection_attributes: Attribute names for database, port, host and user in Connection object. tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to use. If omitted the current configured one is used. meter_provider: The :class:`opentelemetry.metrics.MeterProvider` to use. If omitted the current configured one is used. """ wrap_connect( __name__, database_system, connection_attributes, __version__, tracer_provider, meter_provider, ) def wrap_connect( name: str, database_system: str, connection_attributes: dict = None, version: str = "", tracer_provider: TracerProvider | None = None, meter_provider: MeterProvider | None = None, ): """Integrate with aiopg library. https://github.com/aio-libs/aiopg Args: name: Name of opentelemetry extension for aiopg. database_system: An identifier for the database management system (DBMS) product being used. connection_attributes: Attribute names for database, port, host and user in Connection object. version: Version of opentelemetry extension for aiopg. tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to use. If omitted the current configured one is used. meter_provider: The :class:`opentelemetry.metrics.MeterProvider` to use. If omitted the current configured one is used. """ # pylint: disable=unused-argument def wrap_connect_( wrapped: typing.Callable[..., typing.Any], instance: typing.Any, args: tuple[typing.Any, typing.Any], kwargs: dict[typing.Any, typing.Any], ): db_integration = AiopgIntegration( name, database_system, connection_attributes=connection_attributes, version=version, tracer_provider=tracer_provider, meter_provider=meter_provider, ) return _ContextManager( # pylint: disable=no-value-for-parameter db_integration.wrapped_connection(wrapped, args, kwargs) ) try: wrapt.wrap_function_wrapper(aiopg, "connect", wrap_connect_) except Exception as ex: # pylint: disable=broad-except logger.warning("Failed to integrate with aiopg. %s", str(ex)) def unwrap_connect(): """Disable integration with aiopg library. https://github.com/aio-libs/aiopg """ unwrap(aiopg, "connect") def instrument_connection( name: str, connection, database_system: str, connection_attributes: dict = None, version: str = "", tracer_provider: TracerProvider | None = None, meter_provider: MeterProvider | None = None, ): """Enable instrumentation in a database connection. Args: name: Name of opentelemetry extension for aiopg. connection: The connection to instrument. database_system: An identifier for the database management system (DBMS) product being used. connection_attributes: Attribute names for database, port, host and user in a connection object. version: Version of opentelemetry extension for aiopg. tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to use. If omitted the current configured one is used. meter_provider: The :class:`opentelemetry.metrics.MeterProvider` to use. If omitted the current configured one is used. Returns: An instrumented connection. """ if isinstance(connection, AsyncProxyObject): logger.warning("Connection already instrumented") return connection db_integration = AiopgIntegration( name, database_system, connection_attributes=connection_attributes, version=version, tracer_provider=tracer_provider, meter_provider=meter_provider, ) db_integration.get_connection_attributes(getattr(connection, "_conn", connection)) return get_traced_connection_proxy(connection, db_integration) def uninstrument_connection(connection): """Disable instrumentation in a database connection. Args: connection: The connection to uninstrument. Returns: An uninstrumented connection. """ if isinstance(connection, AsyncProxyObject): return connection.__wrapped__ logger.warning("Connection is not instrumented") return connection def wrap_create_pool( name: str, database_system: str, connection_attributes: dict = None, version: str = "", tracer_provider: TracerProvider | None = None, meter_provider: MeterProvider | None = None, ): # pylint: disable=unused-argument def wrap_create_pool_( wrapped: typing.Callable[..., typing.Any], instance: typing.Any, args: tuple[typing.Any, typing.Any], kwargs: dict[typing.Any, typing.Any], ): db_integration = AiopgIntegration( name, database_system, connection_attributes=connection_attributes, version=version, tracer_provider=tracer_provider, meter_provider=meter_provider, ) return _PoolContextManager(db_integration.wrapped_pool(wrapped, args, kwargs)) try: wrapt.wrap_function_wrapper(aiopg, "create_pool", wrap_create_pool_) except Exception as ex: # pylint: disable=broad-except logger.warning("Failed to integrate with DB API. %s", str(ex)) def unwrap_create_pool(): """Disable integration with aiopg library. https://github.com/aio-libs/aiopg """ unwrap(aiopg, "create_pool") test-requirements-wrapt1.txt000066400000000000000000000000471527070072300376410ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopg-r test-requirements.txt wrapt==1.17.3 test-requirements-wrapt2.txt000066400000000000000000000000461527070072300376410ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopg-r test-requirements.txt wrapt==2.1.2 test-requirements.txt000066400000000000000000000005331527070072300364250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopgaiopg==1.4.0 asgiref==3.8.1 async-timeout==4.0.3 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 psycopg2-binary==2.9.10 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-dbapi -e instrumentation/opentelemetry-instrumentation-aiopg opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopg/tests/000077500000000000000000000000001527070072300334045ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300354240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopg/teststest_aiopg_integration.py000066400000000000000000001055121527070072300404440ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aiopg/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio import contextlib import logging from types import SimpleNamespace from unittest import mock from unittest.mock import MagicMock import aiopg import psycopg2 import opentelemetry.instrumentation.aiopg from opentelemetry import trace as trace_api from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.aiopg import AiopgInstrumentor, wrappers from opentelemetry.instrumentation.aiopg.aiopg_integration import ( AiopgIntegration, _ContextManager, _PoolAcquireContextManager, ) from opentelemetry.sdk import resources from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_STATEMENT, DB_SYSTEM, DB_USER, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv._incubating.metrics.db_metrics import ( DB_CLIENT_OPERATION_DURATION, DB_CLIENT_RESPONSE_RETURNED_ROWS, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_NAMESPACE, DB_OPERATION_NAME, DB_QUERY_TEXT, DB_SYSTEM_NAME, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.test.test_base import TestBase def async_call(coro): return asyncio.run(coro) @contextlib.contextmanager def use_semconv_opt_in(sem_conv_mode): env_patch = mock.patch.dict( "os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode}, ) _OpenTelemetrySemanticConventionStability._initialized = False env_patch.start() try: yield finally: env_patch.stop() _OpenTelemetrySemanticConventionStability._initialized = False class TestAiopgInstrumentor(TestBase): def setUp(self): super().setUp() _OpenTelemetrySemanticConventionStability._initialized = False self.origin_aiopg_connect = aiopg.connect self.origin_aiopg_create_pool = aiopg.create_pool aiopg.connect = mock_connect aiopg.create_pool = mock_create_pool def tearDown(self): _OpenTelemetrySemanticConventionStability._initialized = False super().tearDown() aiopg.connect = self.origin_aiopg_connect aiopg.create_pool = self.origin_aiopg_create_pool with self.disable_logging(): AiopgInstrumentor().uninstrument() def test_instrumentor_connect(self): AiopgInstrumentor().instrument() cnx = async_call(aiopg.connect(database="test")) cursor = async_call(cnx.cursor()) query = "SELECT * FROM test" async_call(cursor.execute(query)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.aiopg) # check that no spans are generated after uninstrument AiopgInstrumentor().uninstrument() cnx = async_call(aiopg.connect(database="test")) cursor = async_call(cnx.cursor()) query = "SELECT * FROM test" async_call(cursor.execute(query)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) def test_instrumentor_connect_new_semconv(self): with use_semconv_opt_in("database,http"): AiopgInstrumentor().instrument() cnx = async_call( aiopg.connect( database="testdatabase", server_host="testhost", server_port=123, ) ) cursor = async_call(cnx.cursor()) async_call(cursor.execute("SELECT * FROM test")) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_SYSTEM_NAME], "postgresql") self.assertEqual(span.attributes[DB_NAMESPACE], "testdatabase") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT * FROM test") self.assertEqual(span.attributes[SERVER_ADDRESS], "testhost") self.assertEqual(span.attributes[SERVER_PORT], 123) self.assertNotIn(DB_SYSTEM, span.attributes) self.assertNotIn(DB_NAME, span.attributes) self.assertNotIn(DB_STATEMENT, span.attributes) self.assertNotIn(NET_PEER_NAME, span.attributes) self.assertNotIn(NET_PEER_PORT, span.attributes) def test_instrumentor_connect_ctx_manager(self): async def _ctx_manager_connect(): AiopgInstrumentor().instrument() async with aiopg.connect(database="test") as cnx: async with cnx.cursor() as cursor: query = "SELECT * FROM test" await cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.aiopg) async_call(_ctx_manager_connect()) def test_instrumentor_create_pool(self): AiopgInstrumentor().instrument() pool = async_call(aiopg.create_pool(database="test")) cnx = async_call(pool.acquire()) cursor = async_call(cnx.cursor()) query = "SELECT * FROM test" async_call(cursor.execute(query)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.aiopg) # check that no spans are generated after uninstrument AiopgInstrumentor().uninstrument() pool = async_call(aiopg.create_pool(database="test")) cnx = async_call(pool.acquire()) cursor = async_call(cnx.cursor()) query = "SELECT * FROM test" async_call(cursor.execute(query)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) def test_instrumentor_create_pool_ctx_manager(self): async def _ctx_manager_pool(): AiopgInstrumentor().instrument() async with aiopg.create_pool(database="test") as pool: async with pool.acquire() as cnx: async with cnx.cursor() as cursor: query = "SELECT * FROM test" await cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.aiopg) async_call(_ctx_manager_pool()) def test_custom_tracer_provider_connect(self): resource = resources.Resource.create({}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result AiopgInstrumentor().instrument(tracer_provider=tracer_provider) cnx = async_call(aiopg.connect(database="test")) cursor = async_call(cnx.cursor()) query = "SELECT * FROM test" async_call(cursor.execute(query)) spans_list = exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertIs(span.resource, resource) def test_custom_tracer_provider_create_pool(self): resource = resources.Resource.create({}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result AiopgInstrumentor().instrument(tracer_provider=tracer_provider) pool = async_call(aiopg.create_pool(database="test")) cnx = async_call(pool.acquire()) cursor = async_call(cnx.cursor()) query = "SELECT * FROM test" async_call(cursor.execute(query)) spans_list = exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertIs(span.resource, resource) @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "database"}) def test_custom_meter_provider_connect(self): meter_provider, metrics_reader = self.create_meter_provider() AiopgInstrumentor().instrument(meter_provider=meter_provider) cnx = async_call(aiopg.connect(database="test")) cursor = async_call(cnx.cursor()) async_call(cursor.execute("SELECT * FROM test", rowcount=3)) metric_names = { metric.name for resource_metrics in metrics_reader.get_metrics_data().resource_metrics for scope_metrics in resource_metrics.scope_metrics for metric in scope_metrics.metrics } self.assertIn(DB_CLIENT_OPERATION_DURATION, metric_names) self.assertIn(DB_CLIENT_RESPONSE_RETURNED_ROWS, metric_names) def test_instrument_connection(self): cnx = async_call( aiopg.connect( database="testdatabase", server_host="testhost", server_port=123, user="testuser", ) ) query = "SELECT * FROM test" cursor = async_call(cnx.cursor()) async_call(cursor.execute(query)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) cnx = AiopgInstrumentor().instrument_connection(cnx) cursor = async_call(cnx.cursor()) async_call(cursor.execute(query)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_NAME], "testdatabase") self.assertEqual(span.attributes[DB_USER], "testuser") self.assertEqual(span.attributes[NET_PEER_NAME], "testhost") self.assertEqual(span.attributes[NET_PEER_PORT], 123) def test_instrument_connection_new_semconv(self): with use_semconv_opt_in("database,http"): cnx = async_call( aiopg.connect( database="testdatabase", server_host="testhost", server_port=123, ) ) cnx = AiopgInstrumentor().instrument_connection(cnx) cursor = async_call(cnx.cursor()) async_call(cursor.execute("SELECT * FROM test")) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_SYSTEM_NAME], "postgresql") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT * FROM test") self.assertNotIn(DB_SYSTEM, span.attributes) self.assertNotIn(DB_STATEMENT, span.attributes) def test_instrument_connection_after_instrument(self): cnx = async_call(aiopg.connect(database="test")) query = "SELECT * FROM test" cursor = async_call(cnx.cursor()) async_call(cursor.execute(query)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) AiopgInstrumentor().instrument() cnx = AiopgInstrumentor().instrument_connection(cnx) cursor = async_call(cnx.cursor()) async_call(cursor.execute(query)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) def test_no_op_tracer_provider(self): cnx = async_call(aiopg.connect(database="test")) AiopgInstrumentor().instrument_connection(cnx, tracer_provider=trace_api.NoOpTracerProvider()) cursor = async_call(cnx.cursor()) query = "SELECT * FROM test" async_call(cursor.execute(query)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) def test_custom_tracer_provider_instrument_connection(self): resource = resources.Resource.create({"service.name": "db-test-service"}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result cnx = async_call(aiopg.connect(database="test")) cnx = AiopgInstrumentor().instrument_connection(cnx, tracer_provider=tracer_provider) cursor = async_call(cnx.cursor()) query = "SELECT * FROM test" async_call(cursor.execute(query)) spans_list = exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.resource.attributes["service.name"], "db-test-service") self.assertIs(span.resource, resource) def test_uninstrument_connection(self): AiopgInstrumentor().instrument() cnx = async_call(aiopg.connect(database="test")) query = "SELECT * FROM test" cursor = async_call(cnx.cursor()) async_call(cursor.execute(query)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) cnx = AiopgInstrumentor().uninstrument_connection(cnx) cursor = async_call(cnx.cursor()) async_call(cursor.execute(query)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) class TestAiopgIntegration(TestBase): def setUp(self): super().setUp() _OpenTelemetrySemanticConventionStability._initialized = False self.tracer = self.tracer_provider.get_tracer(__name__) def tearDown(self): _OpenTelemetrySemanticConventionStability._initialized = False super().tearDown() def test_span_succeeded(self): connection_props = { "database": "testdatabase", "server_host": "testhost", "server_port": 123, "user": "testuser", } connection_attributes = { "database": "database", "port": "server_port", "host": "server_host", "user": "user", } db_integration = AiopgIntegration( self.tracer, "testcomponent", connection_attributes, capture_parameters=True, ) mock_connection = async_call(db_integration.wrapped_connection(mock_connect, {}, connection_props)) cursor = async_call(mock_connection.cursor()) async_call(cursor.execute("Test query", ("param1Value", False))) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "Test") self.assertIs(span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(span.attributes[DB_SYSTEM], "testcomponent") self.assertEqual(span.attributes[DB_NAME], "testdatabase") self.assertEqual(span.attributes[DB_STATEMENT], "Test query") self.assertEqual( span.attributes["db.statement.parameters"], "('param1Value', False)", ) self.assertEqual(span.attributes[DB_USER], "testuser") self.assertEqual(span.attributes[NET_PEER_NAME], "testhost") self.assertEqual(span.attributes[NET_PEER_PORT], 123) self.assertIs(span.status.status_code, trace_api.StatusCode.UNSET) def test_span_not_recording(self): connection_props = { "database": "testdatabase", "server_host": "testhost", "server_port": 123, "user": "testuser", } connection_attributes = { "database": "database", "port": "server_port", "host": "server_host", "user": "user", } db_integration = AiopgIntegration( __name__, "testcomponent", connection_attributes, tracer_provider=trace_api.NoOpTracerProvider(), ) mock_connection = async_call(db_integration.wrapped_connection(mock_connect, {}, connection_props)) cursor = async_call(mock_connection.cursor()) async_call(cursor.execute("Test query", ("param1Value", False))) self.assertEqual(len(self.memory_exporter.get_finished_spans()), 0) def test_span_succeeded_new_semconv(self): connection_props = { "database": "testdatabase", "server_host": "testhost", "server_port": 123, "user": "testuser", } connection_attributes = { "database": "database", "port": "server_port", "host": "server_host", "user": "user", } with use_semconv_opt_in("database,http"): db_integration = AiopgIntegration(__name__, "testcomponent", connection_attributes) mock_connection = async_call(db_integration.wrapped_connection(mock_connect, {}, connection_props)) cursor = async_call(mock_connection.cursor()) async_call(cursor.execute("Test query")) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.25.0", ) self.assertEqual(span.attributes[DB_SYSTEM_NAME], "testcomponent") self.assertEqual(span.attributes[DB_NAMESPACE], "testdatabase") self.assertEqual(span.attributes[DB_QUERY_TEXT], "Test query") self.assertEqual(span.attributes[SERVER_ADDRESS], "testhost") self.assertEqual(span.attributes[SERVER_PORT], 123) self.assertNotIn(DB_SYSTEM, span.attributes) self.assertNotIn(DB_NAME, span.attributes) self.assertNotIn(DB_STATEMENT, span.attributes) self.assertNotIn(DB_USER, span.attributes) self.assertNotIn(NET_PEER_NAME, span.attributes) self.assertNotIn(NET_PEER_PORT, span.attributes) def test_span_succeeded_both_semconv(self): connection_props = { "database": "testdatabase", "server_host": "testhost", "server_port": 123, "user": "testuser", } connection_attributes = { "database": "database", "port": "server_port", "host": "server_host", "user": "user", } with use_semconv_opt_in("database/dup,http/dup"): db_integration = AiopgIntegration(__name__, "testcomponent", connection_attributes) mock_connection = async_call(db_integration.wrapped_connection(mock_connect, {}, connection_props)) cursor = async_call(mock_connection.cursor()) async_call(cursor.execute("Test query")) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_SYSTEM], "testcomponent") self.assertEqual(span.attributes[DB_NAME], "testdatabase") self.assertEqual(span.attributes[DB_STATEMENT], "Test query") self.assertEqual(span.attributes[DB_USER], "testuser") self.assertEqual(span.attributes[NET_PEER_NAME], "testhost") self.assertEqual(span.attributes[NET_PEER_PORT], 123) self.assertEqual(span.attributes[DB_SYSTEM_NAME], "testcomponent") self.assertEqual(span.attributes[DB_NAMESPACE], "testdatabase") self.assertEqual(span.attributes[DB_QUERY_TEXT], "Test query") self.assertEqual(span.attributes[SERVER_ADDRESS], "testhost") self.assertEqual(span.attributes[SERVER_PORT], 123) def test_executemany_new_semconv(self): with use_semconv_opt_in("database"): db_integration = AiopgIntegration(__name__, "testcomponent") mock_connection = async_call(db_integration.wrapped_connection(mock_connect, {}, {})) cursor = async_call(mock_connection.cursor()) async_call(cursor.executemany("Test query")) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_SYSTEM_NAME], "testcomponent") self.assertEqual(span.attributes[DB_QUERY_TEXT], "Test query") self.assertNotIn(DB_SYSTEM, span.attributes) self.assertNotIn(DB_STATEMENT, span.attributes) def test_span_failed(self): db_integration = AiopgIntegration(self.tracer, "testcomponent") mock_connection = async_call(db_integration.wrapped_connection(mock_connect, {}, {})) cursor = async_call(mock_connection.cursor()) with self.assertRaises(Exception): async_call(cursor.execute("Test query", throw_exception=True)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_STATEMENT], "Test query") self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) self.assertEqual(span.status.description, "ProgrammingError: Test Exception") def test_span_failed_new_semconv(self): with use_semconv_opt_in("database"): db_integration = AiopgIntegration(__name__, "testcomponent") mock_connection = async_call(db_integration.wrapped_connection(mock_connect, {}, {})) cursor = async_call(mock_connection.cursor()) with self.assertRaises(psycopg2.ProgrammingError): async_call(cursor.execute("Test query", throw_exception=True)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_QUERY_TEXT], "Test query") self.assertEqual(span.attributes[ERROR_TYPE], "ProgrammingError") self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) def test_executemany(self): db_integration = AiopgIntegration(self.tracer, "testcomponent") mock_connection = async_call(db_integration.wrapped_connection(mock_connect, {}, {})) cursor = async_call(mock_connection.cursor()) async_call(cursor.executemany("Test query")) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_STATEMENT], "Test query") def test_callproc(self): db_integration = AiopgIntegration(self.tracer, "testcomponent") mock_connection = async_call(db_integration.wrapped_connection(mock_connect, {}, {})) cursor = async_call(mock_connection.cursor()) async_call(cursor.callproc("Test stored procedure")) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Test stored procedure", ) def test_wrap_connect(self): aiopg_mock = AiopgMock() with mock.patch("aiopg.connect", aiopg_mock.connect): wrappers.wrap_connect(self.tracer, "-") connection = async_call(aiopg.connect()) self.assertEqual(aiopg_mock.connect_call_count, 1) self.assertIsInstance(connection.__wrapped__, mock.Mock) def test_unwrap_connect(self): wrappers.wrap_connect(self.tracer, "-") aiopg_mock = AiopgMock() with mock.patch("aiopg.connect", aiopg_mock.connect): connection = async_call(aiopg.connect()) self.assertEqual(aiopg_mock.connect_call_count, 1) wrappers.unwrap_connect() connection = async_call(aiopg.connect()) self.assertEqual(aiopg_mock.connect_call_count, 2) self.assertIsInstance(connection, mock.Mock) def test_wrap_create_pool(self): async def check_connection(pool): async with pool.acquire() as connection: self.assertEqual(aiopg_mock.create_pool_call_count, 1) self.assertIsInstance(connection.__wrapped__, AiopgConnectionMock) aiopg_mock = AiopgMock() with mock.patch("aiopg.create_pool", aiopg_mock.create_pool): wrappers.wrap_create_pool(self.tracer, "-") pool = async_call(aiopg.create_pool()) async_call(check_connection(pool)) def test_unwrap_create_pool(self): async def check_connection(pool): async with pool.acquire() as connection: self.assertEqual(aiopg_mock.create_pool_call_count, 2) self.assertIsInstance(connection, AiopgConnectionMock) aiopg_mock = AiopgMock() with mock.patch("aiopg.create_pool", aiopg_mock.create_pool): wrappers.wrap_create_pool(self.tracer, "-") pool = async_call(aiopg.create_pool()) self.assertEqual(aiopg_mock.create_pool_call_count, 1) wrappers.unwrap_create_pool() pool = async_call(aiopg.create_pool()) async_call(check_connection(pool)) def test_instrument_connection(self): connection = mock.Mock() # Avoid get_attributes failing because can't concatenate mock connection.database = "-" connection._conn = connection connection2 = wrappers.instrument_connection(self.tracer, connection, "-") self.assertIs(connection2.__wrapped__, connection) # pylint: disable=no-member def test_uninstrument_connection(self): connection = mock.Mock() # Set connection.database to avoid a failure because mock can't # be concatenated connection.database = "-" connection._conn = connection connection2 = wrappers.instrument_connection(self.tracer, connection, "-") self.assertIs(connection2.__wrapped__, connection) # pylint: disable=no-member connection3 = wrappers.uninstrument_connection(connection2) self.assertIs(connection3, connection) with self.assertLogs(level=logging.WARNING): connection4 = wrappers.uninstrument_connection(connection) self.assertIs(connection4, connection) class TestAiopgMetrics(TestBase): def setUp(self): super().setUp() _OpenTelemetrySemanticConventionStability._initialized = False def tearDown(self): _OpenTelemetrySemanticConventionStability._initialized = False super().tearDown() def _get_metric(self, name): return next( (metric for metric in self.get_sorted_metrics() if metric.name == name), None, ) def test_metrics_not_emitted_in_default_mode(self): db_integration = AiopgIntegration(__name__, "testcomponent") mock_connection = async_call(db_integration.wrapped_connection(mock_connect, (), {})) cursor = async_call(mock_connection.cursor()) async_call(cursor.execute("SELECT 1", rowcount=3)) self.assertIsNone(self._get_metric(DB_CLIENT_OPERATION_DURATION)) self.assertIsNone(self._get_metric(DB_CLIENT_RESPONSE_RETURNED_ROWS)) @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "database"}) def test_operation_duration_recorded(self): connection_props = { "database": "testdatabase", "server_host": "testhost", "server_port": 123, "user": "testuser", } connection_attributes = { "database": "database", "port": "server_port", "host": "server_host", "user": "user", } db_integration = AiopgIntegration(__name__, "testcomponent", connection_attributes) mock_connection = async_call(db_integration.wrapped_connection(mock_connect, (), connection_props)) cursor = async_call(mock_connection.cursor()) async_call(cursor.execute("SELECT * FROM users")) duration_metric = self._get_metric(DB_CLIENT_OPERATION_DURATION) self.assertIsNotNone(duration_metric) self.assertEqual(duration_metric.unit, "s") points = list(duration_metric.data.data_points) self.assertEqual(len(points), 1) attributes = dict(points[0].attributes) self.assertEqual(attributes[DB_SYSTEM_NAME], "testcomponent") self.assertEqual(attributes[DB_NAMESPACE], "testdatabase") self.assertEqual(attributes[DB_OPERATION_NAME], "SELECT") self.assertEqual(attributes[SERVER_ADDRESS], "testhost") self.assertEqual(attributes[SERVER_PORT], 123) self.assertNotIn(ERROR_TYPE, attributes) self.assertEqual(points[0].count, 1) self.assertGreaterEqual(points[0].sum, 0.0) @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "database"}) def test_operation_duration_error_type(self): db_integration = AiopgIntegration(__name__, "testcomponent") mock_connection = async_call(db_integration.wrapped_connection(mock_connect, (), {})) cursor = async_call(mock_connection.cursor()) with self.assertRaises(psycopg2.ProgrammingError): async_call(cursor.execute("SELECT 1", throw_exception=True)) duration_metric = self._get_metric(DB_CLIENT_OPERATION_DURATION) self.assertIsNotNone(duration_metric) points = list(duration_metric.data.data_points) self.assertEqual(len(points), 1) attributes = dict(points[0].attributes) self.assertEqual(attributes[ERROR_TYPE], "ProgrammingError") self.assertEqual(attributes[DB_OPERATION_NAME], "SELECT") self.assertIsNone(self._get_metric(DB_CLIENT_RESPONSE_RETURNED_ROWS)) @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "database"}) def test_returned_rows_recorded_for_executemany(self): db_integration = AiopgIntegration(__name__, "testcomponent") mock_connection = async_call(db_integration.wrapped_connection(mock_connect, (), {})) cursor = async_call(mock_connection.cursor()) async_call(cursor.executemany("INSERT INTO users VALUES (1)", rowcount=3)) rows_metric = self._get_metric(DB_CLIENT_RESPONSE_RETURNED_ROWS) self.assertIsNotNone(rows_metric) self.assertEqual(rows_metric.unit, "{row}") points = list(rows_metric.data.data_points) self.assertEqual(len(points), 1) self.assertEqual(points[0].sum, 3) self.assertEqual(points[0].count, 1) @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "database"}) def test_returned_rows_skipped_when_rowcount_unknown(self): db_integration = AiopgIntegration(__name__, "testcomponent") mock_connection = async_call(db_integration.wrapped_connection(mock_connect, (), {})) cursor = async_call(mock_connection.cursor()) async_call(cursor.execute("CREATE TABLE users (id INT)")) self.assertIsNone(self._get_metric(DB_CLIENT_RESPONSE_RETURNED_ROWS)) self.assertIsNotNone(self._get_metric(DB_CLIENT_OPERATION_DURATION)) # pylint: disable=unused-argument async def mock_connect(*args, **kwargs): database = kwargs.get("database") server_host = kwargs.get("server_host") server_port = kwargs.get("server_port") user = kwargs.get("user") return MockConnection(database, server_port, server_host, user) # pylint: disable=unused-argument async def mock_create_pool(*args, **kwargs): database = kwargs.get("database") server_host = kwargs.get("server_host") server_port = kwargs.get("server_port") user = kwargs.get("user") return MockPool(database, server_port, server_host, user) class MockPool: def __init__(self, database, server_port, server_host, user): self.database = database self.server_port = server_port self.server_host = server_host self.user = user # pylint: disable=no-self-use async def release(self, conn): return conn def acquire(self): """Acquire free connection from the pool.""" coro = self._acquire() return _PoolAcquireContextManager(coro, self) async def _acquire(self): connect = await mock_connect(self.database, self.server_port, self.server_host, self.user) return connect def close(self): pass async def wait_closed(self): pass class MockPsycopg2Connection: def __init__(self, database, server_port, server_host, user): self.database = database self.server_port = server_port self.server_host = server_host self.user = user self.info = SimpleNamespace( dbname=database, port=server_port, host=server_host, user=user, ) class MockConnection: def __init__(self, database, server_port, server_host, user): self._conn = MockPsycopg2Connection(database, server_port, server_host, user) def cursor(self): coro = self._cursor() return _ContextManager(coro) # pylint: disable=no-value-for-parameter # pylint: disable=no-self-use async def _cursor(self): return MockCursor() def close(self): pass class MockCursor: def __init__(self): self.rowcount = -1 # pylint: disable=unused-argument async def execute(self, query, params=None, throw_exception=False, rowcount=-1): self.rowcount = rowcount if throw_exception: raise psycopg2.ProgrammingError("Test Exception") # pylint: disable=unused-argument async def executemany(self, query, params=None, throw_exception=False, rowcount=-1): self.rowcount = rowcount if throw_exception: raise psycopg2.ProgrammingError("Test Exception") # pylint: disable=unused-argument async def callproc(self, query, params=None, throw_exception=False, rowcount=-1): self.rowcount = rowcount if throw_exception: raise psycopg2.ProgrammingError("Test Exception") def close(self): pass class AiopgConnectionMock: _conn = MagicMock() async def __aexit__(self, exc_type, exc_val, exc_tb): pass async def __aenter__(self): return MagicMock() class AiopgPoolMock: # pylint: disable=no-self-use async def release(self, conn): return conn def acquire(self): coro = self._acquire() return _PoolAcquireContextManager(coro, self) # pylint: disable=no-self-use async def _acquire(self): return AiopgConnectionMock() class AiopgMock: def __init__(self): self.connect_call_count = 0 self.create_pool_call_count = 0 async def connect(self, *args, **kwargs): self.connect_call_count += 1 return MagicMock() async def create_pool(self, *args, **kwargs): self.create_pool_call_count += 1 return AiopgPoolMock() opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asgi/000077500000000000000000000000001527070072300320665ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asgi/LICENSE000066400000000000000000000261351527070072300331020ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asgi/README.rst000066400000000000000000000012571527070072300335620ustar00rootroot00000000000000OpenTelemetry ASGI Instrumentation ================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-asgi.svg :target: https://pypi.org/project/opentelemetry-instrumentation-asgi/ This library provides a ASGI middleware that can be used on any ASGI framework (such as Django, Starlette, FastAPI or Quart) to track requests timing through OpenTelemetry. Installation ------------ :: pip install opentelemetry-instrumentation-asgi References ---------- * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000030231527070072300347210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asgi[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-asgi" dynamic = ["version"] description = "ASGI instrumentation for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "asgiref ~= 3.0", "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-util-http == 0.66b2", ] [project.optional-dependencies] instruments = [ "asgiref ~= 3.0", ] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-asgi" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/asgi/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asgi/src/000077500000000000000000000000001527070072300326555ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300354725ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asgi/srcinstrumentation/000077500000000000000000000000001527070072300407355ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetryasgi/000077500000000000000000000000001527070072300416605ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation__init__.py000066400000000000000000001125371527070072300440020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-locals,too-many-lines """ The opentelemetry-instrumentation-asgi package provides an ASGI middleware that can be used on any ASGI framework (such as Django-channels / Quart) to track request timing through OpenTelemetry. Usage (Quart) ------------- .. code-block:: python from quart import Quart from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware app = Quart(__name__) app.asgi_app = OpenTelemetryMiddleware(app.asgi_app) @app.route("/") async def hello(): return "Hello!" if __name__ == "__main__": app.run(debug=True) Usage (Django 3.0) ------------------ Modify the application's ``asgi.py`` file as shown below. .. code-block:: python import os from django.core.asgi import get_asgi_application from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware os.environ.setdefault("DJANGO_SETTINGS_MODULE", "asgi_example.settings") application = get_asgi_application() application = OpenTelemetryMiddleware(application) Usage (Raw ASGI) ---------------- .. code-block:: python from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware app = ... # An ASGI application. app = OpenTelemetryMiddleware(app) Configuration ------------- Request/Response hooks ********************** This instrumentation supports request and response hooks. These are functions that get called right after a span is created for a request and right before the span is finished for the response. - The server request hook is passed a server span and ASGI scope object for every incoming request. - The client request hook is called with the internal span and an ASGI scope when the method ``receive`` is called. - The client response hook is called with the internal span and an ASGI event when the method ``send`` is called. For example, .. code-block:: python from opentelemetry.trace import Span from typing import Any from asgiref.typing import Scope, ASGIReceiveEvent, ASGISendEvent from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware async def application(scope: Scope, receive: ASGIReceiveEvent, send: ASGISendEvent): await send( { "type": "http.response.start", "status": 200, "headers": [ [b"content-type", b"text/plain"], ], } ) await send( { "type": "http.response.body", "body": b"Hello, world!", } ) def server_request_hook(span: Span, scope: Scope): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_request_hook", "some-value") def client_request_hook(span: Span, scope: Scope, message: dict[str, Any]): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_client_request_hook", "some-value") def client_response_hook(span: Span, scope: Scope, message: dict[str, Any]): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_response_hook", "some-value") OpenTelemetryMiddleware( application, server_request_hook=server_request_hook, client_request_hook=client_request_hook, client_response_hook=client_response_hook, ) Capture HTTP request and response headers ***************************************** You can configure the agent to capture specified HTTP headers as span attributes, according to the `semantic conventions `_. Request headers *************** To capture HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. Request header names in ASGI are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" Would match all request headers that start with ``Accept`` and ``X-``. To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*" The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a list containing the header values. For example: ``http.request.header.custom_request_header = ["", ""]`` Response headers **************** To capture HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. Response header names in ASGI are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" Would match all response headers that start with ``Content`` and ``X-``. To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*" The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a list containing the header values. For example: ``http.response.header.custom_response_header = ["", ""]`` Sanitizing headers ****************** In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be matched in a case-insensitive manner. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. Note: The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. API --- """ from __future__ import annotations import typing import urllib from collections import defaultdict from collections.abc import Awaitable, Callable from functools import wraps from timeit import default_timer from typing import Any from asgiref.compatibility import guarantee_single_callable from opentelemetry import context, trace from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, _filter_semconv_active_request_count_attr, _filter_semconv_duration_attrs, _get_schema_url, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _report_new, _report_old, _server_active_requests_count_attrs_new, _server_active_requests_count_attrs_old, _server_duration_attrs_new, _server_duration_attrs_old, _set_http_flavor_version, _set_http_host_server, _set_http_method, _set_http_net_host_port, _set_http_peer_ip_server, _set_http_peer_port_server, _set_http_scheme, _set_http_target, _set_http_url, _set_http_user_agent, _set_status, _StabilityMode, ) from opentelemetry.instrumentation.asgi.types import ( ClientRequestHook, ClientResponseHook, ServerRequestHook, ) from opentelemetry.instrumentation.asgi.version import __version__ from opentelemetry.instrumentation.propagators import ( get_global_response_propagator, ) from opentelemetry.instrumentation.utils import ( _start_internal_or_server_span, is_http_instrumentation_enabled, ) from opentelemetry.metrics import get_meter from opentelemetry.propagators.textmap import Getter, Setter from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_SERVER_NAME, HTTP_TARGET, ) from opentelemetry.semconv._incubating.attributes.user_agent_attributes import ( USER_AGENT_SYNTHETIC_TYPE, ) from opentelemetry.semconv._incubating.metrics.http_metrics import ( create_http_server_active_requests, create_http_server_request_body_size, create_http_server_response_body_size, ) from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.semconv.metrics.http_metrics import ( HTTP_SERVER_REQUEST_DURATION, ) from opentelemetry.trace import Span, set_span_in_context from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, ExcludeList, SanitizeValue, _parse_url_query, detect_synthetic_user_agent, get_custom_headers, normalise_request_header_name, normalise_response_header_name, normalize_user_agent, parse_excluded_urls, redact_url, sanitize_method, ) class ASGIGetter(Getter[dict]): def get(self, carrier: dict, key: str) -> list[str] | None: """Getter implementation to retrieve a HTTP header value from the ASGI scope. Args: carrier: ASGI scope object key: header name in scope Returns: A list with a single string with the header value if it exists, else None. """ headers = carrier.get("headers") if not headers: return None # ASGI header keys are in lower case key = key.lower() decoded = [ _decode_header_item(_value) for (_key, _value) in headers if _decode_header_item(_key).lower() == key ] if not decoded: return None return decoded def keys(self, carrier: dict) -> list[str]: headers = carrier.get("headers") or [] return [_decode_header_item(_key) for (_key, _value) in headers] asgi_getter = ASGIGetter() class ASGISetter(Setter[dict]): def set(self, carrier: dict, key: str, value: str) -> None: # pylint: disable=no-self-use """Sets response header values on an ASGI scope according to `the spec `_. Args: carrier: ASGI scope object key: response header name to set value: response header value Returns: None """ headers = carrier.get("headers") if not headers: headers = [] carrier["headers"] = headers headers.append([key.lower().encode(), value.encode()]) asgi_setter = ASGISetter() # pylint: disable=too-many-branches def collect_request_attributes(scope, sem_conv_opt_in_mode=_StabilityMode.DEFAULT): """Collects HTTP request attributes from the ASGI scope and returns a dictionary to be used as span creation attributes.""" server_host, port, http_url = get_host_port_url_tuple(scope) query_string = scope.get("query_string") if query_string and http_url: if isinstance(query_string, bytes): query_string = query_string.decode("utf8") http_url += "?" + urllib.parse.unquote(query_string) result = {} scheme = scope.get("scheme") if scheme: _set_http_scheme(result, scheme, sem_conv_opt_in_mode) if server_host: _set_http_host_server(result, server_host, sem_conv_opt_in_mode) if port: _set_http_net_host_port(result, port, sem_conv_opt_in_mode) flavor = scope.get("http_version") if flavor: _set_http_flavor_version(result, flavor, sem_conv_opt_in_mode) path = scope.get("path") if path: _set_http_target(result, path, path, query_string, sem_conv_opt_in_mode) if http_url: if _report_old(sem_conv_opt_in_mode): _set_http_url( result, redact_url(http_url), _StabilityMode.DEFAULT, ) http_method = scope.get("method", "") if http_method: _set_http_method( result, http_method, sanitize_method(http_method), sem_conv_opt_in_mode, ) http_host_value_list = asgi_getter.get(scope, "host") if http_host_value_list: if _report_old(sem_conv_opt_in_mode): result[HTTP_SERVER_NAME] = ",".join(http_host_value_list) http_user_agent = asgi_getter.get(scope, "user-agent") if http_user_agent: user_agent_raw = http_user_agent[0] user_agent_value = normalize_user_agent(user_agent_raw) if user_agent_value: _set_http_user_agent(result, user_agent_value, sem_conv_opt_in_mode) # Check for synthetic user agent type synthetic_type = detect_synthetic_user_agent(user_agent_value) if synthetic_type: result[USER_AGENT_SYNTHETIC_TYPE] = synthetic_type if "client" in scope and scope["client"] is not None: _set_http_peer_ip_server(result, scope.get("client")[0], sem_conv_opt_in_mode) _set_http_peer_port_server(result, scope.get("client")[1], sem_conv_opt_in_mode) # remove None values result = {k: v for k, v in result.items() if v is not None} return result def collect_custom_headers_attributes( scope_or_response_message: dict[str, Any], sanitize: SanitizeValue, header_regexes: list[str], normalize_names: Callable[[str], str], ) -> dict[str, list[str]]: """ Returns custom HTTP request or response headers to be added into SERVER span as span attributes. Refer to semantic conventions: - https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#http-server-span """ headers: defaultdict[str, list[str]] = defaultdict(list) raw_headers = scope_or_response_message.get("headers") if raw_headers: for key, value in raw_headers: # Decode headers before processing. headers[_decode_header_item(key)].append(_decode_header_item(value)) return sanitize.sanitize_header_values( headers, header_regexes, normalize_names, ) def get_host_port_url_tuple(scope): """Returns (host, port, full_url) tuple.""" server = scope.get("server") or ["0.0.0.0", 80] port = server[1] host_header = asgi_getter.get(scope, "host") if host_header: host_value = host_header[0] # Ensure host_value is a string, not bytes if isinstance(host_value, bytes): host_value = _decode_header_item(host_value) url_host = host_value else: url_host = server[0] + (":" + str(port) if str(port) != "80" else "") server_host = server[0] + (":" + str(port) if str(port) != "80" else "") # using the scope path is enough, see: # - https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope (see: root_path and path) # - https://asgi.readthedocs.io/en/latest/specs/www.html#wsgi-compatibility (see: PATH_INFO) # PATH_INFO can be derived by stripping root_path from path # -> that means that the path should contain the root_path already, so prefixing it again is not necessary # - https://wsgi.readthedocs.io/en/latest/definitions.html#envvar-PATH_INFO full_path = scope.get("path", "") http_url = scope.get("scheme", "http") + "://" + url_host + full_path return server_host, port, http_url def set_status_code( span, status_code, metric_attributes=None, sem_conv_opt_in_mode=_StabilityMode.DEFAULT, ): """Adds HTTP response attributes to span using the status_code argument.""" status_code_str = str(status_code) try: status_code = int(status_code) except ValueError: status_code = -1 if metric_attributes is None: metric_attributes = {} _set_status( span, metric_attributes, status_code, status_code_str, server_span=True, sem_conv_opt_in_mode=sem_conv_opt_in_mode, ) def get_default_span_details(scope: dict) -> tuple[str, dict]: """ Default span name is the HTTP method and URL path, or just the method. https://github.com/open-telemetry/opentelemetry-specification/pull/3165 https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#name Args: scope: the ASGI scope dictionary Returns: a tuple of the span name, and any attributes to attach to the span. """ path = scope.get("path", "").strip() method = sanitize_method(scope.get("method", "").strip()) if method == "_OTHER": method = "HTTP" if method and path: # http return f"{method} {path}", {} if path: # websocket return path, {} return method, {} # http with no path def _collect_target_attribute( scope: dict[str, typing.Any], ) -> str | None: """ Returns the target path as defined by the Semantic Conventions. This value is suitable to use in metrics as it should replace concrete values with a parameterized name. Example: /api/users/{user_id} Refer to the specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#parameterized-attributes Note: this function requires specific code for each framework, as there's no standard attribute to use. """ # FastAPI root_path = scope.get("root_path", "") route = scope.get("route") path_format = getattr(route, "path_format", None) if path_format: return f"{root_path}{path_format}" return None class OpenTelemetryMiddleware: """The ASGI application middleware. This class is an ASGI middleware that starts and annotates spans for any requests it is invoked with. Args: app: The ASGI application callable to forward requests to. default_span_details: Callback which should return a string and a tuple, representing the desired default span name and a dictionary with any additional span attributes to set. Optional: Defaults to get_default_span_details. server_request_hook: Optional callback which is called with the server span and ASGI scope object for every incoming request. client_request_hook: Optional callback which is called with the internal span, and ASGI scope and event which are sent as dictionaries for when the method receive is called. client_response_hook: Optional callback which is called with the internal span, and ASGI scope and event which are sent as dictionaries for when the method send is called. tracer_provider: The optional tracer provider to use. If omitted the current globally configured one is used. meter_provider: The optional meter provider to use. If omitted the current globally configured one is used. exclude_spans: Optionally exclude HTTP `send` and/or `receive` spans from the trace. """ # pylint: disable=too-many-branches,too-many-positional-arguments def __init__( self, app, excluded_urls: ExcludeList | str | None = None, default_span_details=None, server_request_hook: ServerRequestHook = None, client_request_hook: ClientRequestHook = None, client_response_hook: ClientResponseHook = None, tracer_provider=None, meter_provider=None, tracer=None, meter=None, http_capture_headers_server_request: list[str] | None = None, http_capture_headers_server_response: list[str] | None = None, http_capture_headers_sanitize_fields: list[str] | None = None, exclude_spans: list[typing.Literal["receive", "send"]] | None = None, ): # initialize semantic conventions opt-in if needed _OpenTelemetrySemanticConventionStability._initialize() sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) self.app = guarantee_single_callable(app) self.tracer = ( trace.get_tracer( __name__, __version__, tracer_provider, schema_url=_get_schema_url(sem_conv_opt_in_mode), ) if tracer is None else tracer ) self.meter = ( get_meter( __name__, __version__, meter_provider, schema_url=_get_schema_url(sem_conv_opt_in_mode), ) if meter is None else meter ) self.duration_histogram_old = None if _report_old(sem_conv_opt_in_mode): self.duration_histogram_old = self.meter.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", description="Measures the duration of inbound HTTP requests.", ) self.duration_histogram_new = None if _report_new(sem_conv_opt_in_mode): self.duration_histogram_new = self.meter.create_histogram( name=HTTP_SERVER_REQUEST_DURATION, description="Duration of HTTP server requests.", unit="s", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) self.server_response_size_histogram = None if _report_old(sem_conv_opt_in_mode): self.server_response_size_histogram = self.meter.create_histogram( name=MetricInstruments.HTTP_SERVER_RESPONSE_SIZE, unit="By", description="measures the size of HTTP response messages (compressed).", ) self.server_response_body_size_histogram = None if _report_new(sem_conv_opt_in_mode): self.server_response_body_size_histogram = create_http_server_response_body_size(self.meter) self.server_request_size_histogram = None if _report_old(sem_conv_opt_in_mode): self.server_request_size_histogram = self.meter.create_histogram( name=MetricInstruments.HTTP_SERVER_REQUEST_SIZE, unit="By", description="Measures the size of HTTP request messages (compressed).", ) self.server_request_body_size_histogram = None if _report_new(sem_conv_opt_in_mode): self.server_request_body_size_histogram = create_http_server_request_body_size(self.meter) self.active_requests_counter = create_http_server_active_requests(self.meter) if isinstance(excluded_urls, str): excluded_urls = parse_excluded_urls(excluded_urls) self.excluded_urls = excluded_urls self.default_span_details = default_span_details or get_default_span_details def failsafe(func): if func is None: return None @wraps(func) def wrapper(span: Span, *args, **kwargs): try: func(span, *args, **kwargs) except Exception as exc: # pylint: disable=broad-exception-caught span.record_exception(exc) return wrapper self.server_request_hook = failsafe(server_request_hook) self.client_request_hook = failsafe(client_request_hook) self.client_response_hook = failsafe(client_response_hook) self.content_length_header = None self._sem_conv_opt_in_mode = sem_conv_opt_in_mode # Environment variables as constructor parameters self.http_capture_headers_server_request = ( http_capture_headers_server_request or (get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST)) or None ) self.http_capture_headers_server_response = ( http_capture_headers_server_response or (get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE)) or None ) self.http_capture_headers_sanitize_fields = SanitizeValue( http_capture_headers_sanitize_fields or (get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS)) or [] ) self.exclude_receive_span = "receive" in exclude_spans if exclude_spans else False self.exclude_send_span = "send" in exclude_spans if exclude_spans else False # pylint: disable=too-many-statements async def __call__( self, scope: typing.MutableMapping[str, Any], receive: Callable[[], Awaitable[typing.MutableMapping[str, Any]]], send: Callable[[typing.MutableMapping[str, Any]], Awaitable[None]], ) -> None: """The ASGI application Args: scope: An ASGI environment. receive: An awaitable callable yielding dictionaries send: An awaitable callable taking a single dictionary as argument. """ start = default_timer() if not is_http_instrumentation_enabled() or scope["type"] not in ( "http", "websocket", ): return await self.app(scope, receive, send) _, _, url = get_host_port_url_tuple(scope) if self.excluded_urls and self.excluded_urls.url_disabled(url): return await self.app(scope, receive, send) span_name, additional_attributes = self.default_span_details(scope) attributes = collect_request_attributes(scope, self._sem_conv_opt_in_mode) attributes.update(additional_attributes) span, token = _start_internal_or_server_span( tracer=self.tracer, span_name=span_name, start_time=None, context_carrier=scope, context_getter=asgi_getter, attributes=attributes, ) active_requests_count_attrs = _parse_active_request_count_attrs( attributes, self._sem_conv_opt_in_mode, ) if scope["type"] == "http": self.active_requests_counter.add(1, active_requests_count_attrs) try: with trace.use_span(span, end_on_exit=False) as current_span: if current_span.is_recording(): for key, value in attributes.items(): current_span.set_attribute(key, value) if current_span.kind == trace.SpanKind.SERVER: custom_attributes = ( collect_custom_headers_attributes( scope, self.http_capture_headers_sanitize_fields, self.http_capture_headers_server_request, normalise_request_header_name, ) if self.http_capture_headers_server_request else {} ) if len(custom_attributes) > 0: current_span.set_attributes(custom_attributes) if callable(self.server_request_hook): self.server_request_hook(current_span, scope) otel_receive = self._get_otel_receive(span_name, scope, receive) otel_send = self._get_otel_send( current_span, span_name, scope, send, attributes, ) await self.app(scope, otel_receive, otel_send) finally: if scope["type"] == "http": target = _collect_target_attribute(scope) if target: path, query = _parse_url_query(target) _set_http_target( attributes, target, path, query, self._sem_conv_opt_in_mode, ) duration_s = default_timer() - start duration_attrs_old = _parse_duration_attrs(attributes, _StabilityMode.DEFAULT) if target: duration_attrs_old[HTTP_TARGET] = target duration_attrs_new = _parse_duration_attrs(attributes, _StabilityMode.HTTP) span_ctx = set_span_in_context(span) if self.duration_histogram_old: self.duration_histogram_old.record( max(round(duration_s * 1000), 0), duration_attrs_old, context=span_ctx, ) if self.duration_histogram_new: self.duration_histogram_new.record( max(duration_s, 0), duration_attrs_new, context=span_ctx, ) self.active_requests_counter.add(-1, active_requests_count_attrs) if self.content_length_header: if self.server_response_size_histogram: self.server_response_size_histogram.record( self.content_length_header, duration_attrs_old, context=span_ctx, ) if self.server_response_body_size_histogram: self.server_response_body_size_histogram.record( self.content_length_header, duration_attrs_new, context=span_ctx, ) request_size = asgi_getter.get(scope, "content-length") if request_size: try: request_size_amount = int(request_size[0]) except ValueError: pass else: if self.server_request_size_histogram: self.server_request_size_histogram.record( request_size_amount, duration_attrs_old, context=span_ctx, ) if self.server_request_body_size_histogram: self.server_request_body_size_histogram.record( request_size_amount, duration_attrs_new, context=span_ctx, ) if token: context.detach(token) if span.is_recording(): span.end() # pylint: enable=too-many-branches def _get_otel_receive(self, server_span_name, scope, receive): if self.exclude_receive_span: return receive @wraps(receive) async def otel_receive(): with self.tracer.start_as_current_span( " ".join((server_span_name, scope["type"], "receive")) ) as receive_span: message = await receive() if callable(self.client_request_hook): self.client_request_hook(receive_span, scope, message) if receive_span.is_recording(): if message["type"] == "websocket.receive": set_status_code( receive_span, 200, None, self._sem_conv_opt_in_mode, ) receive_span.set_attribute("asgi.event.type", message["type"]) return message return otel_receive def _set_send_span( self, server_span_name, scope, send, message, status_code, expecting_trailers, ): """Set send span attributes and status code.""" with self.tracer.start_as_current_span(" ".join((server_span_name, scope["type"], "send"))) as send_span: if callable(self.client_response_hook): self.client_response_hook(send_span, scope, message) if send_span.is_recording(): if message["type"] == "http.response.start": expecting_trailers = message.get("trailers", False) send_span.set_attribute("asgi.event.type", message["type"]) if status_code: set_status_code( send_span, status_code, None, self._sem_conv_opt_in_mode, ) return expecting_trailers def _set_server_span(self, server_span, message, status_code, duration_attrs): """Set server span attributes and status code.""" if server_span.is_recording() and server_span.kind == trace.SpanKind.SERVER and "headers" in message: custom_response_attributes = ( collect_custom_headers_attributes( message, self.http_capture_headers_sanitize_fields, self.http_capture_headers_server_response, normalise_response_header_name, ) if self.http_capture_headers_server_response else {} ) if len(custom_response_attributes) > 0: server_span.set_attributes(custom_response_attributes) if status_code: set_status_code( server_span, status_code, duration_attrs, self._sem_conv_opt_in_mode, ) def _get_otel_send( self, server_span, server_span_name, scope, send, duration_attrs, ): expecting_trailers = False @wraps(send) async def otel_send(message: dict[str, Any]): nonlocal expecting_trailers status_code = None if message["type"] == "http.response.start": status_code = message["status"] elif message["type"] == "websocket.send": status_code = 200 if not self.exclude_send_span: expecting_trailers = self._set_send_span( server_span_name, scope, send, message, status_code, expecting_trailers, ) self._set_server_span(server_span, message, status_code, duration_attrs) propagator = get_global_response_propagator() if propagator: propagator.inject( message, context=set_span_in_context(server_span, trace.context_api.Context()), setter=asgi_setter, ) content_length = asgi_getter.get(message, "content-length") if content_length: try: self.content_length_header = int(content_length[0]) except ValueError: pass await send(message) # pylint: disable=too-many-boolean-expressions if ( not expecting_trailers and message["type"] == "http.response.body" and not message.get("more_body", False) ) or ( expecting_trailers and message["type"] == "http.response.trailers" and not message.get("more_trailers", False) ): server_span.end() return otel_send def _parse_duration_attrs(req_attrs, sem_conv_opt_in_mode=_StabilityMode.DEFAULT): return _filter_semconv_duration_attrs( req_attrs, _server_duration_attrs_old, _server_duration_attrs_new, sem_conv_opt_in_mode, ) def _parse_active_request_count_attrs(req_attrs, sem_conv_opt_in_mode=_StabilityMode.DEFAULT): return _filter_semconv_active_request_count_attr( req_attrs, _server_active_requests_count_attrs_old, _server_active_requests_count_attrs_new, sem_conv_opt_in_mode, ) def _decode_header_item(value): try: return value.decode("utf-8") except ValueError: # ASGI header encoding specs, see: # - https://asgi.readthedocs.io/en/latest/specs/www.html#wsgi-encoding-differences (see: WSGI encoding differences) # - https://docs.python.org/3/library/codecs.html#text-encodings (see: Text Encodings) return value.decode("unicode_escape") package.py000066400000000000000000000002521527070072300436240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("asgiref ~= 3.0",) _supports_metrics = True _semconv_status = "migration" types.py000066400000000000000000000013601527070072300433760ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable from typing import Any from opentelemetry.trace import Span _Scope = dict[str, Any] _Message = dict[str, Any] ServerRequestHook = Callable[[Span, _Scope], None] | None """ Incoming request callback type. Args: - Server span - ASGI scope as a mapping """ ClientRequestHook = Callable[[Span, _Scope, _Message], None] | None """ Receive callback type. Args: - Internal span - ASGI scope as a mapping - ASGI event as a mapping """ ClientResponseHook = Callable[[Span, _Scope, _Message], None] | None """ Send callback type. Args: - Internal span - ASGI scope as a mapping - ASGI event as a mapping """ version.py000066400000000000000000000001441527070072300437160ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000004521527070072300362510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asgiasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-asgi opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asgi/tests/000077500000000000000000000000001527070072300332305ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300352500ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asgi/teststest_asgi_custom_headers.py000066400000000000000000000417661527070072300406100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asgi/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import opentelemetry.instrumentation.asgi as otel_asgi from opentelemetry.test.asgitestutil import AsyncAsgiTestBase from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, ) from .test_asgi_middleware import simple_asgi async def http_app_with_custom_headers(scope, receive, send): message = await receive() assert scope["type"] == "http" if message.get("type") == "http.request": await send( { "type": "http.response.start", "status": 200, "headers": [ (b"Content-Type", b"text/plain"), (b"custom-test-header-1", b"test-header-value-1"), (b"custom-test-header-2", b"test-header-value-2"), ( b"my-custom-regex-header-1", b"my-custom-regex-value-1,my-custom-regex-value-2", ), ( b"My-Custom-Regex-Header-2", b"my-custom-regex-value-3,my-custom-regex-value-4", ), (b"my-secret-header", b"my-secret-value"), ( b"non-utf8-header", b"Moto Z\xb2", ), ( b"Moto-Z\xb2-non-utf8-header-key", b"Moto Z\xb2", ), ], } ) await send({"type": "http.response.body", "body": b"*"}) async def http_app_with_repeat_headers(scope, receive, send): message = await receive() assert scope["type"] == "http" if message.get("type") == "http.request": await send( { "type": "http.response.start", "status": 200, "headers": [ (b"Content-Type", b"text/plain"), (b"custom-test-header-1", b"test-header-value-1"), (b"custom-test-header-1", b"test-header-value-2"), ], } ) await send({"type": "http.response.body", "body": b"*"}) async def websocket_app_with_custom_headers(scope, receive, send): assert scope["type"] == "websocket" while True: message = await receive() if message.get("type") == "websocket.connect": await send( { "type": "websocket.accept", "headers": [ (b"custom-test-header-1", b"test-header-value-1"), (b"custom-test-header-2", b"test-header-value-2"), ( b"my-custom-regex-header-1", b"my-custom-regex-value-1,my-custom-regex-value-2", ), ( b"My-Custom-Regex-Header-2", b"my-custom-regex-value-3,my-custom-regex-value-4", ), (b"my-secret-header", b"my-secret-value"), ], } ) if message.get("type") == "websocket.receive": if message.get("text") == "ping": await send({"type": "websocket.send", "text": "pong"}) if message.get("type") == "websocket.disconnect": break class TestCustomHeaders(AsyncAsgiTestBase): constructor_params = {} __test__ = False def __init_subclass__(cls) -> None: if cls is not TestCustomHeaders: cls.__test__ = True def setUp(self): super().setUp() self.tracer_provider, self.exporter = TestBase.create_tracer_provider() self.tracer = self.tracer_provider.get_tracer(__name__) self.app = otel_asgi.OpenTelemetryMiddleware( simple_asgi, tracer_provider=self.tracer_provider, **self.constructor_params, ) async def test_http_custom_request_headers_in_span_attributes(self): self.scope["headers"].extend( [ (b"custom-test-header-1", b"test-header-value-1"), (b"custom-test-header-2", b"test-header-value-2"), (b"Regex-Test-Header-1", b"Regex Test Value 1"), (b"regex-test-header-2", b"RegexTestValue2,RegexTestValue3"), (b"My-Secret-Header", b"My Secret Value"), ( b"non-utf8-header", b"Moto Z\xb2", ), ( b"Moto-Z\xb2-non-utf8-header-key", b"Moto Z\xb2", ), ] ) self.seed_app(self.app) await self.send_default_request() await self.get_all_output() span_list = self.exporter.get_finished_spans() expected = { "http.request.header.custom_test_header_1": ("test-header-value-1",), "http.request.header.custom_test_header_2": ("test-header-value-2",), "http.request.header.regex_test_header_1": ("Regex Test Value 1",), "http.request.header.regex_test_header_2": ("RegexTestValue2,RegexTestValue3",), "http.request.header.non_utf8_header": ("Moto Z²",), "http.request.header.moto_z²_non_utf8_header_key": ("Moto Z²",), "http.request.header.my_secret_header": ("[REDACTED]",), } for span in span_list: if span.kind == SpanKind.SERVER: self.assertSpanHasAttributes(span, expected) async def test_http_repeat_request_headers_in_span_attributes(self): self.scope["headers"].extend( [ (b"custom-test-header-1", b"test-header-value-1"), (b"custom-test-header-1", b"test-header-value-2"), ] ) self.seed_app(self.app) await self.send_default_request() await self.get_all_output() span_list = self.exporter.get_finished_spans() expected = { "http.request.header.custom_test_header_1": ( "test-header-value-1", "test-header-value-2", ), } span = next(span for span in span_list if span.kind == SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) async def test_http_custom_request_headers_not_in_span_attributes(self): self.scope["headers"].extend( [ (b"custom-test-header-1", b"test-header-value-1"), ] ) self.seed_app(self.app) await self.send_default_request() await self.get_all_output() span_list = self.exporter.get_finished_spans() expected = { "http.request.header.custom_test_header_1": ("test-header-value-1",), } not_expected = { "http.request.header.custom_test_header_2": ("test-header-value-2",), } for span in span_list: if span.kind == SpanKind.SERVER: self.assertSpanHasAttributes(span, expected) for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) async def test_http_custom_response_headers_in_span_attributes(self): self.app = otel_asgi.OpenTelemetryMiddleware( http_app_with_custom_headers, tracer_provider=self.tracer_provider, **self.constructor_params, ) self.seed_app(self.app) await self.send_default_request() await self.get_all_output() span_list = self.exporter.get_finished_spans() expected = { "http.response.header.custom_test_header_1": ("test-header-value-1",), "http.response.header.custom_test_header_2": ("test-header-value-2",), "http.response.header.my_custom_regex_header_1": ("my-custom-regex-value-1,my-custom-regex-value-2",), "http.response.header.my_custom_regex_header_2": ("my-custom-regex-value-3,my-custom-regex-value-4",), "http.response.header.my_secret_header": ("[REDACTED]",), "http.response.header.non_utf8_header": ("Moto Z²",), "http.response.header.moto_z²_non_utf8_header_key": ("Moto Z²",), } for span in span_list: if span.kind == SpanKind.SERVER: self.assertSpanHasAttributes(span, expected) async def test_http_repeat_response_headers_in_span_attributes(self): self.app = otel_asgi.OpenTelemetryMiddleware( http_app_with_repeat_headers, tracer_provider=self.tracer_provider, **self.constructor_params, ) self.seed_app(self.app) await self.send_default_request() await self.get_all_output() span_list = self.exporter.get_finished_spans() expected = { "http.response.header.custom_test_header_1": ( "test-header-value-1", "test-header-value-2", ), } span = next(span for span in span_list if span.kind == SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) async def test_http_custom_response_headers_not_in_span_attributes(self): self.app = otel_asgi.OpenTelemetryMiddleware( http_app_with_custom_headers, tracer_provider=self.tracer_provider, **self.constructor_params, ) self.seed_app(self.app) await self.send_default_request() await self.get_all_output() span_list = self.exporter.get_finished_spans() not_expected = { "http.response.header.custom_test_header_3": ("test-header-value-3",), } for span in span_list: if span.kind == SpanKind.SERVER: for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) async def test_websocket_custom_request_headers_in_span_attributes(self): self.scope = { "type": "websocket", "http_version": "1.1", "scheme": "ws", "path": "/", "query_string": b"", "headers": [ (b"custom-test-header-1", b"test-header-value-1"), (b"custom-test-header-2", b"test-header-value-2"), (b"Regex-Test-Header-1", b"Regex Test Value 1"), (b"regex-test-header-2", b"RegexTestValue2,RegexTestValue3"), (b"My-Secret-Header", b"My Secret Value"), ], "client": ("127.0.0.1", 32767), "server": ("127.0.0.1", 80), } self.seed_app(self.app) await self.send_input({"type": "websocket.connect"}) await self.send_input({"type": "websocket.receive", "text": "ping"}) await self.send_input({"type": "websocket.disconnect"}) await self.get_all_output() span_list = self.exporter.get_finished_spans() expected = { "http.request.header.custom_test_header_1": ("test-header-value-1",), "http.request.header.custom_test_header_2": ("test-header-value-2",), "http.request.header.regex_test_header_1": ("Regex Test Value 1",), "http.request.header.regex_test_header_2": ("RegexTestValue2,RegexTestValue3",), "http.request.header.my_secret_header": ("[REDACTED]",), } for span in span_list: if span.kind == SpanKind.SERVER: self.assertSpanHasAttributes(span, expected) async def test_websocket_custom_request_headers_not_in_span_attributes( self, ): self.scope = { "type": "websocket", "http_version": "1.1", "scheme": "ws", "path": "/", "query_string": b"", "headers": [ (b"Custom-Test-Header-1", b"test-header-value-1"), (b"Custom-Test-Header-2", b"test-header-value-2"), ], "client": ("127.0.0.1", 32767), "server": ("127.0.0.1", 80), } self.seed_app(self.app) await self.send_input({"type": "websocket.connect"}) await self.send_input({"type": "websocket.receive", "text": "ping"}) await self.send_input({"type": "websocket.disconnect"}) await self.get_all_output() span_list = self.exporter.get_finished_spans() not_expected = { "http.request.header.custom_test_header_3": ("test-header-value-3",), } for span in span_list: if span.kind == SpanKind.SERVER: for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) async def test_websocket_custom_response_headers_in_span_attributes(self): self.scope = { "type": "websocket", "http_version": "1.1", "scheme": "ws", "path": "/", "query_string": b"", "headers": [], "client": ("127.0.0.1", 32767), "server": ("127.0.0.1", 80), } self.app = otel_asgi.OpenTelemetryMiddleware( websocket_app_with_custom_headers, tracer_provider=self.tracer_provider, **self.constructor_params, ) self.seed_app(self.app) await self.send_input({"type": "websocket.connect"}) await self.send_input({"type": "websocket.receive", "text": "ping"}) await self.send_input({"type": "websocket.disconnect"}) await self.get_all_output() span_list = self.exporter.get_finished_spans() expected = { "http.response.header.custom_test_header_1": ("test-header-value-1",), "http.response.header.custom_test_header_2": ("test-header-value-2",), "http.response.header.my_custom_regex_header_1": ("my-custom-regex-value-1,my-custom-regex-value-2",), "http.response.header.my_custom_regex_header_2": ("my-custom-regex-value-3,my-custom-regex-value-4",), "http.response.header.my_secret_header": ("[REDACTED]",), } for span in span_list: if span.kind == SpanKind.SERVER: self.assertSpanHasAttributes(span, expected) async def test_websocket_custom_response_headers_not_in_span_attributes( self, ): self.scope = { "type": "websocket", "http_version": "1.1", "scheme": "ws", "path": "/", "query_string": b"", "headers": [], "client": ("127.0.0.1", 32767), "server": ("127.0.0.1", 80), } self.app = otel_asgi.OpenTelemetryMiddleware( websocket_app_with_custom_headers, tracer_provider=self.tracer_provider, **self.constructor_params, ) self.seed_app(self.app) await self.send_input({"type": "websocket.connect"}) await self.send_input({"type": "websocket.receive", "text": "ping"}) await self.send_input({"type": "websocket.disconnect"}) await self.get_all_output() span_list = self.exporter.get_finished_spans() not_expected = { "http.response.header.custom_test_header_3": ("test-header-value-3",), } for span in span_list: if span.kind == SpanKind.SERVER: for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) SANITIZE_FIELDS_TEST_VALUE = ".*my-secret.*" SERVER_REQUEST_TEST_VALUE = "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*,non-utf8-header,Moto-Z²-non-utf8-header-key" SERVER_RESPONSE_TEST_VALUE = "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*,non-utf8-header,Moto-Z²-non-utf8-header-key" class TestCustomHeadersEnv(TestCustomHeaders): def setUp(self): os.environ.update( { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: SANITIZE_FIELDS_TEST_VALUE, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: SERVER_REQUEST_TEST_VALUE, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: SERVER_RESPONSE_TEST_VALUE, } ) super().setUp() def tearDown(self): os.environ.pop(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, None) os.environ.pop(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, None) os.environ.pop(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, None) super().tearDown() class TestCustomHeadersConstructor(TestCustomHeaders): constructor_params = { "http_capture_headers_sanitize_fields": SANITIZE_FIELDS_TEST_VALUE.split(","), "http_capture_headers_server_request": SERVER_REQUEST_TEST_VALUE.split(","), "http_capture_headers_server_response": SERVER_RESPONSE_TEST_VALUE.split(","), } test_asgi_middleware.py000066400000000000000000002415661527070072300377200ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asgi/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines import sys import time import unittest from timeit import default_timer from unittest import mock import opentelemetry.instrumentation.asgi as otel_asgi from opentelemetry import trace as trace_api from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, _server_active_requests_count_attrs_new, _server_active_requests_count_attrs_old, _server_duration_attrs_new, _server_duration_attrs_old, _StabilityMode, ) from opentelemetry.instrumentation.propagators import ( TraceResponsePropagator, get_global_response_propagator, set_global_response_propagator, ) from opentelemetry.instrumentation.utils import suppress_http_instrumentation from opentelemetry.sdk import resources from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, NumberDataPoint, ) from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_FLAVOR, HTTP_HOST, HTTP_METHOD, HTTP_SCHEME, HTTP_SERVER_NAME, HTTP_STATUS_CODE, HTTP_TARGET, HTTP_URL, HTTP_USER_AGENT, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_HOST_PORT, NET_PEER_IP, NET_PEER_PORT, ) from opentelemetry.semconv._incubating.attributes.user_agent_attributes import ( USER_AGENT_SYNTHETIC_TYPE, ) from opentelemetry.semconv.attributes.client_attributes import ( CLIENT_ADDRESS, CLIENT_PORT, ) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, ) from opentelemetry.semconv.attributes.network_attributes import ( NETWORK_PROTOCOL_VERSION, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.semconv.attributes.url_attributes import ( URL_PATH, URL_QUERY, URL_SCHEME, ) from opentelemetry.semconv.attributes.user_agent_attributes import ( USER_AGENT_ORIGINAL, ) from opentelemetry.test.asgitestutil import ( AsyncAsgiTestBase, setup_testing_defaults, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind, format_span_id, format_trace_id _expected_metric_names_old = [ "http.server.active_requests", "http.server.duration", "http.server.response.size", "http.server.request.size", ] _expected_metric_names_new = [ "http.server.active_requests", "http.server.request.duration", "http.server.response.body.size", "http.server.request.body.size", ] _expected_metric_names_both = _expected_metric_names_old _expected_metric_names_both.extend(_expected_metric_names_new) _recommended_attrs_old = { "http.server.active_requests": _server_active_requests_count_attrs_old, "http.server.duration": _server_duration_attrs_old, "http.server.response.size": _server_duration_attrs_old, "http.server.request.size": _server_duration_attrs_old, } _recommended_attrs_new = { "http.server.active_requests": _server_active_requests_count_attrs_new, "http.server.request.duration": _server_duration_attrs_new, "http.server.response.body.size": _server_duration_attrs_new, "http.server.request.body.size": _server_duration_attrs_new, } _recommended_attrs_both = _recommended_attrs_old.copy() _recommended_attrs_both.update(_recommended_attrs_new) _recommended_attrs_both["http.server.active_requests"].extend(_server_active_requests_count_attrs_old) _SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S = 0.01 async def http_app(scope, receive, send): message = await receive() scope["headers"] = [(b"content-length", b"128")] assert scope["type"] == "http" if message.get("type") == "http.request": await send( { "type": "http.response.start", "status": 200, "headers": [ [b"Content-Type", b"text/plain"], [b"content-length", b"1024"], ], } ) await send({"type": "http.response.body", "body": b"*"}) async def websocket_app(scope, receive, send): assert scope["type"] == "websocket" while True: message = await receive() if message.get("type") == "websocket.connect": await send({"type": "websocket.accept"}) if message.get("type") == "websocket.receive": if message.get("text") == "ping": await send({"type": "websocket.send", "text": "pong"}) if message.get("type") == "websocket.disconnect": break async def simple_asgi(scope, receive, send): assert isinstance(scope, dict) if scope["type"] == "http": await http_app(scope, receive, send) elif scope["type"] == "websocket": await websocket_app(scope, receive, send) async def long_response_asgi(scope, receive, send): assert isinstance(scope, dict) assert scope["type"] == "http" message = await receive() scope["headers"] = [(b"content-length", b"128")] assert scope["type"] == "http" if message.get("type") == "http.request": await send( { "type": "http.response.start", "status": 200, "headers": [ [b"Content-Type", b"text/plain"], [b"content-length", b"1024"], ], } ) await send({"type": "http.response.body", "body": b"*", "more_body": True}) await send({"type": "http.response.body", "body": b"*", "more_body": True}) await send({"type": "http.response.body", "body": b"*", "more_body": True}) await send({"type": "http.response.body", "body": b"*", "more_body": False}) async def background_execution_asgi(scope, receive, send): assert isinstance(scope, dict) assert scope["type"] == "http" message = await receive() scope["headers"] = [(b"content-length", b"128")] assert scope["type"] == "http" if message.get("type") == "http.request": await send( { "type": "http.response.start", "status": 200, "headers": [ [b"Content-Type", b"text/plain"], [b"content-length", b"1024"], ], } ) await send( { "type": "http.response.body", "body": b"*", } ) # Record when the background task starts; the server span must already be ended by now. scope["background_task_start_ns"] = time.time_ns() time.sleep(_SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S) async def background_execution_trailers_asgi(scope, receive, send): assert isinstance(scope, dict) assert scope["type"] == "http" message = await receive() scope["headers"] = [(b"content-length", b"128")] assert scope["type"] == "http" if message.get("type") == "http.request": await send( { "type": "http.response.start", "status": 200, "headers": [ [b"Content-Type", b"text/plain"], [b"content-length", b"1024"], ], "trailers": True, } ) await send({"type": "http.response.body", "body": b"*", "more_body": True}) await send({"type": "http.response.body", "body": b"*", "more_body": False}) await send( { "type": "http.response.trailers", "headers": [ [b"trailer", b"test-trailer"], ], "more_trailers": True, } ) await send( { "type": "http.response.trailers", "headers": [ [b"trailer", b"second-test-trailer"], ], "more_trailers": False, } ) # Record when the background task starts; the server span must already be ended by now. scope["background_task_start_ns"] = time.time_ns() time.sleep(_SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S) async def error_asgi(scope, receive, send): assert isinstance(scope, dict) assert scope["type"] == "http" message = await receive() scope["headers"] = [(b"content-length", b"128")] if message.get("type") == "http.request": try: raise ValueError except ValueError: scope["hack_exc_info"] = sys.exc_info() await send( { "type": "http.response.start", "status": 200, "headers": [ [b"Content-Type", b"text/plain"], [b"content-length", b"1024"], ], } ) await send({"type": "http.response.body", "body": b"*"}) class UnhandledException(Exception): pass def failing_hook(msg): def hook(*_): raise UnhandledException(msg) return hook SCOPE = "opentelemetry.instrumentation.asgi" # pylint: disable=too-many-public-methods class TestAsgiApplication(AsyncAsgiTestBase): def setUp(self): super().setUp() test_name = "" if hasattr(self, "_testMethodName"): test_name = self._testMethodName sem_conv_mode = "default" if "new_semconv" in test_name: sem_conv_mode = "http" elif "both_semconv" in test_name: sem_conv_mode = "http/dup" self.env_patch = mock.patch.dict( "os.environ", { OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) _OpenTelemetrySemanticConventionStability._initialized = False self.env_patch.start() def tearDown(self): self.env_patch.stop() super().tearDown() def subTest(self, msg=..., **params): sub = super().subTest(msg, **params) self.memory_exporter.clear() return sub # Helper to assert exemplars presence across specified histogram metric names. def _assert_exemplars_present(self, metric_names: set[str], context: str = ""): metrics = self.get_sorted_metrics(SCOPE) found = {name: 0 for name in metric_names} for metric in metrics: if metric.name not in metric_names: continue for point in metric.data.data_points: found[metric.name] += 1 exemplars = getattr(point, "exemplars", None) self.assertIsNotNone( exemplars, msg=f"Expected exemplars list attribute on histogram data point for {metric.name} ({context})", ) self.assertGreater( len(exemplars or []), 0, msg=f"Expected at least one exemplar on histogram data point for {metric.name} ({context}) but none found.", ) for ex in exemplars or []: if hasattr(ex, "span_id"): self.assertNotEqual(ex.span_id, 0) if hasattr(ex, "trace_id"): self.assertNotEqual(ex.trace_id, 0) for name, count in found.items(): self.assertGreater( count, 0, msg=f"Did not encounter any data points for metric {name} while checking exemplars ({context}).", ) # pylint: disable=too-many-locals def validate_outputs( self, outputs, error=None, modifiers=None, old_sem_conv=True, new_sem_conv=False, ): # Ensure modifiers is a list modifiers = modifiers or [] # Check for expected outputs self.assertTrue(outputs, "ASGI application produced no response messages") response_start = outputs[0] response_final_body = [output for output in outputs if output["type"] == "http.response.body"][-1] self.assertEqual(response_start["type"], "http.response.start") self.assertEqual(response_final_body["type"], "http.response.body") self.assertEqual(response_final_body.get("more_body", False), False) # Check http response body self.assertEqual(response_final_body["body"], b"*") # Check http response start self.assertEqual(response_start["status"], 200) self.assertEqual( response_start["headers"], [[b"Content-Type", b"text/plain"], [b"content-length", b"1024"]], ) exc_info = self.scope.get("hack_exc_info") if error: self.assertIs(exc_info[0], error) self.assertIsInstance(exc_info[1], error) self.assertIsNotNone(exc_info[2]) else: self.assertIsNone(exc_info) # Check spans span_list = self.get_finished_spans() expected_old = [ { "name": "GET / http receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "http.request"}, }, { "name": "GET / http send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { HTTP_STATUS_CODE: 200, "asgi.event.type": "http.response.start", }, }, { "name": "GET / http send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "http.response.body"}, }, { "name": "GET /", "kind": trace_api.SpanKind.SERVER, "attributes": { HTTP_METHOD: "GET", HTTP_SCHEME: "http", NET_HOST_PORT: 80, HTTP_HOST: "127.0.0.1", HTTP_FLAVOR: "1.0", HTTP_TARGET: "/", HTTP_URL: "http://127.0.0.1/", NET_PEER_IP: "127.0.0.1", NET_PEER_PORT: 32767, HTTP_STATUS_CODE: 200, }, }, ] expected_new = [ { "name": "GET / http receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "http.request"}, }, { "name": "GET / http send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { HTTP_RESPONSE_STATUS_CODE: 200, "asgi.event.type": "http.response.start", }, }, { "name": "GET / http send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "http.response.body"}, }, { "name": "GET /", "kind": trace_api.SpanKind.SERVER, "attributes": { HTTP_REQUEST_METHOD: "GET", URL_SCHEME: "http", SERVER_ADDRESS: "127.0.0.1", SERVER_PORT: 80, NETWORK_PROTOCOL_VERSION: "1.0", URL_PATH: "/", CLIENT_ADDRESS: "127.0.0.1", CLIENT_PORT: 32767, HTTP_RESPONSE_STATUS_CODE: 200, }, }, ] expected_both = [ { "name": "GET / http receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "http.request"}, }, { "name": "GET / http send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { HTTP_STATUS_CODE: 200, HTTP_RESPONSE_STATUS_CODE: 200, "asgi.event.type": "http.response.start", }, }, { "name": "GET / http send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "http.response.body"}, }, { "name": "GET /", "kind": trace_api.SpanKind.SERVER, "attributes": { HTTP_REQUEST_METHOD: "GET", URL_SCHEME: "http", SERVER_ADDRESS: "127.0.0.1", SERVER_PORT: 80, NETWORK_PROTOCOL_VERSION: "1.0", URL_PATH: "/", CLIENT_ADDRESS: "127.0.0.1", CLIENT_PORT: 32767, HTTP_RESPONSE_STATUS_CODE: 200, HTTP_METHOD: "GET", HTTP_SCHEME: "http", NET_HOST_PORT: 80, HTTP_HOST: "127.0.0.1", HTTP_FLAVOR: "1.0", HTTP_TARGET: "/", HTTP_URL: "http://127.0.0.1/", NET_PEER_IP: "127.0.0.1", NET_PEER_PORT: 32767, HTTP_STATUS_CODE: 200, }, }, ] expected = expected_old if new_sem_conv: if old_sem_conv: expected = expected_both else: expected = expected_new # Run our expected modifiers for modifier in modifiers: expected = modifier(expected) # Check that output matches self.assertEqual(len(span_list), len(expected)) for span, expected in zip(span_list, expected): self.assertEqual(span.name, expected["name"]) self.assertEqual(span.kind, expected["kind"]) self.assertDictEqual(dict(span.attributes), expected["attributes"]) self.assertEqual( span.instrumentation_scope.name, SCOPE, ) if "events" in expected: self.assertEqual(len(span.events), len(expected["events"])) for event, expected in zip(span.events, expected["events"]): self.assertEqual(event.name, expected["name"]) for name, value in expected["attributes"].items(): self.assertEqual(event.attributes[name], value) async def test_basic_asgi_call(self): """Test that spans are emitted as expected.""" app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs(outputs) async def test_basic_asgi_call_new_semconv(self): """Test that spans are emitted as expected.""" app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs(outputs, old_sem_conv=False, new_sem_conv=True) async def test_basic_asgi_call_both_semconv(self): """Test that spans are emitted as expected.""" app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs(outputs, old_sem_conv=True, new_sem_conv=True) async def test_asgi_not_recording(self): mock_tracer = mock.Mock() mock_span = mock.Mock() mock_span.is_recording.return_value = False mock_tracer.start_as_current_span.return_value = mock_span mock_tracer.start_as_current_span.return_value.__enter__ = mock.Mock(return_value=mock_span) mock_tracer.start_as_current_span.return_value.__exit__ = mock_span with mock.patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) async def test_asgi_exc_info(self): """Test that exception information is emitted as expected.""" app = otel_asgi.OpenTelemetryMiddleware(error_asgi) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs(outputs, error=ValueError) async def test_long_response(self): """Test that the server span is ended on the final response body message. If the server span is ended early then this test will fail due to discrepancies in the expected list of spans and the emitted list of spans. """ app = otel_asgi.OpenTelemetryMiddleware(long_response_asgi) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() def add_more_body_spans(expected: list): more_body_span = { "name": "GET / http send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "http.response.body"}, } extra_spans = [more_body_span] * 3 expected[2:2] = extra_spans return expected self.validate_outputs(outputs, modifiers=[add_more_body_spans]) async def test_background_execution(self): """Test that the server span is ended BEFORE the background task is finished.""" app = otel_asgi.OpenTelemetryMiddleware(background_execution_asgi) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs(outputs) span_list = self.get_finished_spans() server_span = span_list[-1] assert server_span.kind == SpanKind.SERVER self.assertLessEqual( server_span.end_time, self.scope["background_task_start_ns"], ) async def test_exclude_internal_spans(self): """Test that internal spans are excluded from the emitted spans when the `exclude_receive_span` or `exclude_send_span` attributes are set. """ cases = [ (["receive", "send"], ["GET / http receive", "GET / http send"]), (["send"], ["GET / http send"]), (["receive"], ["GET / http receive"]), ([], []), ] for exclude_spans, excluded_spans in cases: self.memory_exporter.clear() app = otel_asgi.OpenTelemetryMiddleware(simple_asgi, exclude_spans=exclude_spans) self.seed_app(app) await self.send_default_request() await self.get_all_output() span_list = self.get_finished_spans() self.assertTrue(span_list) for span in span_list: for excluded_span in excluded_spans: self.assertNotEqual(span.name, excluded_span) async def test_trailers(self): """Test that trailers are emitted as expected and that the server span is ended BEFORE the background task is finished.""" app = otel_asgi.OpenTelemetryMiddleware(background_execution_trailers_asgi) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() def add_body_and_trailer_span(expected: list): body_span = { "name": "GET / http send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "http.response.body"}, } trailer_span = { "name": "GET / http send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "http.response.trailers"}, } expected[2:2] = [body_span] expected[4:4] = [trailer_span] * 2 return expected self.validate_outputs(outputs, modifiers=[add_body_and_trailer_span]) span_list = self.get_finished_spans() server_span = span_list[-1] assert server_span.kind == SpanKind.SERVER self.assertLessEqual( server_span.end_time, self.scope["background_task_start_ns"], ) async def test_override_span_name(self): """Test that default span_names can be overwritten by our callback function.""" span_name = "Dymaxion" def get_predefined_span_details(_): return span_name, {} def update_expected_span_name(expected): for entry in expected: if entry["kind"] == trace_api.SpanKind.SERVER: entry["name"] = span_name else: entry["name"] = " ".join([span_name] + entry["name"].split(" ")[2:]) return expected app = otel_asgi.OpenTelemetryMiddleware(simple_asgi, default_span_details=get_predefined_span_details) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs(outputs, modifiers=[update_expected_span_name]) async def test_custom_tracer_provider_otel_asgi(self): resource = resources.Resource.create({"service-test-key": "value"}) result = TestBase.create_tracer_provider(resource=resource) tracer_provider, exporter = result app = otel_asgi.OpenTelemetryMiddleware(simple_asgi, tracer_provider=tracer_provider) self.seed_app(app) await self.send_default_request() span_list = exporter.get_finished_spans() for span in span_list: self.assertEqual(span.resource.attributes["service-test-key"], "value") async def test_no_op_tracer_provider_otel_asgi(self): app = otel_asgi.OpenTelemetryMiddleware(simple_asgi, tracer_provider=trace_api.NoOpTracerProvider()) self.seed_app(app) await self.send_default_request() response_start, response_body, *_ = await self.get_all_output() self.assertEqual(response_body["body"], b"*") self.assertEqual(response_start["status"], 200) span_list = self.get_finished_spans() self.assertEqual(len(span_list), 0) async def test_behavior_with_scope_server_as_none(self): """Test that middleware is ok when server is none in scope.""" def update_expected_server(expected): expected[3]["attributes"].update( { HTTP_HOST: "0.0.0.0", NET_HOST_PORT: 80, HTTP_URL: "http://0.0.0.0/", } ) return expected self.scope["server"] = None app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs(outputs, modifiers=[update_expected_server]) async def test_behavior_with_scope_server_as_none_new_semconv(self): """Test that middleware is ok when server is none in scope.""" def update_expected_server(expected): expected[3]["attributes"].update( { SERVER_ADDRESS: "0.0.0.0", SERVER_PORT: 80, } ) return expected self.scope["server"] = None app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs( outputs, modifiers=[update_expected_server], old_sem_conv=False, new_sem_conv=True, ) async def test_behavior_with_scope_server_as_none_both_semconv(self): """Test that middleware is ok when server is none in scope.""" def update_expected_server(expected): expected[3]["attributes"].update( { HTTP_HOST: "0.0.0.0", NET_HOST_PORT: 80, HTTP_URL: "http://0.0.0.0/", SERVER_ADDRESS: "0.0.0.0", SERVER_PORT: 80, } ) return expected self.scope["server"] = None app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs( outputs, modifiers=[update_expected_server], old_sem_conv=True, new_sem_conv=True, ) async def test_host_header(self): """Test that host header is converted to http.server_name.""" hostname = b"server_name_1" def update_expected_server(expected): expected[3]["attributes"].update( { HTTP_SERVER_NAME: hostname.decode("utf8"), HTTP_URL: f"http://{hostname.decode('utf8')}/", } ) return expected self.scope["headers"].append([b"host", hostname]) app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs(outputs, modifiers=[update_expected_server]) async def test_host_header_both_semconv(self): """Test that host header is converted to http.server_name.""" hostname = b"server_name_1" def update_expected_server(expected): expected[3]["attributes"].update( { HTTP_SERVER_NAME: hostname.decode("utf8"), HTTP_URL: f"http://{hostname.decode('utf8')}/", } ) return expected self.scope["headers"].append([b"host", hostname]) app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs( outputs, modifiers=[update_expected_server], old_sem_conv=True, new_sem_conv=True, ) async def test_user_agent(self): """Test that host header is converted to http.server_name.""" user_agent = b"test-agent" def update_expected_user_agent(expected): expected[3]["attributes"].update({HTTP_USER_AGENT: user_agent.decode("utf8")}) return expected self.scope["headers"].append([b"user-agent", user_agent]) app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs(outputs, modifiers=[update_expected_user_agent]) async def test_user_agent_new_semconv(self): """Test that host header is converted to http.server_name.""" user_agent = b"test-agent" def update_expected_user_agent(expected): expected[3]["attributes"].update({USER_AGENT_ORIGINAL: user_agent.decode("utf8")}) return expected self.scope["headers"].append([b"user-agent", user_agent]) app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs( outputs, modifiers=[update_expected_user_agent], old_sem_conv=False, new_sem_conv=True, ) async def test_user_agent_both_semconv(self): """Test that host header is converted to http.server_name.""" user_agent = b"test-agent" def update_expected_user_agent(expected): expected[3]["attributes"].update( { HTTP_USER_AGENT: user_agent.decode("utf8"), USER_AGENT_ORIGINAL: user_agent.decode("utf8"), } ) return expected self.scope["headers"].append([b"user-agent", user_agent]) app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() await self.communicator.wait() outputs = await self.get_all_output() self.validate_outputs( outputs, modifiers=[update_expected_user_agent], old_sem_conv=True, new_sem_conv=True, ) async def test_user_agent_synthetic_bot_detection(self): """Test that bot user agents are detected as synthetic with type 'bot'""" test_cases = [ b"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", b"Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", b"googlebot/1.0", b"bingbot/1.0", ] # Test each user agent case separately to avoid span accumulation for user_agent in test_cases: with self.subTest(user_agent=user_agent): # Clear headers first self.scope["headers"] = [] def update_expected_synthetic_bot(expected, ua: bytes = user_agent): expected[3]["attributes"].update( { HTTP_USER_AGENT: ua.decode("utf8"), USER_AGENT_SYNTHETIC_TYPE: "bot", } ) return expected self.scope["headers"].append([b"user-agent", user_agent]) app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs(outputs, modifiers=[update_expected_synthetic_bot]) async def test_user_agent_synthetic_test_detection(self): """Test that test user agents are detected as synthetic with type 'test'""" test_cases = [ b"alwayson/1.0", b"AlwaysOn/2.0", b"test-alwayson-client", ] # Test each user agent case separately to avoid span accumulation for user_agent in test_cases: with self.subTest(user_agent=user_agent): # Clear headers first self.scope["headers"] = [] def update_expected_synthetic_test(expected, ua: bytes = user_agent): expected[3]["attributes"].update( { HTTP_USER_AGENT: ua.decode("utf8"), USER_AGENT_SYNTHETIC_TYPE: "test", } ) return expected self.scope["headers"].append([b"user-agent", user_agent]) app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs(outputs, modifiers=[update_expected_synthetic_test]) async def test_user_agent_non_synthetic(self): """Test that normal user agents are not marked as synthetic""" test_cases = [ b"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", b"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", b"PostmanRuntime/7.28.4", b"curl/7.68.0", ] # Test each user agent case separately to avoid span accumulation for user_agent in test_cases: with self.subTest(user_agent=user_agent): # Clear headers first self.scope["headers"] = [] def update_expected_non_synthetic(expected, ua: bytes = user_agent): # Should only have the user agent, not synthetic type expected[3]["attributes"].update( { HTTP_USER_AGENT: ua.decode("utf8"), } ) return expected self.scope["headers"].append([b"user-agent", user_agent]) app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs(outputs, modifiers=[update_expected_non_synthetic]) async def test_user_agent_synthetic_new_semconv(self): """Test synthetic user agent detection with new semantic conventions""" user_agent = b"Mozilla/5.0 (compatible; Googlebot/2.1)" def update_expected_synthetic_new_semconv(expected): expected[3]["attributes"].update( { USER_AGENT_ORIGINAL: user_agent.decode("utf8"), USER_AGENT_SYNTHETIC_TYPE: "bot", } ) return expected self.scope["headers"] = [] self.scope["headers"].append([b"user-agent", user_agent]) app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs( outputs, modifiers=[update_expected_synthetic_new_semconv], old_sem_conv=False, new_sem_conv=True, ) async def test_traceresponse_header(self): """Test a traceresponse header is sent when a global propagator is set.""" orig = get_global_response_propagator() set_global_response_propagator(TraceResponsePropagator()) app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() response_start, response_body, *_ = await self.get_all_output() span = self.get_finished_spans()[-1] self.assertEqual(trace_api.SpanKind.SERVER, span.kind) self.assertEqual(response_body["body"], b"*") self.assertEqual(response_start["status"], 200) trace_id = format_trace_id(span.get_span_context().trace_id) span_id = format_span_id(span.get_span_context().span_id) trace_flags = span.get_span_context().trace_flags traceresponse = f"00-{trace_id}-{span_id}-{trace_flags:02x}" self.assertListEqual( response_start["headers"], [ [b"Content-Type", b"text/plain"], [b"content-length", b"1024"], [b"traceresponse", f"{traceresponse}".encode()], [b"access-control-expose-headers", b"traceresponse"], ], ) set_global_response_propagator(orig) async def test_websocket(self): self.scope = { "method": "GET", "type": "websocket", "http_version": "1.1", "scheme": "ws", "path": "/", "query_string": b"", "headers": [], "client": ("127.0.0.1", 32767), "server": ("127.0.0.1", 80), } app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_input({"type": "websocket.connect"}) await self.send_input({"type": "websocket.receive", "text": "ping"}) await self.send_input({"type": "websocket.disconnect"}) await self.get_all_output() span_list = self.get_finished_spans() self.assertEqual(len(span_list), 6) expected = [ { "name": "GET / websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.connect"}, }, { "name": "GET / websocket send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.accept"}, }, { "name": "GET / websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": { "asgi.event.type": "websocket.receive", HTTP_STATUS_CODE: 200, }, }, { "name": "GET / websocket send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { "asgi.event.type": "websocket.send", HTTP_STATUS_CODE: 200, }, }, { "name": "GET / websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.disconnect"}, }, { "name": "GET /", "kind": trace_api.SpanKind.SERVER, "attributes": { HTTP_SCHEME: self.scope["scheme"], NET_HOST_PORT: self.scope["server"][1], HTTP_HOST: self.scope["server"][0], HTTP_FLAVOR: self.scope["http_version"], HTTP_TARGET: self.scope["path"], HTTP_URL: f"{self.scope['scheme']}://{self.scope['server'][0]}{self.scope['path']}", NET_PEER_IP: self.scope["client"][0], NET_PEER_PORT: self.scope["client"][1], HTTP_STATUS_CODE: 200, HTTP_METHOD: self.scope["method"], }, }, ] for span, expected in zip(span_list, expected): self.assertEqual(span.name, expected["name"]) self.assertEqual(span.kind, expected["kind"]) self.assertDictEqual(dict(span.attributes), expected["attributes"]) async def test_websocket_new_semconv(self): self.scope = { "method": "GET", "type": "websocket", "http_version": "1.1", "scheme": "ws", "path": "/", "query_string": b"", "headers": [], "client": ("127.0.0.1", 32767), "server": ("127.0.0.1", 80), } app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_input({"type": "websocket.connect"}) await self.send_input({"type": "websocket.receive", "text": "ping"}) await self.send_input({"type": "websocket.disconnect"}) await self.get_all_output() span_list = self.get_finished_spans() self.assertEqual(len(span_list), 6) expected = [ { "name": "GET / websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.connect"}, }, { "name": "GET / websocket send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.accept"}, }, { "name": "GET / websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": { "asgi.event.type": "websocket.receive", HTTP_RESPONSE_STATUS_CODE: 200, }, }, { "name": "GET / websocket send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { "asgi.event.type": "websocket.send", HTTP_RESPONSE_STATUS_CODE: 200, }, }, { "name": "GET / websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.disconnect"}, }, { "name": "GET /", "kind": trace_api.SpanKind.SERVER, "attributes": { URL_SCHEME: self.scope["scheme"], SERVER_ADDRESS: self.scope["server"][0], SERVER_PORT: self.scope["server"][1], NETWORK_PROTOCOL_VERSION: self.scope["http_version"], URL_PATH: self.scope["path"], CLIENT_ADDRESS: self.scope["client"][0], CLIENT_PORT: self.scope["client"][1], HTTP_RESPONSE_STATUS_CODE: 200, HTTP_REQUEST_METHOD: self.scope["method"], }, }, ] for span, expected in zip(span_list, expected): self.assertEqual(span.name, expected["name"]) self.assertEqual(span.kind, expected["kind"]) self.assertDictEqual(dict(span.attributes), expected["attributes"]) async def test_websocket_both_semconv(self): self.scope = { "method": "GET", "type": "websocket", "http_version": "1.1", "scheme": "ws", "path": "/", "query_string": b"", "headers": [], "client": ("127.0.0.1", 32767), "server": ("127.0.0.1", 80), } app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_input({"type": "websocket.connect"}) await self.send_input({"type": "websocket.receive", "text": "ping"}) await self.send_input({"type": "websocket.disconnect"}) await self.get_all_output() span_list = self.get_finished_spans() self.assertEqual(len(span_list), 6) expected = [ { "name": "GET / websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.connect"}, }, { "name": "GET / websocket send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.accept"}, }, { "name": "GET / websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": { "asgi.event.type": "websocket.receive", HTTP_RESPONSE_STATUS_CODE: 200, HTTP_STATUS_CODE: 200, }, }, { "name": "GET / websocket send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { "asgi.event.type": "websocket.send", HTTP_RESPONSE_STATUS_CODE: 200, HTTP_STATUS_CODE: 200, }, }, { "name": "GET / websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.disconnect"}, }, { "name": "GET /", "kind": trace_api.SpanKind.SERVER, "attributes": { HTTP_SCHEME: self.scope["scheme"], NET_HOST_PORT: self.scope["server"][1], HTTP_HOST: self.scope["server"][0], HTTP_FLAVOR: self.scope["http_version"], HTTP_TARGET: self.scope["path"], HTTP_URL: f"{self.scope['scheme']}://{self.scope['server'][0]}{self.scope['path']}", NET_PEER_IP: self.scope["client"][0], NET_PEER_PORT: self.scope["client"][1], HTTP_STATUS_CODE: 200, HTTP_METHOD: self.scope["method"], URL_SCHEME: self.scope["scheme"], SERVER_ADDRESS: self.scope["server"][0], SERVER_PORT: self.scope["server"][1], NETWORK_PROTOCOL_VERSION: self.scope["http_version"], URL_PATH: self.scope["path"], CLIENT_ADDRESS: self.scope["client"][0], CLIENT_PORT: self.scope["client"][1], HTTP_RESPONSE_STATUS_CODE: 200, HTTP_REQUEST_METHOD: self.scope["method"], }, }, ] for span, expected in zip(span_list, expected): self.assertEqual(span.name, expected["name"]) self.assertEqual(span.kind, expected["kind"]) self.assertDictEqual(dict(span.attributes), expected["attributes"]) async def test_websocket_traceresponse_header(self): """Test a traceresponse header is set for websocket messages""" orig = get_global_response_propagator() set_global_response_propagator(TraceResponsePropagator()) self.scope = { "type": "websocket", "http_version": "1.1", "scheme": "ws", "path": "/", "query_string": b"", "headers": [], "client": ("127.0.0.1", 32767), "server": ("127.0.0.1", 80), } app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_input({"type": "websocket.connect"}) await self.send_input({"type": "websocket.receive", "text": "ping"}) await self.send_input({"type": "websocket.disconnect"}) _, socket_send, *_ = await self.get_all_output() span = self.get_finished_spans()[-1] self.assertEqual(trace_api.SpanKind.SERVER, span.kind) trace_id = format_trace_id(span.get_span_context().trace_id) span_id = format_span_id(span.get_span_context().span_id) trace_flags = span.get_span_context().trace_flags traceresponse = f"00-{trace_id}-{span_id}-{trace_flags:02x}" self.assertListEqual( socket_send["headers"], [ [b"traceresponse", f"{traceresponse}".encode()], [b"access-control-expose-headers", b"traceresponse"], ], ) set_global_response_propagator(orig) async def test_lifespan(self): self.scope["type"] = "lifespan" app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() span_list = self.get_finished_spans() self.assertEqual(len(span_list), 0) async def test_hooks(self): def server_request_hook(span, scope): span.update_name("name from server hook") def client_request_hook(receive_span, scope, message): receive_span.update_name("name from client request hook") def client_response_hook(send_span, scope, message): send_span.set_attribute("attr-from-hook", "value") def update_expected_hook_results(expected): for entry in expected: if entry["kind"] == trace_api.SpanKind.SERVER: entry["name"] = "name from server hook" elif entry["name"] == "GET / http receive": entry["name"] = "name from client request hook" elif entry["name"] == "GET / http send": entry["attributes"].update({"attr-from-hook": "value"}) return expected app = otel_asgi.OpenTelemetryMiddleware( simple_asgi, server_request_hook=server_request_hook, client_request_hook=client_request_hook, client_response_hook=client_response_hook, ) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs(outputs, modifiers=[update_expected_hook_results]) async def test_hook_exceptions(self): def exception_event(msg): return { "name": "exception", "attributes": { "exception.type": f"{__name__}.UnhandledException", "exception.message": msg, }, } def update_expected_hook_results(expected): for entry in expected: if entry["kind"] == trace_api.SpanKind.SERVER: entry["events"] = [exception_event("server request")] elif entry["name"] == "GET / http receive": entry["events"] = [exception_event("client request")] elif entry["name"] == "GET / http send": entry["events"] = [exception_event("client response")] return expected app = otel_asgi.OpenTelemetryMiddleware( simple_asgi, server_request_hook=failing_hook("server request"), client_request_hook=failing_hook("client request"), client_response_hook=failing_hook("client response"), ) self.seed_app(app) await self.send_default_request() outputs = await self.get_all_output() self.validate_outputs(outputs, modifiers=[update_expected_hook_results]) async def test_asgi_metrics(self): app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() await self.get_all_output() self.seed_app(app) await self.send_default_request() await self.get_all_output() self.seed_app(app) await self.send_default_request() await self.get_all_output() number_data_point_seen = False histogram_data_point_seen = False metrics = self.get_sorted_metrics(SCOPE) self.assertTrue(len(metrics) != 0) for metric in metrics: self.assertIn(metric.name, _expected_metric_names_old) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 3) histogram_data_point_seen = True if isinstance(point, NumberDataPoint): number_data_point_seen = True for attr in point.attributes: self.assertIn(attr, _recommended_attrs_old[metric.name]) self.assertTrue(number_data_point_seen and histogram_data_point_seen) async def test_asgi_metrics_new_semconv(self): # pylint: disable=too-many-nested-blocks app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() await self.get_all_output() self.seed_app(app) await self.send_default_request() await self.get_all_output() self.seed_app(app) await self.send_default_request() await self.get_all_output() number_data_point_seen = False histogram_data_point_seen = False metrics = self.get_sorted_metrics(SCOPE) self.assertTrue(len(metrics) != 0) for metric in metrics: self.assertIn(metric.name, _expected_metric_names_new) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 3) if metric.name == "http.server.request.duration": self.assertEqual( point.explicit_bounds, HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) histogram_data_point_seen = True if isinstance(point, NumberDataPoint): number_data_point_seen = True for attr in point.attributes: self.assertIn(attr, _recommended_attrs_new[metric.name]) self.assertTrue(number_data_point_seen and histogram_data_point_seen) async def test_asgi_metrics_both_semconv(self): # pylint: disable=too-many-nested-blocks app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_default_request() await self.get_all_output() self.seed_app(app) await self.send_default_request() await self.get_all_output() self.seed_app(app) await self.send_default_request() await self.get_all_output() number_data_point_seen = False histogram_data_point_seen = False metrics = self.get_sorted_metrics(SCOPE) self.assertTrue(len(metrics) != 0) for metric in metrics: self.assertIn(metric.name, _expected_metric_names_both) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 3) if metric.name == "http.server.request.duration": self.assertEqual( point.explicit_bounds, HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) histogram_data_point_seen = True if isinstance(point, NumberDataPoint): number_data_point_seen = True for attr in point.attributes: self.assertIn(attr, _recommended_attrs_both[metric.name]) self.assertTrue(number_data_point_seen and histogram_data_point_seen) async def test_asgi_metrics_exemplars_expected_old_semconv(self): """Failing test placeholder asserting exemplars should be present for duration histogram (old semconv).""" app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) for _ in range(5): self.seed_app(app) await self.send_default_request() await self.get_all_output() self._assert_exemplars_present({"http.server.duration"}, context="old semconv") async def test_asgi_metrics_exemplars_expected_new_semconv(self): """Failing test placeholder asserting exemplars should be present for request duration histogram (new semconv).""" app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) for _ in range(5): self.seed_app(app) await self.send_default_request() await self.get_all_output() self._assert_exemplars_present({"http.server.request.duration"}, context="new semconv") async def test_asgi_metrics_exemplars_expected_both_semconv(self): """Failing test placeholder asserting exemplars should be present for both duration histograms when both semconv modes enabled.""" app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) for _ in range(5): self.seed_app(app) await self.send_default_request() await self.get_all_output() self._assert_exemplars_present( {"http.server.duration", "http.server.request.duration"}, context="both semconv", ) async def test_basic_metric_success(self): app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) start = default_timer() await self.send_default_request() duration = max(round((default_timer() - start) * 1000), 0) await self.get_all_output() expected_duration_attributes = { "http.method": "GET", "http.host": "127.0.0.1", "http.scheme": "http", "http.flavor": "1.0", "net.host.port": 80, "http.status_code": 200, } expected_requests_count_attributes = { "http.method": "GET", "http.host": "127.0.0.1", "http.scheme": "http", "http.flavor": "1.0", } metrics = self.get_sorted_metrics(SCOPE) # pylint: disable=too-many-nested-blocks for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertDictEqual( expected_duration_attributes, dict(point.attributes), ) self.assertEqual(point.count, 1) if metric.name == "http.server.duration": self.assertAlmostEqual(duration, point.sum, delta=30) elif metric.name == "http.server.response.size": self.assertEqual(1024, point.sum) elif metric.name == "http.server.request.size": self.assertEqual(128, point.sum) elif isinstance(point, NumberDataPoint): self.assertDictEqual( expected_requests_count_attributes, dict(point.attributes), ) self.assertEqual(point.value, 0) async def test_basic_metric_success_nonrecording_span(self): mock_tracer = mock.Mock() mock_span = mock.Mock() mock_span.is_recording.return_value = False mock_tracer.start_as_current_span.return_value = mock_span mock_tracer.start_as_current_span.return_value.__enter__ = mock.Mock(return_value=mock_span) mock_tracer.start_as_current_span.return_value.__exit__ = mock_span with mock.patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) start = default_timer() await self.send_default_request() duration = max(round((default_timer() - start) * 1000), 0) await self.get_all_output() expected_duration_attributes = { "http.method": "GET", "http.host": "127.0.0.1", "http.scheme": "http", "http.flavor": "1.0", "net.host.port": 80, "http.status_code": 200, } expected_requests_count_attributes = { "http.method": "GET", "http.host": "127.0.0.1", "http.scheme": "http", "http.flavor": "1.0", } metrics = self.get_sorted_metrics(SCOPE) # pylint: disable=too-many-nested-blocks for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertDictEqual( expected_duration_attributes, dict(point.attributes), ) self.assertEqual(point.count, 1) if metric.name == "http.server.duration": self.assertAlmostEqual(duration, point.sum, delta=15) elif metric.name == "http.server.response.size": self.assertEqual(1024, point.sum) elif metric.name == "http.server.request.size": self.assertEqual(128, point.sum) elif isinstance(point, NumberDataPoint): self.assertDictEqual( expected_requests_count_attributes, dict(point.attributes), ) self.assertEqual(point.value, 0) async def test_basic_metric_success_new_semconv(self): app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) start = default_timer() await self.send_default_request() duration_s = max(default_timer() - start, 0) await self.get_all_output() expected_duration_attributes = { "http.request.method": "GET", "url.scheme": "http", "network.protocol.version": "1.0", "http.response.status_code": 200, } expected_requests_count_attributes = { "http.request.method": "GET", "url.scheme": "http", } metrics = self.get_sorted_metrics(SCOPE) # pylint: disable=too-many-nested-blocks for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertDictEqual( expected_duration_attributes, dict(point.attributes), ) self.assertEqual(point.count, 1) if metric.name == "http.server.request.duration": self.assertAlmostEqual(duration_s, point.sum, places=2) elif metric.name == "http.server.response.body.size": self.assertEqual(1024, point.sum) elif metric.name == "http.server.request.body.size": self.assertEqual(128, point.sum) elif isinstance(point, NumberDataPoint): self.assertDictEqual( expected_requests_count_attributes, dict(point.attributes), ) self.assertEqual(point.value, 0) async def test_basic_metric_success_both_semconv(self): app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) start = default_timer() await self.send_default_request() duration = max(round((default_timer() - start) * 1000), 0) duration_s = max(default_timer() - start, 0) await self.get_all_output() expected_duration_attributes_old = { "http.method": "GET", "http.host": "127.0.0.1", "http.scheme": "http", "http.flavor": "1.0", "net.host.port": 80, "http.status_code": 200, } expected_requests_count_attributes = { "http.method": "GET", "http.host": "127.0.0.1", "http.scheme": "http", "http.flavor": "1.0", "http.request.method": "GET", "url.scheme": "http", } expected_duration_attributes_new = { "http.request.method": "GET", "url.scheme": "http", "network.protocol.version": "1.0", "http.response.status_code": 200, } metrics = self.get_sorted_metrics(SCOPE) # pylint: disable=too-many-nested-blocks for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) if metric.name == "http.server.request.duration": self.assertAlmostEqual(duration_s, point.sum, places=2) self.assertDictEqual( expected_duration_attributes_new, dict(point.attributes), ) elif metric.name == "http.server.response.body.size": self.assertEqual(1024, point.sum) self.assertDictEqual( expected_duration_attributes_new, dict(point.attributes), ) elif metric.name == "http.server.request.body.size": self.assertEqual(128, point.sum) self.assertDictEqual( expected_duration_attributes_new, dict(point.attributes), ) elif metric.name == "http.server.duration": self.assertAlmostEqual(duration, point.sum, delta=30) self.assertDictEqual( expected_duration_attributes_old, dict(point.attributes), ) elif metric.name == "http.server.response.size": self.assertEqual(1024, point.sum) self.assertDictEqual( expected_duration_attributes_old, dict(point.attributes), ) elif metric.name == "http.server.request.size": self.assertEqual(128, point.sum) self.assertDictEqual( expected_duration_attributes_old, dict(point.attributes), ) elif isinstance(point, NumberDataPoint): self.assertDictEqual( expected_requests_count_attributes, dict(point.attributes), ) self.assertEqual(point.value, 0) async def test_metric_target_attribute(self): expected_target = "/api/user/{id}" class TestRoute: path_format = expected_target async def target_asgi(scope, receive, send): assert isinstance(scope, dict) if scope["type"] == "http": await http_app(scope, receive, send) scope["route"] = TestRoute() else: raise ValueError("websockets not supported") app = otel_asgi.OpenTelemetryMiddleware(target_asgi) self.seed_app(app) await self.send_default_request() await self.get_all_output() metrics = self.get_sorted_metrics(SCOPE) assertions = 0 for metric in metrics: if metric.name == "http.server.active_requests": continue for point in metric.data.data_points: if isinstance(point, HistogramDataPoint): self.assertEqual( point.attributes["http.target"], expected_target, ) assertions += 1 self.assertEqual(assertions, 3) async def test_no_metric_for_websockets(self): self.scope = { "type": "websocket", "http_version": "1.1", "scheme": "ws", "path": "/", "query_string": b"", "headers": [], "client": ("127.0.0.1", 32767), "server": ("127.0.0.1", 80), } app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) await self.send_input({"type": "websocket.connect"}) await self.send_input({"type": "websocket.receive", "text": "ping"}) await self.send_input({"type": "websocket.disconnect"}) await self.get_all_output() self.assertEqual(len(self.get_sorted_metrics(SCOPE)), 0) async def test_excluded_urls(self): self.scope["path"] = "/test_excluded_urls" app = otel_asgi.OpenTelemetryMiddleware(simple_asgi, excluded_urls="test_excluded_urls") self.seed_app(app) await self.send_default_request() await self.get_all_output() spans = self.get_finished_spans() self.assertEqual(len(spans), 0) async def test_no_excluded_urls(self): self.scope["path"] = "/test_excluded_urls" app = otel_asgi.OpenTelemetryMiddleware(simple_asgi, excluded_urls="test_excluded_urls") self.seed_app(app) self.scope["path"] = "/test_no_excluded_urls" await self.send_default_request() await self.get_all_output() spans = self.get_finished_spans() self.assertGreater(len(spans), 0) async def test_suppress_http_instrumentation(self): app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) async def suppression_wrapper(scope, receive, send): with suppress_http_instrumentation(): await app(scope, receive, send) self.seed_app(suppression_wrapper) await self.send_default_request() await self.get_all_output() spans = self.get_finished_spans() self.assertEqual(len(spans), 0) class TestAsgiAttributes(unittest.TestCase): def setUp(self): self.scope = {} setup_testing_defaults(self.scope) self.span = mock.create_autospec(trace_api.Span, spec_set=True) def test_request_attributes(self): self.scope["query_string"] = b"foo=bar" headers = [] headers.append((b"host", b"test")) self.scope["headers"] = headers attrs = otel_asgi.collect_request_attributes(self.scope) self.assertDictEqual( attrs, { HTTP_METHOD: "GET", HTTP_HOST: "127.0.0.1", HTTP_TARGET: "/", HTTP_URL: "http://test/?foo=bar", NET_HOST_PORT: 80, HTTP_SCHEME: "http", HTTP_SERVER_NAME: "test", HTTP_FLAVOR: "1.0", NET_PEER_IP: "127.0.0.1", NET_PEER_PORT: 32767, }, ) def test_request_attributes_new_semconv(self): self.scope["query_string"] = b"foo=bar" headers = [] headers.append((b"host", b"test")) self.scope["headers"] = headers attrs = otel_asgi.collect_request_attributes( self.scope, _StabilityMode.HTTP, ) self.assertDictEqual( attrs, { HTTP_REQUEST_METHOD: "GET", URL_PATH: "/", URL_QUERY: "foo=bar", SERVER_ADDRESS: "127.0.0.1", SERVER_PORT: 80, URL_SCHEME: "http", NETWORK_PROTOCOL_VERSION: "1.0", CLIENT_ADDRESS: "127.0.0.1", CLIENT_PORT: 32767, }, ) def test_request_attributes_both_semconv(self): self.scope["query_string"] = b"foo=bar" headers = [] headers.append((b"host", b"test")) self.scope["headers"] = headers attrs = otel_asgi.collect_request_attributes( self.scope, _StabilityMode.HTTP_DUP, ) self.assertDictEqual( attrs, { HTTP_METHOD: "GET", HTTP_HOST: "127.0.0.1", HTTP_TARGET: "/", HTTP_URL: "http://test/?foo=bar", NET_HOST_PORT: 80, HTTP_SCHEME: "http", HTTP_SERVER_NAME: "test", HTTP_FLAVOR: "1.0", NET_PEER_IP: "127.0.0.1", NET_PEER_PORT: 32767, HTTP_REQUEST_METHOD: "GET", URL_PATH: "/", URL_QUERY: "foo=bar", SERVER_ADDRESS: "127.0.0.1", SERVER_PORT: 80, URL_SCHEME: "http", NETWORK_PROTOCOL_VERSION: "1.0", CLIENT_ADDRESS: "127.0.0.1", CLIENT_PORT: 32767, }, ) def test_query_string(self): self.scope["query_string"] = b"foo=bar" attrs = otel_asgi.collect_request_attributes(self.scope) self.assertEqual(attrs[HTTP_URL], "http://127.0.0.1/?foo=bar") def test_query_string_new_semconv(self): self.scope["query_string"] = b"foo=bar" attrs = otel_asgi.collect_request_attributes( self.scope, _StabilityMode.HTTP, ) self.assertEqual(attrs[URL_SCHEME], "http") self.assertEqual(attrs[CLIENT_ADDRESS], "127.0.0.1") self.assertEqual(attrs[URL_PATH], "/") self.assertEqual(attrs[URL_QUERY], "foo=bar") def test_query_string_both_semconv(self): self.scope["query_string"] = b"foo=bar" attrs = otel_asgi.collect_request_attributes( self.scope, _StabilityMode.HTTP_DUP, ) self.assertEqual(attrs[HTTP_URL], "http://127.0.0.1/?foo=bar") self.assertEqual(attrs[URL_SCHEME], "http") self.assertEqual(attrs[CLIENT_ADDRESS], "127.0.0.1") self.assertEqual(attrs[URL_PATH], "/") self.assertEqual(attrs[URL_QUERY], "foo=bar") def test_query_string_percent_bytes(self): self.scope["query_string"] = b"foo%3Dbar" attrs = otel_asgi.collect_request_attributes(self.scope) self.assertEqual(attrs[HTTP_URL], "http://127.0.0.1/?foo=bar") def test_query_string_percent_str(self): self.scope["query_string"] = "foo%3Dbar" attrs = otel_asgi.collect_request_attributes(self.scope) self.assertEqual(attrs[HTTP_URL], "http://127.0.0.1/?foo=bar") def test_response_attributes(self): otel_asgi.set_status_code(self.span, 404) expected = (mock.call(HTTP_STATUS_CODE, 404),) self.assertEqual(self.span.set_attribute.call_count, 1) self.assertEqual(self.span.set_attribute.call_count, 1) self.span.set_attribute.assert_has_calls(expected, any_order=True) def test_response_attributes_new_semconv(self): otel_asgi.set_status_code( self.span, 404, None, _StabilityMode.HTTP, ) expected = (mock.call(HTTP_RESPONSE_STATUS_CODE, 404),) self.assertEqual(self.span.set_attribute.call_count, 1) self.assertEqual(self.span.set_attribute.call_count, 1) self.span.set_attribute.assert_has_calls(expected, any_order=True) def test_response_attributes_both_semconv(self): otel_asgi.set_status_code( self.span, 404, None, _StabilityMode.HTTP_DUP, ) expected = (mock.call(HTTP_STATUS_CODE, 404),) expected2 = (mock.call(HTTP_RESPONSE_STATUS_CODE, 404),) self.assertEqual(self.span.set_attribute.call_count, 2) self.assertEqual(self.span.set_attribute.call_count, 2) self.span.set_attribute.assert_has_calls(expected, any_order=True) self.span.set_attribute.assert_has_calls(expected2, any_order=True) def test_response_attributes_invalid_status_code(self): otel_asgi.set_status_code(self.span, "Invalid Status Code") self.assertEqual(self.span.set_status.call_count, 1) def test_remove_sensitive_params(self): self.scope["server"] = ("username:password@mock", 80) self.scope["path"] = "/status/200" self.scope["query_string"] = b"X-Goog-Signature=1234567890" attrs = otel_asgi.collect_request_attributes(self.scope) self.assertEqual( attrs[HTTP_URL], "http://REDACTED:REDACTED@mock/status/200?X-Goog-Signature=REDACTED", ) def test_collect_target_attribute_missing(self): self.assertIsNone(otel_asgi._collect_target_attribute(self.scope)) def test_collect_target_attribute_fastapi(self): class TestRoute: path_format = "/api/users/{user_id}" self.scope["route"] = TestRoute() self.assertEqual( otel_asgi._collect_target_attribute(self.scope), "/api/users/{user_id}", ) def test_collect_target_attribute_fastapi_mounted(self): class TestRoute: path_format = "/users/{user_id}" self.scope["route"] = TestRoute() self.scope["root_path"] = "/api/v2" self.assertEqual( otel_asgi._collect_target_attribute(self.scope), "/api/v2/users/{user_id}", ) def test_collect_target_attribute_fastapi_starlette_invalid(self): self.scope["route"] = object() self.assertIsNone( otel_asgi._collect_target_attribute(self.scope), "HTTP_TARGET values is not None", ) class TestWrappedApplication(AsyncAsgiTestBase): async def test_mark_span_internal_in_presence_of_span_from_other_framework( self, ): tracer_provider, exporter = TestBase.create_tracer_provider() tracer = tracer_provider.get_tracer(__name__) app = otel_asgi.OpenTelemetryMiddleware(simple_asgi, tracer_provider=tracer_provider) # Wrapping the otel intercepted app with server span async def wrapped_app(scope, receive, send): with tracer.start_as_current_span("test", kind=SpanKind.SERVER) as _: await app(scope, receive, send) self.seed_app(wrapped_app) await self.send_default_request() await self.get_all_output() span_list = exporter.get_finished_spans() self.assertEqual(SpanKind.INTERNAL, span_list[0].kind) self.assertEqual(SpanKind.INTERNAL, span_list[1].kind) self.assertEqual(SpanKind.INTERNAL, span_list[2].kind) self.assertEqual(trace_api.SpanKind.INTERNAL, span_list[3].kind) # SERVER "test" self.assertEqual(SpanKind.SERVER, span_list[4].kind) # internal span should be child of the test span we have provided self.assertEqual(span_list[4].context.span_id, span_list[3].parent.span_id) class TestAsgiApplicationRaisingError(AsyncAsgiTestBase): def tearDown(self): pass async def test_asgi_value_error_exception(self): """ Test that exception UnboundLocalError local variable 'start' referenced before assignment is not raised See https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1883 """ async def bad_app(_scope, _receive, _send): raise ValueError("whatever") app = otel_asgi.OpenTelemetryMiddleware(bad_app) self.seed_app(app) await self.send_default_request() try: await self.communicator.wait() except ValueError as exc_info: self.assertEqual(exc_info.args[0], "whatever") except Exception as exc_info: # pylint: disable=W0703 self.fail("expecting ValueError('whatever'), received instead: " + str(exc_info)) else: self.fail("expecting ValueError('whatever')") if __name__ == "__main__": unittest.main() test_getter.py000066400000000000000000000037201527070072300360560ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asgi/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from opentelemetry.instrumentation.asgi import ASGIGetter class TestASGIGetter(TestCase): def test_get_none_empty_carrier(self): getter = ASGIGetter() carrier = {} val = getter.get(carrier, "test") self.assertIsNone(val) def test_get_none_empty_headers(self): getter = ASGIGetter() carrier = {"headers": []} val = getter.get(carrier, "test") self.assertIsNone(val) def test_get_(self): getter = ASGIGetter() carrier = {"headers": [(b"test-key", b"val")]} expected_val = ["val"] self.assertEqual( getter.get(carrier, "Test-Key"), expected_val, "Should be case insensitive", ) self.assertEqual( getter.get(carrier, "test-key"), expected_val, "Should be case insensitive", ) self.assertEqual( getter.get(carrier, "TEST-KEY"), expected_val, "Should be case insensitive", ) def test_keys_empty_carrier(self): getter = ASGIGetter() keys = getter.keys({}) self.assertEqual(keys, []) def test_keys_empty_headers(self): getter = ASGIGetter() keys = getter.keys({"headers": []}) self.assertEqual(keys, []) def test_keys(self): getter = ASGIGetter() carrier = {"headers": [(b"test-key", b"val")]} expected_val = ["test-key"] self.assertEqual( getter.keys(carrier), expected_val, "Should be equal", ) def test_non_utf8_headers(self): getter = ASGIGetter() carrier = {"headers": [(b"test-key", b"Moto Z\xb2")]} expected_val = ["Moto Z²"] self.assertEqual( getter.get(carrier, "test-key"), expected_val, "Should be equal", ) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncclick/000077500000000000000000000000001527070072300332665ustar00rootroot00000000000000README.rst000066400000000000000000000012261527070072300346770ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncclickOpenTelemetry asyncclick Instrumentation ======================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-asyncclick.svg :target: https://pypi.org/project/opentelemetry-instrumentation-asyncclick/ This library allows tracing requests made by the asyncclick fork of the click library. Installation ------------ :: pip install opentelemetry-instrumentation-asyncclick References ---------- * `OpenTelemetry asyncclick/ Tracing `_ * `OpenTelemetry Project `_ pyproject.toml000066400000000000000000000032351527070072300361260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncclick[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-asyncclick" dynamic = ["version"] description = "Async Click instrumentation for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "wrapt ~= 1.0", "typing_extensions ~= 4.12", ] [project.optional-dependencies] instruments = [ "asyncclick ~= 8.0", ] [project.entry-points.opentelemetry_instrumentor] asyncclick = "opentelemetry.instrumentation.asyncclick:AsyncClickInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-asyncclick" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/asyncclick/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncclick/src/000077500000000000000000000000001527070072300340555ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300366725ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncclick/srcinstrumentation/000077500000000000000000000000001527070072300421355ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetryasyncclick/000077500000000000000000000000001527070072300442605ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation__init__.py000066400000000000000000000076441527070072300464040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Instrument `asyncclick`_ CLI applications. The instrumentor will avoid instrumenting well-known servers (e.g. *flask run* and *uvicorn*) to avoid unexpected effects like every request having the same Trace ID. .. _asyncclick: https://pypi.org/project/asyncclick/ Usage ----- .. code-block:: python import asyncio import asyncclick from opentelemetry.instrumentation.asyncclick import AsyncClickInstrumentor AsyncClickInstrumentor().instrument() @asyncclick.command() async def hello(): asyncclick.echo(f"Hello world!") if __name__ == "__main__": asyncio.run(hello()) API --- """ from __future__ import annotations import os import sys from collections.abc import Awaitable, Callable, Collection from functools import partial from logging import getLogger from typing import ( TYPE_CHECKING, Any, TypeVar, ) import asyncclick from typing_extensions import ParamSpec, Unpack from wrapt import ( wrap_function_wrapper, # type: ignore[reportUnknownVariableType] ) from opentelemetry import trace from opentelemetry.instrumentation.asyncclick.package import _instruments from opentelemetry.instrumentation.asyncclick.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import ( unwrap, ) from opentelemetry.semconv._incubating.attributes.process_attributes import ( PROCESS_COMMAND_ARGS, PROCESS_EXECUTABLE_NAME, PROCESS_EXIT_CODE, PROCESS_PID, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.trace.status import StatusCode if TYPE_CHECKING: from typing import TypedDict class InstrumentKwargs(TypedDict, total=False): tracer_provider: trace.TracerProvider class UninstrumentKwargs(TypedDict, total=False): pass _logger = getLogger(__name__) T = TypeVar("T") P = ParamSpec("P") async def _command_invoke_wrapper( wrapped: Callable[P, Awaitable[T]], instance: asyncclick.core.Command, args: tuple[Any, ...], kwargs: dict[str, Any], tracer: trace.Tracer, ) -> T: # Subclasses of Command include groups and CLI runners, but # we only want to instrument the actual commands which are # instances of Command itself. if instance.__class__ != asyncclick.Command: return await wrapped(*args, **kwargs) ctx = args[0] span_name = ctx.info_name span_attributes = { PROCESS_COMMAND_ARGS: sys.argv, PROCESS_EXECUTABLE_NAME: sys.argv[0], PROCESS_EXIT_CODE: 0, PROCESS_PID: os.getpid(), } with tracer.start_as_current_span( name=span_name, kind=trace.SpanKind.INTERNAL, attributes=span_attributes, ) as span: try: result = await wrapped(*args, **kwargs) return result except Exception as exc: span.set_status(StatusCode.ERROR, str(exc)) if span.is_recording(): span.set_attribute(ERROR_TYPE, type(exc).__qualname__) span.set_attribute(PROCESS_EXIT_CODE, getattr(exc, "exit_code", 1)) raise # pylint: disable=no-self-use class AsyncClickInstrumentor(BaseInstrumentor): """An instrumentor for asyncclick""" def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs: Unpack[InstrumentKwargs]) -> None: tracer_provider = kwargs.get("tracer_provider") tracer = trace.get_tracer( __name__, __version__, tracer_provider, ) wrap_function_wrapper( asyncclick.core.Command, "invoke", partial(_command_invoke_wrapper, tracer=tracer), ) def _uninstrument(self, **kwargs: Unpack[UninstrumentKwargs]) -> None: unwrap(asyncclick.core.Command, "invoke") package.py000066400000000000000000000001641527070072300462260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("asyncclick ~= 8.0",) py.typed000066400000000000000000000000001527070072300457450ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclickversion.py000066400000000000000000000001441527070072300463160ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000004371527070072300374540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncclick asyncclick~=8.0 Flask~=3.0 pytest==7.4.4 anyio~=4.5 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-asyncclick -e instrumentation/opentelemetry-instrumentation-flask -e instrumentation/opentelemetry-instrumentation-wsgi -e util/opentelemetry-util-http opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncclick/tests/000077500000000000000000000000001527070072300344305ustar00rootroot00000000000000test_asyncclick.py000066400000000000000000000273471527070072300401220ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncclick/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import asyncio import os from typing import Any from unittest import IsolatedAsyncioTestCase, mock import asyncclick import asyncclick.testing from asyncclick.testing import CliRunner from opentelemetry.instrumentation.asyncclick import AsyncClickInstrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind from opentelemetry.trace.status import StatusCode def run_asyncclick_command_test( command: asyncclick.core.Command, args: tuple[Any, ...] = (), **kwargs: Any ) -> asyncclick.testing.Result: """ Run an asyncclick command and return the result. Args: command: The AsyncClick command to run args: Command-line arguments **kwargs: Additional arguments for CliRunner.invoke Returns: The result of invoking the command """ async def _run() -> asyncclick.testing.Result: runner = CliRunner() return await runner.invoke(command, args, **kwargs) return asyncio.run(_run()) class ClickTestCase(TestBase, IsolatedAsyncioTestCase): # pylint: disable=unbalanced-tuple-unpacking def setUp(self): # pylint: disable=invalid-name super().setUp() AsyncClickInstrumentor().instrument() def tearDown(self): # pylint: disable=invalid-name super().tearDown() AsyncClickInstrumentor().uninstrument() @mock.patch("sys.argv", ["command.py"]) def test_cli_command_wrapping(self): @asyncclick.command() async def command() -> None: pass result = run_asyncclick_command_test(command) self.assertEqual(result.exit_code, 0) (span,) = self.memory_exporter.get_finished_spans() self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.kind, SpanKind.INTERNAL) self.assertEqual(span.name, "command") self.assertEqual( dict(span.attributes), { "process.executable.name": "command.py", "process.command_args": ("command.py",), "process.exit.code": 0, "process.pid": os.getpid(), }, ) @mock.patch("sys.argv", ["command.py"]) def test_cli_command_wrapping_with_name(self): @asyncclick.command("mycommand") async def renamedcommand() -> None: pass result = run_asyncclick_command_test(renamedcommand) self.assertEqual(result.exit_code, 0) (span,) = self.memory_exporter.get_finished_spans() self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.kind, SpanKind.INTERNAL) self.assertEqual(span.name, "mycommand") self.assertEqual( dict(span.attributes), { "process.executable.name": "command.py", "process.command_args": ("command.py",), "process.exit.code": 0, "process.pid": os.getpid(), }, ) @mock.patch("sys.argv", ["command.py", "--opt", "argument"]) def test_cli_command_wrapping_with_options(self): @asyncclick.command() @asyncclick.argument("argument") @asyncclick.option("--opt/--no-opt", default=False) async def command(argument: str, opt: str) -> None: pass argv = ["command.py", "--opt", "argument"] result = run_asyncclick_command_test(command, argv[1:]) self.assertEqual(result.exit_code, 0) (span,) = self.memory_exporter.get_finished_spans() self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.kind, SpanKind.INTERNAL) self.assertEqual(span.name, "command") self.assertEqual( dict(span.attributes), { "process.executable.name": "command.py", "process.command_args": tuple(argv), "process.exit.code": 0, "process.pid": os.getpid(), }, ) @mock.patch("sys.argv", ["command-raises.py"]) def test_cli_command_raises_error(self): @asyncclick.command() async def command_raises() -> None: raise ValueError() result = run_asyncclick_command_test(command_raises) self.assertEqual(result.exit_code, 1) (span,) = self.memory_exporter.get_finished_spans() self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(span.kind, SpanKind.INTERNAL) self.assertEqual(span.name, "command-raises") self.assertEqual( dict(span.attributes), { "process.executable.name": "command-raises.py", "process.command_args": ("command-raises.py",), "process.exit.code": 1, "process.pid": os.getpid(), "error.type": "ValueError", }, ) def test_uninstrument(self): AsyncClickInstrumentor().uninstrument() @asyncclick.command() async def notracecommand() -> None: pass result = run_asyncclick_command_test(notracecommand) self.assertEqual(result.exit_code, 0) self.assertFalse(self.memory_exporter.get_finished_spans()) AsyncClickInstrumentor().instrument() @mock.patch("sys.argv", ["command.py", "sub1", "sub2"]) def test_nested_command_groups(self): """Test instrumentation of nested command groups.""" @asyncclick.group() async def cli() -> None: pass @cli.group() async def sub1() -> None: pass @sub1.command() async def sub2() -> None: pass result = run_asyncclick_command_test(cli, ["sub1", "sub2"]) self.assertEqual(result.exit_code, 0) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) # Only the leaf command should be instrumented span = spans[0] self.assertEqual(span.name, "sub2") self.assertEqual( dict(span.attributes), { "process.executable.name": "command.py", "process.command_args": ("command.py", "sub1", "sub2"), "process.exit.code": 0, "process.pid": os.getpid(), }, ) @mock.patch("sys.argv", ["command.py"]) def test_command_with_callback(self): """Test instrumentation of commands with callbacks.""" callback_called = False def callback_func(ctx: asyncclick.Context, param: asyncclick.Parameter, value: bool) -> bool: nonlocal callback_called callback_called = True return value @asyncclick.command() @asyncclick.option("--option", callback=callback_func, default="default") async def command(option: str) -> None: pass result = run_asyncclick_command_test(command) self.assertEqual(result.exit_code, 0) self.assertTrue(callback_called) (span,) = self.memory_exporter.get_finished_spans() self.assertEqual(span.name, "command") self.assertEqual(span.status.status_code, StatusCode.UNSET) @mock.patch("sys.argv", ["command.py"]) def test_command_with_result_callback(self): """Test instrumentation with result callbacks.""" callback_called = False @asyncclick.group(chain=True) async def cli() -> None: pass @cli.result_callback() async def process_result(result: asyncclick.testing.Result) -> None: nonlocal callback_called callback_called = True @cli.command() async def command() -> None: pass result = run_asyncclick_command_test(cli, ["command"]) self.assertEqual(result.exit_code, 0) self.assertTrue(callback_called) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "command") @mock.patch("sys.argv", ["command.py"]) def test_command_chaining(self): """Test instrumentation with command chaining.""" @asyncclick.group(chain=True) async def cli() -> None: pass @cli.command() async def cmd1() -> None: return "result1" @cli.command() async def cmd2() -> None: return "result2" result = run_asyncclick_command_test(cli, ["cmd1", "cmd2"]) self.assertEqual(result.exit_code, 0) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # Spans should be ordered by execution self.assertEqual(spans[0].name, "cmd1") self.assertEqual(spans[1].name, "cmd2") @mock.patch("sys.argv", ["command.py"]) def test_custom_exit_codes(self) -> None: """Test instrumentation with custom exit codes.""" @asyncclick.command() async def command() -> None: raise asyncclick.exceptions.Exit(code=42) result = run_asyncclick_command_test(command) self.assertEqual(result.exit_code, 42) (span,) = self.memory_exporter.get_finished_spans() self.assertEqual(span.name, "command") self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(span.attributes["process.exit.code"], 42) self.assertEqual(span.attributes["error.type"], "Exit") @mock.patch("sys.argv", ["command.py"]) def test_context_object_passing(self): """Test that instrumentation preserves context object passing.""" @asyncclick.group() @asyncclick.option("--debug/--no-debug", default=False) @asyncclick.pass_context async def cli(ctx: asyncclick.Context, debug: bool) -> None: ctx.ensure_object(dict) ctx.obj["DEBUG"] = debug @cli.command() @asyncclick.pass_context async def command(ctx: asyncclick.Context) -> None: assert isinstance(ctx.obj, dict) assert "DEBUG" in ctx.obj result = run_asyncclick_command_test(cli, ["--debug", "command"]) self.assertEqual(result.exit_code, 0) (span,) = self.memory_exporter.get_finished_spans() self.assertEqual(span.name, "command") def test_multiple_instrumentation(self): """Test that instrumenting multiple times only applies once.""" # Already instrumented in setUp, instrument again AsyncClickInstrumentor().instrument() @asyncclick.command() async def command() -> None: pass result = run_asyncclick_command_test(command) self.assertEqual(result.exit_code, 0) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) def test_concurrency(self): """Test instrumentation with concurrent command execution.""" @asyncclick.command() async def command1() -> None: pass @asyncclick.command() async def command2() -> None: pass async def run_both() -> tuple[asyncclick.testing.Result, asyncclick.testing.Result]: runner = CliRunner() task1 = asyncio.create_task(runner.invoke(command1)) task2 = asyncio.create_task(runner.invoke(command2)) results = await asyncio.gather(task1, task2) return results results = asyncio.run(run_both()) self.assertEqual(results[0].exit_code, 0) self.assertEqual(results[1].exit_code, 0) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) span_names = [span.name for span in spans] self.assertIn("command1", span_names) self.assertIn("command2", span_names) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/000077500000000000000000000000001527070072300326105ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/LICENSE000066400000000000000000000261351527070072300336240ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/README.rst000066400000000000000000000063421527070072300343040ustar00rootroot00000000000000OpenTelemetry asyncio Instrumentation ====================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-asyncio.svg :target: https://pypi.org/project/opentelemetry-instrumentation-asyncio/ AsyncioInstrumentor: Tracing Requests Made by the Asyncio Library The opentelemetry-instrumentation-asyncio package allows tracing asyncio applications. It also includes metrics for duration and counts of coroutines and futures. Metrics are generated even if coroutines are not traced. Set the names of coroutines you want to trace. ------------------------------------------------- .. code:: bash export OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE=coro_name,coro_name2,coro_name3 If you want to trace specific blocking functions executed with the ``to_thread`` function of asyncio, set the name of the functions in ``OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE``. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ .. code:: bash export OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE=func_name,func_name2,func_name3 You can enable tracing futures with ``OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED`` ----------------------------------------------------------------------------------------------- .. code:: bash export OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED=true Run instrumented application ----------------------------- 1. coroutine -------------------- .. code:: python # export OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE=sleep import asyncio from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor AsyncioInstrumentor().instrument() async def main(): await asyncio.create_task(asyncio.sleep(0.1)) asyncio.run(main()) 2. future -------------------- .. code:: python # export OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED=true import asyncio from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor AsyncioInstrumentor().instrument() async def main(): future = asyncio.Future() future.set_result(1) await asyncio.ensure_future(future) asyncio.run(main()) 3. to_thread -------------------- .. code:: python # export OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE=func import asyncio from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor AsyncioInstrumentor().instrument() async def main(): await asyncio.to_thread(func) def func(): pass asyncio.run(main()) asyncio metric types ---------------------- * `asyncio.process.duration` (seconds) - Duration of asyncio process * `asyncio.process.created` ({process}) - Number of asyncio process API --- Installation ------------ :: pip install opentelemetry-instrumentation-asyncio References ----------- * `OpenTelemetry asyncio/ Tracing `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031451527070072300354500ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-asyncio" dynamic = ["version"] description = "OpenTelemetry instrumentation for asyncio" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.14", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [] [project.entry-points.opentelemetry_instrumentor] asyncio = "opentelemetry.instrumentation.asyncio:AsyncioInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-asyncio" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/asyncio/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/src/000077500000000000000000000000001527070072300333775ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300362145ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/srcinstrumentation/000077500000000000000000000000001527070072300414575ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetryasyncio/000077500000000000000000000000001527070072300431245ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation__init__.py000066400000000000000000000306151527070072300452420ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ .. asyncio: https://github.com/python/asyncio The opentelemetry-instrumentation-asyncio package allows tracing asyncio applications. The metric for coroutine, future, is generated even if there is no setting to generate a span. Run instrumented application ------------------------------ 1. coroutine ---------------- .. code:: python # export OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE=sleep import asyncio from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor AsyncioInstrumentor().instrument() async def main(): await asyncio.create_task(asyncio.sleep(0.1)) asyncio.run(main()) 2. future ------------ .. code:: python # export OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED=true import asyncio from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor AsyncioInstrumentor().instrument() async def main(): future = asyncio.Future() future.set_result(1) await asyncio.ensure_future(future) asyncio.run(main()) 3. to_thread ------------- .. code:: python # export OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE=func import asyncio from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor AsyncioInstrumentor().instrument() async def main(): await asyncio.to_thread(func) def func(): pass asyncio.run(main()) asyncio metric types --------------------- * asyncio.process.duration (seconds) - Duration of asyncio process * asyncio.process.created ({process}) - Number of asyncio process API --- """ import asyncio import functools import sys from asyncio import futures from collections.abc import Callable, Collection from timeit import default_timer from typing import ParamSpec, TypeVar from wrapt import wrap_function_wrapper as _wrap from opentelemetry.instrumentation.asyncio.instrumentation_state import ( _is_instrumented, ) from opentelemetry.instrumentation.asyncio.package import _instruments from opentelemetry.instrumentation.asyncio.utils import ( get_coros_to_trace, get_future_trace_enabled, get_to_thread_to_trace, ) from opentelemetry.instrumentation.asyncio.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap from opentelemetry.metrics import get_meter from opentelemetry.trace import get_tracer from opentelemetry.trace.status import Status, StatusCode P = ParamSpec("P") R = TypeVar("R") ASYNCIO_PREFIX = "asyncio" class AsyncioInstrumentor(BaseInstrumentor): """ An instrumentor for asyncio See `BaseInstrumentor` """ methods_with_coroutine = [ "create_task", "ensure_future", "wait_for", "wait", "as_completed", "run_coroutine_threadsafe", ] def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): self._tracer = get_tracer(__name__, __version__, kwargs.get("tracer_provider")) self._meter = get_meter(__name__, __version__, kwargs.get("meter_provider")) self._coros_name_to_trace = get_coros_to_trace() self._future_active_enabled = get_future_trace_enabled() self._to_thread_name_to_trace = get_to_thread_to_trace() self.process_duration_histogram = self._meter.create_histogram( name="asyncio.process.duration", description="Duration of asyncio process", unit="s", ) self.process_created_counter = self._meter.create_counter( name="asyncio.process.created", description="Number of asyncio process", unit="{process}", ) for method in self.methods_with_coroutine: self.instrument_method_with_coroutine(method) self.instrument_gather() self.instrument_to_thread() self.instrument_taskgroup_create_task() def _uninstrument(self, **kwargs): for method in self.methods_with_coroutine: uninstrument_method_with_coroutine(method) uninstrument_gather() uninstrument_to_thread() uninstrument_taskgroup_create_task() def instrument_method_with_coroutine(self, method_name: str): """ Instruments specified asyncio method. """ def wrap_coro_or_future(method, instance, args, kwargs): # If the first argument is a coroutine or future, # we decorate it with a span and return the task. if args and len(args) > 0: first_arg = args[0] # Check if it's a coroutine or future and wrap it if asyncio.iscoroutine(first_arg) or futures.isfuture(first_arg): args = (self.trace_item(first_arg),) + args[1:] # Check if it's a list and wrap each item elif isinstance(first_arg, list): args = ([self.trace_item(item) for item in first_arg],) + args[1:] return method(*args, **kwargs) _wrap(asyncio, method_name, wrap_coro_or_future) def instrument_gather(self): def wrap_coros_or_futures(method, instance, args, kwargs): if args and len(args) > 0: # Check if it's a coroutine or future and wrap it wrapped_args = tuple(self.trace_item(item) for item in args) return method(*wrapped_args, **kwargs) return method(*args, **kwargs) _wrap(asyncio, "gather", wrap_coros_or_futures) def instrument_to_thread(self) -> None: def wrap_to_thread(method, instance, args, kwargs) -> None: if args: first_arg = args[0] # Wrap the first argument wrapped_first_arg = self.wrap_to_thread_func(first_arg) wrapped_args = (wrapped_first_arg,) + args[1:] return method(*wrapped_args, **kwargs) return method(*args, **kwargs) _wrap(asyncio, "to_thread", wrap_to_thread) def instrument_taskgroup_create_task(self) -> None: # TaskGroup.create_task was added in Python 3.11 if sys.version_info < (3, 11): return def wrap_taskgroup_create_task(method, instance, args, kwargs) -> None: if args: coro = args[0] wrapped_coro = self.trace_coroutine(coro) wrapped_args = (wrapped_coro,) + args[1:] return method(*wrapped_args, **kwargs) return method(*args, **kwargs) _wrap( asyncio.TaskGroup, # pylint: disable=no-member "create_task", wrap_taskgroup_create_task, ) def trace_to_thread(self, func: Callable[P, R]) -> Callable[P, R]: return self.wrap_to_thread_func(func) def wrap_to_thread_func(self, func: Callable[P, R]) -> Callable[P, R]: """ Wrap a function so that its execution in the worker thread is measured and, if enabled, traced. """ func_name = _get_func_name(func) @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: start = default_timer() span = ( self._tracer.start_span(f"{ASYNCIO_PREFIX} to_thread-{func_name}") if func_name in self._to_thread_name_to_trace else None ) attr = {"type": "to_thread", "name": func_name} exception = None try: result = func(*args, **kwargs) attr["state"] = "finished" return result # Match trace_coroutine and trace_future: a cancellation is # recorded as state="cancelled" but not as a span error. except asyncio.CancelledError: attr["state"] = "cancelled" raise except BaseException as exc: exception = exc attr["state"] = determine_state(exc) raise finally: self.record_process(start, attr, span, exception) return wrapper def trace_item(self, coro_or_future): """Trace a coroutine or future item.""" # Task is already traced, return it if isinstance(coro_or_future, asyncio.Task): return coro_or_future if asyncio.iscoroutine(coro_or_future): return self.trace_coroutine(coro_or_future) if futures.isfuture(coro_or_future): return self.trace_future(coro_or_future) return coro_or_future async def trace_coroutine(self, coro): """ Wrap a coroutine so that we measure its duration, metrics, etc. If already instrumented, simply 'await coro' to preserve call behavior. """ if _is_instrumented(coro): return await coro if not hasattr(coro, "__name__"): return await coro start = default_timer() attr = { "type": "coroutine", "name": coro.__name__, } span = ( self._tracer.start_span(f"{ASYNCIO_PREFIX} coro-" + coro.__name__) if coro.__name__ in self._coros_name_to_trace else None ) exception = None try: attr["state"] = "finished" return await coro # CancelledError is raised when a coroutine is cancelled # before it has a chance to run. We don't want to record # this as an error. # Still it needs to be raised in order for `asyncio.wait_for` # to properly work with timeout and raise accordingly `asyncio.TimeoutError` except asyncio.CancelledError: attr["state"] = "cancelled" raise except Exception as exc: exception = exc state = determine_state(exception) attr["state"] = state raise finally: self.record_process(start, attr, span, exception) def trace_future(self, future): """ Wrap a Future's done callback. If already instrumented, skip re-wrapping. """ if _is_instrumented(future): return future start = default_timer() span = self._tracer.start_span(f"{ASYNCIO_PREFIX} future") if self._future_active_enabled else None def callback(f): attr = { "type": "future", "state": ("cancelled" if f.cancelled() else determine_state(f.exception())), } self.record_process(start, attr, span, None if f.cancelled() else f.exception()) future.add_done_callback(callback) return future def record_process(self, start: float, attr: dict, span=None, exception=None) -> None: """ Record the processing time, update histogram and counter, and handle span. :param start: Start time of the process. :param attr: Attributes for the histogram and counter. :param span: Optional span for tracing. :param exception: Optional exception occurred during the process. """ duration = max(default_timer() - start, 0) self.process_duration_histogram.record(duration, attr) self.process_created_counter.add(1, attr) if span: if span.is_recording() and exception: span.set_status(Status(StatusCode.ERROR)) span.record_exception(exception) span.end() def _get_func_name(func: object) -> str | None: func_name = getattr(func, "__name__", None) if func_name is None and isinstance(func, functools.partial): func_name = func.func.__name__ return func_name def determine_state(exception: BaseException) -> str: if isinstance(exception, asyncio.CancelledError): return "cancelled" if isinstance(exception, asyncio.TimeoutError): return "timeout" if exception: return "exception" return "finished" def uninstrument_taskgroup_create_task() -> None: # TaskGroup.create_task was added in Python 3.11 if sys.version_info < (3, 11): return unwrap(asyncio.TaskGroup, "create_task") # pylint: disable=no-member def uninstrument_to_thread() -> None: unwrap(asyncio, "to_thread") def uninstrument_gather() -> None: unwrap(asyncio, "gather") def uninstrument_method_with_coroutine(method_name: str) -> None: """ Uninstrument specified asyncio method. """ unwrap(asyncio, method_name) environment_variables.py000066400000000000000000000015601527070072300500740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ .. envvar:: OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE Enter the names of the coroutines to be traced through the environment variable below, separated by commas. .. envvar:: OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED To determine whether the tracing feature for Future of Asyncio in Python is enabled or not. .. envvar:: OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE Enter the names of the functions to be traced through the environment variable below, separated by commas. """ OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE = "OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE" OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED = "OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED" OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE = "OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE" instrumentation_state.py000066400000000000000000000032341527070072300501430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Instrumentation State Tracker This module provides helper functions to safely track whether a coroutine, Future, or function has already been instrumented by the OpenTelemetry asyncio instrumentation layer. Some Python objects (like coroutines or functions) may not support setting custom attributes or weak references. To avoid memory leaks and runtime errors, this module uses a WeakKeyDictionary to safely track instrumented objects. If an object cannot be weak-referenced, it is silently skipped. Usage: if not _is_instrumented(obj): _mark_instrumented(obj) # instrument the object... """ import weakref from typing import Any # A global WeakSet to track instrumented objects. # Entries are automatically removed when the objects are garbage collected. _instrumented_tasks = weakref.WeakSet() def _is_instrumented(obj: Any) -> bool: """ Check whether the object has already been instrumented. If not, mark it as instrumented (only if weakref is supported). Args: obj: A coroutine, function, or Future. Returns: True if the object was already instrumented. False if the object is not trackable (no weakref support), or just marked now. Note: In Python 3.12+, some internal types like `async_generator_asend` raise TypeError when weakref is attempted. """ try: if obj in _instrumented_tasks: return True _instrumented_tasks.add(obj) return False except TypeError: # Object doesn't support weak references → can't track instrumentation return False package.py000066400000000000000000000001441527070072300450700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = tuple() utils.py000066400000000000000000000027271527070072300446460ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os # pylint: disable=no-name-in-module from opentelemetry.instrumentation.asyncio.environment_variables import ( OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE, OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED, OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE, ) def separate_coro_names_by_comma(coro_names: str) -> set[str]: """ Function to separate the coroutines to be traced by comma """ if coro_names is None: return set() return {coro_name.strip() for coro_name in coro_names.split(",")} def get_coros_to_trace() -> set: """ Function to get the coroutines to be traced from the environment variable """ coro_names = os.getenv(OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE) return separate_coro_names_by_comma(coro_names) def get_future_trace_enabled() -> bool: """ Function to get the future active enabled flag from the environment variable default value is False """ return os.getenv(OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED, "False").lower() == "true" def get_to_thread_to_trace() -> set: """ Function to get the functions to be traced from the environment variable """ func_names = os.getenv(OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE) return separate_coro_names_by_comma(func_names) __all__ = [ "get_coros_to_trace", "get_future_trace_enabled", "get_to_thread_to_trace", ] version.py000066400000000000000000000001441527070072300451620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000004441527070072300367740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncioasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 pytest-asyncio==0.23.5 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-asyncio opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/tests/000077500000000000000000000000001527070072300337525ustar00rootroot00000000000000__init__.py000066400000000000000000000001141527070072300360000ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 common_test_func.py000066400000000000000000000013241527070072300376070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio async def async_func(): await asyncio.sleep(0.1) async def factorial(number: int): factorial_value = 1 for value in range(2, number + 1): await asyncio.sleep(0) factorial_value *= value return factorial_value async def cancellable_coroutine(): await asyncio.sleep(2) async def cancellation_coro(): task = asyncio.create_task(cancellable_coroutine()) await asyncio.sleep(0.1) task.cancel() await task async def cancellation_create_task(): await asyncio.create_task(cancellation_coro()) async def ensure_future(): await asyncio.ensure_future(asyncio.sleep(0)) test_asyncio_anext.py000066400000000000000000000034541527070072300401560ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio from unittest.mock import patch # pylint: disable=no-name-in-module from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor from opentelemetry.instrumentation.asyncio.environment_variables import ( OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import get_tracer class TestAsyncioAnext(TestBase): @patch.dict( "os.environ", {OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "async_func"}, ) def setUp(self): super().setUp() AsyncioInstrumentor().instrument() self._tracer = get_tracer( __name__, ) def tearDown(self): super().tearDown() AsyncioInstrumentor().uninstrument() # Asyncio anext() does not have __name__ attribute, which is used to determine if the coroutine should be traced. # This test is to ensure that the instrumentation does not break when the coroutine does not have __name__ attribute. # Additionally, ensure the coroutine is actually awaited. def test_asyncio_anext(self): async def main(): async def async_gen(): # nothing special about this range other than to avoid returning a zero # from a function named 'main' (which might cause confusion about intent) for it in range(2, 4): yield it async_gen_instance = async_gen() agen = anext(async_gen_instance) return await asyncio.create_task(agen) ret = asyncio.run(main()) self.assertEqual(ret, 2) # first iteration from range() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) test_asyncio_cancellation.py000066400000000000000000000047511527070072300414740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio from unittest.mock import patch # pylint: disable=no-name-in-module from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor from opentelemetry.instrumentation.asyncio.environment_variables import ( OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind, get_tracer from .common_test_func import cancellation_create_task class TestAsyncioCancel(TestBase): @patch.dict( "os.environ", {OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "cancellation_coro, cancellable_coroutine"}, ) def setUp(self): super().setUp() AsyncioInstrumentor().instrument() self._tracer = get_tracer( __name__, ) def tearDown(self): super().tearDown() AsyncioInstrumentor().uninstrument() def test_cancel(self): with self._tracer.start_as_current_span("root", kind=SpanKind.SERVER): try: asyncio.run(cancellation_create_task()) except asyncio.CancelledError: pass spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) self.assertEqual(spans[0].context.trace_id, spans[1].context.trace_id) self.assertEqual(spans[2].context.trace_id, spans[1].context.trace_id) self.assertEqual(spans[0].name, "asyncio coro-cancellable_coroutine") self.assertEqual(spans[1].name, "asyncio coro-cancellation_coro") for metric in self.memory_metrics_reader.get_metrics_data().resource_metrics[0].scope_metrics[0].metrics: if metric.name == "asyncio.process.duration": for point in metric.data.data_points: self.assertEqual(point.attributes["type"], "coroutine") self.assertIn( point.attributes["name"], ["cancellation_coro", "cancellable_coroutine"], ) if metric.name == "asyncio.process.created": for point in metric.data.data_points: self.assertEqual(point.attributes["type"], "coroutine") self.assertIn( point.attributes["name"], ["cancellation_coro", "cancellable_coroutine"], ) self.assertIn(point.attributes["state"], ["finished", "cancelled"]) test_asyncio_create_task.py000066400000000000000000000023371527070072300413230ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio from unittest.mock import patch # pylint: disable=no-name-in-module from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor from opentelemetry.instrumentation.asyncio.environment_variables import ( OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import get_tracer from .common_test_func import factorial class TestAsyncioCreateTask(TestBase): @patch.dict("os.environ", {OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "sleep"}) def setUp(self): super().setUp() AsyncioInstrumentor().instrument() self._tracer = get_tracer( __name__, ) def tearDown(self): super().tearDown() AsyncioInstrumentor().uninstrument() def test_asyncio_create_task(self): async def async_func(): await asyncio.create_task(asyncio.sleep(0)) await asyncio.create_task(factorial(3)) asyncio.run(async_func()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].name, "asyncio coro-sleep") test_asyncio_duplicate_instrument.py000066400000000000000000000035361527070072300433020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ A general test verifying that when the same Future objects (or coroutines) are repeatedly instrumented (for example, via `trace_future`), callback references do not leak. In this example, we mimic a typical scenario where a small set of Futures might be reused throughout an application's lifecycle. """ import asyncio from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor from opentelemetry.test.test_base import TestBase class TestAsyncioDuplicateInstrument(TestBase): """ Tests whether repeated instrumentation of the same Futures leads to exponential callback growth (potential memory leak). """ def setUp(self): super().setUp() self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.instrumentor = AsyncioInstrumentor() self.instrumentor.instrument() def tearDown(self): self.instrumentor.uninstrument() self.loop.close() asyncio.set_event_loop(None) super().tearDown() def test_duplicate_instrumentation_of_futures(self): """ If instrumentor.trace_future is called multiple times on the same Future, we should NOT see an unbounded accumulation of callbacks. """ fut1 = asyncio.Future() fut2 = asyncio.Future() num_iterations = 10 for _ in range(num_iterations): self.instrumentor.trace_future(fut1) self.instrumentor.trace_future(fut2) self.assertLessEqual( len(fut1._callbacks), 1, f"fut1 has {len(fut1._callbacks)} callbacks. Potential leak!", ) self.assertLessEqual( len(fut2._callbacks), 1, f"fut2 has {len(fut2._callbacks)} callbacks. Potential leak!", ) test_asyncio_ensure_future.py000066400000000000000000000047401527070072300417310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio from unittest.mock import patch import pytest # pylint: disable=no-name-in-module from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor from opentelemetry.instrumentation.asyncio.environment_variables import ( OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import get_tracer from .common_test_func import async_func class TestAsyncioEnsureFuture(TestBase): @patch.dict("os.environ", {OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED: "true"}) def setUp(self): super().setUp() AsyncioInstrumentor().instrument() self._tracer = get_tracer( __name__, ) def tearDown(self): super().tearDown() AsyncioInstrumentor().uninstrument() @pytest.mark.asyncio def test_asyncio_loop_ensure_future(self): """ async_func is not traced because it is not set in the environment variable """ async def test(): task = asyncio.ensure_future(async_func()) await task asyncio.run(test()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) @pytest.mark.asyncio def test_asyncio_ensure_future_with_future(self): async def test(): with self._tracer.start_as_current_span("root"): future = asyncio.Future() future.set_result(1) task = asyncio.ensure_future(future) await task asyncio.run(test()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) for span in spans: if span.name == "root": self.assertEqual(span.parent, None) if span.name == "asyncio future": self.assertNotEqual(span.parent.trace_id, 0) for metric in self.memory_metrics_reader.get_metrics_data().resource_metrics[0].scope_metrics[0].metrics: if metric.name == "asyncio.process.duration": for point in metric.data.data_points: self.assertEqual(point.attributes["type"], "future") if metric.name == "asyncio.process.created": for point in metric.data.data_points: self.assertEqual(point.attributes["type"], "future") self.assertEqual(point.attributes["state"], "finished") test_asyncio_future_cancellation.py000066400000000000000000000036141527070072300430630ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio from unittest.mock import patch from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor from opentelemetry.instrumentation.asyncio.environment_variables import ( OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import get_tracer SCOPE = "opentelemetry.instrumentation.asyncio" class TestTraceFuture(TestBase): @patch.dict("os.environ", {OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED: "true"}) def setUp(self): super().setUp() self._tracer = get_tracer( __name__, ) self.instrumentor = AsyncioInstrumentor() self.instrumentor.instrument() def tearDown(self): super().tearDown() self.instrumentor.uninstrument() def test_trace_future_cancelled(self): async def future_cancelled(): with self._tracer.start_as_current_span("root"): future = asyncio.Future() future = self.instrumentor.trace_future(future) future.cancel() try: asyncio.run(future_cancelled()) except asyncio.CancelledError as exc: self.assertEqual(isinstance(exc, asyncio.CancelledError), True) spans = self.get_finished_spans() self.assertEqual(len(spans), 2) self.assertEqual(spans[0].name, "root") self.assertEqual(spans[1].name, "asyncio future") metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 2) self.assertEqual(metrics[0].name, "asyncio.process.created") self.assertEqual(metrics[0].data.data_points[0].attributes["state"], "cancelled") self.assertEqual(metrics[1].name, "asyncio.process.duration") self.assertEqual(metrics[1].data.data_points[0].attributes["state"], "cancelled") test_asyncio_gather.py000066400000000000000000000025431527070072300403070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio from unittest.mock import patch # pylint: disable=no-name-in-module from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor from opentelemetry.instrumentation.asyncio.environment_variables import ( OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import get_tracer from .common_test_func import factorial class TestAsyncioGather(TestBase): @patch.dict( "os.environ", {OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "factorial"}, ) def setUp(self): super().setUp() AsyncioInstrumentor().instrument() self._tracer = get_tracer( __name__, ) def tearDown(self): super().tearDown() AsyncioInstrumentor().uninstrument() def test_asyncio_gather(self): async def gather_factorial(): await asyncio.gather(factorial(2), factorial(3), factorial(4)) asyncio.run(gather_factorial()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) self.assertEqual(spans[0].name, "asyncio coro-factorial") self.assertEqual(spans[1].name, "asyncio coro-factorial") self.assertEqual(spans[2].name, "asyncio coro-factorial") test_asyncio_integration.py000066400000000000000000000021641527070072300413570ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio from unittest.mock import patch # pylint: disable=no-name-in-module from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor from opentelemetry.instrumentation.asyncio.environment_variables import ( OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import get_tracer from .common_test_func import ensure_future class TestAsyncioInstrumentor(TestBase): def setUp(self): super().setUp() self._tracer = get_tracer( __name__, ) @patch.dict("os.environ", {OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "sleep"}) def test_asyncio_integration(self): AsyncioInstrumentor().instrument() asyncio.run(ensure_future()) spans = self.memory_exporter.get_finished_spans() self.memory_exporter.clear() assert spans AsyncioInstrumentor().uninstrument() asyncio.run(ensure_future()) spans = self.memory_exporter.get_finished_spans() assert not spans test_asyncio_run_coroutine_threadsafe.py000066400000000000000000000030621527070072300441130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio import threading from concurrent.futures import ThreadPoolExecutor from unittest.mock import patch # pylint: disable=no-name-in-module from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor from opentelemetry.instrumentation.asyncio.environment_variables import ( OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import get_tracer class TestRunCoroutineThreadSafe(TestBase): @patch.dict("os.environ", {OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "coro"}) def setUp(self): super().setUp() AsyncioInstrumentor().instrument() self.loop = asyncio.new_event_loop() self.executor = ThreadPoolExecutor(max_workers=1) self.loop.set_default_executor(self.executor) self.thread = threading.Thread(target=self.loop.run_forever) self.thread.start() self._tracer = get_tracer( __name__, ) def tearDown(self): super().tearDown() self.loop.call_soon_threadsafe(self.loop.stop) self.thread.join() self.loop.close() AsyncioInstrumentor().uninstrument() def test_run_coroutine_threadsafe(self): async def coro(): return 42 future = asyncio.run_coroutine_threadsafe(coro(), self.loop) result = future.result(timeout=1) self.assertEqual(result, 42) spans = self.memory_exporter.get_finished_spans() assert spans test_asyncio_taskgroup.py000066400000000000000000000030151527070072300410470ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio import sys from unittest import skipIf from unittest.mock import patch # pylint: disable=no-name-in-module from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor from opentelemetry.instrumentation.asyncio.environment_variables import ( OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import get_tracer from .common_test_func import async_func class TestAsyncioTaskgroup(TestBase): @patch.dict( "os.environ", {OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "async_func"}, ) def setUp(self): super().setUp() AsyncioInstrumentor().instrument() self._tracer = get_tracer( __name__, ) def tearDown(self): super().tearDown() AsyncioInstrumentor().uninstrument() @skipIf( sys.version_info < (3, 11), "TaskGroup is only available in Python 3.11+", ) def test_task_group_create_task(self): async def main(): async with asyncio.TaskGroup() as tg: # pylint: disable=no-member for _ in range(10): tg.create_task(async_func()) with self._tracer.start_as_current_span("root"): asyncio.run(main()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 11) self.assertEqual(spans[4].context.trace_id, spans[5].context.trace_id) test_asyncio_to_thread.py000066400000000000000000000154231527070072300410070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio import functools import time from unittest.mock import patch # pylint: disable=no-name-in-module from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor from opentelemetry.instrumentation.asyncio.environment_variables import ( OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import StatusCode, get_tracer SCOPE = "opentelemetry.instrumentation.asyncio" SLEEP_SECONDS = 0.05 class TestAsyncioToThread(TestBase): @patch.dict( "os.environ", {OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE: "multiply"}, ) def setUp(self): super().setUp() AsyncioInstrumentor().instrument() self._tracer = get_tracer( __name__, ) def tearDown(self): super().tearDown() AsyncioInstrumentor().uninstrument() def get_created_and_duration_metrics(self): metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 2) self.assertEqual(metrics[0].name, "asyncio.process.created") self.assertEqual(metrics[1].name, "asyncio.process.duration") return metrics[0], metrics[1] def test_to_thread(self): def multiply(x, y): return x * y with self._tracer.start_as_current_span("root"): result = asyncio.run(asyncio.to_thread(multiply, 2, 3)) assert result == 6 spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) assert spans[0].name == "asyncio to_thread-multiply" created, duration = self.get_created_and_duration_metrics() for metric in (created, duration): self.assertEqual(len(metric.data.data_points), 1) point = metric.data.data_points[0] self.assertEqual(point.attributes["type"], "to_thread") self.assertEqual(point.attributes["name"], "multiply") def test_to_thread_duration_covers_execution(self): def multiply(x, y): time.sleep(SLEEP_SECONDS) return x * y asyncio.run(asyncio.to_thread(multiply, 2, 3)) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] # only a lower bound is asserted: it is what fails when the span does # not cover the call, and an upper bound would be flaky on a busy CI # host where the worker thread can be descheduled. self.assertGreaterEqual( (span.end_time - span.start_time) / 10**9, SLEEP_SECONDS * 0.9, ) _, duration = self.get_created_and_duration_metrics() self.assertEqual(len(duration.data.data_points), 1) self.assertGreaterEqual( duration.data.data_points[0].sum, SLEEP_SECONDS * 0.9, ) def test_to_thread_exception(self): def multiply(x, y): raise ValueError("fail") with self.assertRaises(ValueError): asyncio.run(asyncio.to_thread(multiply, 2, 3)) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "asyncio to_thread-multiply") self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(len(span.events), 1) self.assertEqual(span.events[0].name, "exception") created, duration = self.get_created_and_duration_metrics() for metric in (created, duration): self.assertEqual(len(metric.data.data_points), 1) self.assertEqual(metric.data.data_points[0].attributes["state"], "exception") def test_to_thread_timeout_state(self): def multiply(x, y): raise asyncio.TimeoutError("fail") with self.assertRaises(asyncio.TimeoutError): asyncio.run(asyncio.to_thread(multiply, 2, 3)) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(len(span.events), 1) self.assertEqual(span.events[0].name, "exception") created, duration = self.get_created_and_duration_metrics() for metric in (created, duration): self.assertEqual(len(metric.data.data_points), 1) self.assertEqual(metric.data.data_points[0].attributes["state"], "timeout") def test_to_thread_cancelled_state(self): def multiply(x, y): raise asyncio.CancelledError() with self.assertRaises(asyncio.CancelledError): asyncio.run(asyncio.to_thread(multiply, 2, 3)) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] # cancellation is not an error, consistent with coroutines and futures self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(len(span.events), 0) created, duration = self.get_created_and_duration_metrics() for metric in (created, duration): self.assertEqual(len(metric.data.data_points), 1) self.assertEqual(metric.data.data_points[0].attributes["state"], "cancelled") def test_to_thread_repeated_calls(self): def multiply(x, y): return x * y async def to_thread(): assert await asyncio.to_thread(multiply, 2, 3) == 6 assert await asyncio.to_thread(multiply, 4, 5) == 20 asyncio.run(to_thread()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) created, duration = self.get_created_and_duration_metrics() self.assertEqual(len(created.data.data_points), 1) self.assertEqual(created.data.data_points[0].value, 2) self.assertEqual(len(duration.data.data_points), 1) self.assertEqual(duration.data.data_points[0].count, 2) def test_to_thread_partial_func(self): def multiply(x, y): return x * y double = functools.partial(multiply, 2) with self._tracer.start_as_current_span("root"): result = asyncio.run(asyncio.to_thread(double, 3)) assert result == 6 spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) assert spans[0].name == "asyncio to_thread-multiply" created, duration = self.get_created_and_duration_metrics() for metric in (created, duration): self.assertEqual(len(metric.data.data_points), 1) point = metric.data.data_points[0] self.assertEqual(point.attributes["type"], "to_thread") self.assertEqual(point.attributes["name"], "multiply") test_asyncio_utils.py000066400000000000000000000016371527070072300402000ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from unittest.mock import patch # pylint: disable=no-name-in-module from opentelemetry.instrumentation.asyncio.environment_variables import ( OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE, OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED, ) from opentelemetry.instrumentation.asyncio.utils import ( get_coros_to_trace, get_future_trace_enabled, ) class TestAsyncioToThread(TestCase): @patch.dict( "os.environ", {OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "test1,test2,test3 ,test4"}, ) def test_separator(self): self.assertEqual(get_coros_to_trace(), {"test1", "test2", "test3", "test4"}) @patch.dict("os.environ", {OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED: "true"}) def test_future_trace_enabled(self): self.assertEqual(get_future_trace_enabled(), True) test_asyncio_wait.py000066400000000000000000000060341527070072300400000ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncio/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio import sys from unittest.mock import patch # pylint: disable=no-name-in-module from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor from opentelemetry.instrumentation.asyncio.environment_variables import ( OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import get_tracer from .common_test_func import async_func class TestAsyncioWait(TestBase): @patch.dict( "os.environ", {OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "async_func"}, ) def setUp(self): super().setUp() AsyncioInstrumentor().instrument() self._tracer = get_tracer( __name__, ) def tearDown(self): super().tearDown() AsyncioInstrumentor().uninstrument() def test_asyncio_wait_with_create_task(self): async def main(): if sys.version_info >= (3, 11): # In Python 3.11, you can't send coroutines directly to asyncio.wait(). # Instead, you must wrap them in asyncio.create_task(). tasks = [ asyncio.create_task(async_func()), asyncio.create_task(async_func()), ] await asyncio.wait(tasks) else: await asyncio.wait([async_func(), async_func()]) asyncio.run(main()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) def test_asyncio_wait_for(self): async def main(): await asyncio.wait_for(async_func(), 1) await asyncio.wait_for(async_func(), 1) asyncio.run(main()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) def test_asyncio_wait_for_with_timeout(self): expected_timeout_error = None async def main(): nonlocal expected_timeout_error try: await asyncio.wait_for(async_func(), 0.01) except asyncio.TimeoutError as timeout_error: expected_timeout_error = timeout_error asyncio.run(main()) self.assertNotEqual(expected_timeout_error, None) def test_asyncio_as_completed(self): async def main(): if sys.version_info >= (3, 11): # In Python 3.11, you can't send coroutines directly to asyncio.as_completed(). # Instead, you must wrap them in asyncio.create_task(). tasks = [ asyncio.create_task(async_func()), asyncio.create_task(async_func()), ] for task in asyncio.as_completed(tasks): await task else: for task in asyncio.as_completed([async_func(), async_func()]): await task asyncio.run(main()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncpg/000077500000000000000000000000001527070072300326075ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncpg/LICENSE000066400000000000000000000261351527070072300336230ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncpg/README.rst000066400000000000000000000014401527070072300342750ustar00rootroot00000000000000OpenTelemetry asyncpg Instrumentation ===================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-asyncpg.svg :target: https://pypi.org/project/opentelemetry-instrumentation-asyncpg/ This library allows tracing PostgreSQL queries made by the `asyncpg `_ library. Installation ------------ :: pip install opentelemetry-instrumentation-asyncpg References ---------- * `OpenTelemetry asyncpg Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031401527070072300354420ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncpg[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-asyncpg" dynamic = ["version"] description = "OpenTelemetry instrumentation for AsyncPG" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", ] [project.optional-dependencies] instruments = [ "asyncpg >= 0.12.0", ] [project.entry-points.opentelemetry_instrumentor] asyncpg = "opentelemetry.instrumentation.asyncpg:AsyncPGInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-asyncpg" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/asyncpg/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncpg/src/000077500000000000000000000000001527070072300333765ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300362135ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncpg/srcinstrumentation/000077500000000000000000000000001527070072300414565ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetryasyncpg/000077500000000000000000000000001527070072300431225ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation__init__.py000066400000000000000000000257671527070072300452540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This library allows tracing PostgreSQL queries made by the `asyncpg `_ library. Usage ----- Start PostgreSQL: :: docker run -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DATABASE=database -p 5432:5432 postgres Run instrumented code: .. code-block:: python import asyncio import asyncpg from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor # You can optionally pass a custom TracerProvider to AsyncPGInstrumentor.instrument() AsyncPGInstrumentor().instrument() async def main(): conn = await asyncpg.connect(user="user", password="password") await conn.fetch('''SELECT 42;''') await conn.close() asyncio.run(main()) API --- """ import re from collections.abc import Collection import asyncpg import asyncpg.prepared_stmt import wrapt from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( _get_schema_url_for_signal_types, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _report_new, _set_db_name, _set_db_statement, _set_db_system, _set_db_user, _set_http_net_peer_name_client, _set_http_peer_port_client, _set_net_transport, _StabilityMode, ) from opentelemetry.instrumentation.asyncpg.package import _instruments from opentelemetry.instrumentation.asyncpg.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import is_instrumentation_enabled, unwrap from opentelemetry.semconv._incubating.attributes.db_attributes import ( DbSystemValues, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NetTransportValues, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.network_attributes import ( NetworkTransportValues, ) from opentelemetry.trace import SpanKind from opentelemetry.trace.status import Status, StatusCode _PREPARED_STMT_METHODS = ( "fetch", "fetchval", "fetchrow", "executemany", "fetchmany", ) def _hydrate_span_from_args( connection, query, parameters, semconv_opt_in_mode=_StabilityMode.DEFAULT, ) -> dict: """Get network and database span attributes""" span_attributes: dict = {} _set_db_system( span_attributes, DbSystemValues.POSTGRESQL.value, semconv_opt_in_mode, ) # connection contains _params attribute which is a namedtuple ConnectionParameters. # https://github.com/MagicStack/asyncpg/blob/master/asyncpg/connection.py#L68 params = getattr(connection, "_params", None) dbname = getattr(params, "database", None) if dbname: _set_db_name(span_attributes, dbname, semconv_opt_in_mode) user = getattr(params, "user", None) if user: _set_db_user(span_attributes, user, semconv_opt_in_mode) # connection contains _addr attribute which is either a host/port tuple, or unix socket string # https://magicstack.github.io/asyncpg/current/_modules/asyncpg/connection.html addr = getattr(connection, "_addr", None) if isinstance(addr, tuple): _set_http_net_peer_name_client(span_attributes, addr[0], semconv_opt_in_mode) _set_http_peer_port_client(span_attributes, addr[1], semconv_opt_in_mode) _set_net_transport( span_attributes, NetTransportValues.IP_TCP.value, NetworkTransportValues.TCP.value, semconv_opt_in_mode, ) elif isinstance(addr, str): _set_http_net_peer_name_client(span_attributes, addr, semconv_opt_in_mode) _set_net_transport( span_attributes, NetTransportValues.OTHER.value, NetworkTransportValues.PIPE.value, semconv_opt_in_mode, ) if query is not None: _set_db_statement(span_attributes, query, semconv_opt_in_mode) if parameters is not None and len(parameters) > 0: span_attributes["db.statement.parameters"] = str(parameters) return span_attributes class AsyncPGInstrumentor(BaseInstrumentor): _leading_comment_remover = re.compile(r"^/\*.*?\*/") _tracer = None def __init__(self, capture_parameters=False): super().__init__() self.capture_parameters = capture_parameters def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): tracer_provider = kwargs.get("tracer_provider") _OpenTelemetrySemanticConventionStability._initialize() self._semconv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.DATABASE, ) self._tracer = trace.get_tracer( __name__, __version__, tracer_provider, schema_url=_get_schema_url_for_signal_types( [ _OpenTelemetryStabilitySignalType.DATABASE, ] ), ) for method in [ "Connection.execute", "Connection.executemany", "Connection.fetch", "Connection.fetchval", "Connection.fetchrow", ]: wrapt.wrap_function_wrapper("asyncpg.connection", method, self._do_execute) for method in [ "Cursor.fetch", "Cursor.forward", "Cursor.fetchrow", "CursorIterator.__anext__", ]: wrapt.wrap_function_wrapper("asyncpg.cursor", method, self._do_cursor_execute) for method in _PREPARED_STMT_METHODS: if hasattr(asyncpg.prepared_stmt.PreparedStatement, method): wrapt.wrap_function_wrapper( "asyncpg.prepared_stmt", f"PreparedStatement.{method}", self._do_prepared_execute, ) def _uninstrument(self, **__): for cls, methods in [ ( asyncpg.connection.Connection, ("execute", "executemany", "fetch", "fetchval", "fetchrow"), ), (asyncpg.cursor.Cursor, ("forward", "fetch", "fetchrow")), (asyncpg.cursor.CursorIterator, ("__anext__",)), ]: for method_name in methods: unwrap(cls, method_name) for method_name in _PREPARED_STMT_METHODS: if hasattr(asyncpg.prepared_stmt.PreparedStatement, method_name): unwrap(asyncpg.prepared_stmt.PreparedStatement, method_name) async def _do_execute(self, func, instance, args, kwargs): if not is_instrumentation_enabled(): return await func(*args, **kwargs) exception = None params = getattr(instance, "_params", None) name = args[0] if args[0] else getattr(params, "database", "postgresql") try: # Strip leading comments so we get the operation name. name = self._leading_comment_remover.sub("", name).split()[0] except IndexError: name = "" # Hydrate attributes before span creation to enable filtering span_attributes = _hydrate_span_from_args( instance, args[0], args[1:] if self.capture_parameters else None, semconv_opt_in_mode=self._semconv_opt_in_mode, ) with self._tracer.start_as_current_span(name, kind=SpanKind.CLIENT, attributes=span_attributes) as span: try: result = await func(*args, **kwargs) except Exception as exc: # pylint: disable=W0703 exception = exc raise finally: if span.is_recording() and exception is not None: span.set_status(Status(StatusCode.ERROR)) if _report_new(self._semconv_opt_in_mode): span.set_attribute(ERROR_TYPE, type(exception).__qualname__) return result async def _do_cursor_execute(self, func, instance, args, kwargs): """Wrap cursor based functions. For every call this will generate a new span.""" if not is_instrumentation_enabled(): return await func(*args, **kwargs) exception = None params = getattr(instance._connection, "_params", None) name = instance._query if instance._query else getattr(params, "database", "postgresql") try: # Strip leading comments so we get the operation name. name = self._leading_comment_remover.sub("", name).split()[0] except IndexError: name = "" # Hydrate attributes before span creation to enable filtering span_attributes = _hydrate_span_from_args( instance._connection, instance._query, instance._args if self.capture_parameters else None, semconv_opt_in_mode=self._semconv_opt_in_mode, ) stop = False with self._tracer.start_as_current_span( f"CURSOR: {name}", kind=SpanKind.CLIENT, attributes=span_attributes, ) as span: try: result = await func(*args, **kwargs) except StopAsyncIteration: # Do not show this exception to the span stop = True except Exception as exc: # pylint: disable=W0703 exception = exc raise finally: if span.is_recording() and exception is not None: span.set_status(Status(StatusCode.ERROR)) if _report_new(self._semconv_opt_in_mode): span.set_attribute(ERROR_TYPE, type(exception).__qualname__) if not stop: return result raise StopAsyncIteration async def _do_prepared_execute(self, func, instance, args, kwargs): if not is_instrumentation_enabled(): return await func(*args, **kwargs) exception = None query = instance._query or "" try: name = self._leading_comment_remover.sub("", query).split()[0] except IndexError: name = "" span_attributes = _hydrate_span_from_args( instance._connection, query, args if self.capture_parameters else None, semconv_opt_in_mode=self._semconv_opt_in_mode, ) with self._tracer.start_as_current_span(name, kind=SpanKind.CLIENT, attributes=span_attributes) as span: try: result = await func(*args, **kwargs) except Exception as exc: # pylint: disable=W0703 exception = exc raise finally: if span.is_recording() and exception is not None: span.set_status(Status(StatusCode.ERROR)) if _report_new(self._semconv_opt_in_mode): span.set_attribute(ERROR_TYPE, type(exception).__qualname__) return result package.py000066400000000000000000000002231527070072300450640ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("asyncpg >= 0.12.0",) _semconv_status = "migration" version.py000066400000000000000000000001441527070072300451600ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements-wrapt1.txt000066400000000000000000000000471527070072300402060ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncpg-r test-requirements.txt wrapt==1.17.3 test-requirements-wrapt2.txt000066400000000000000000000000461527070072300402060ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncpg-r test-requirements.txt wrapt==2.1.2 test-requirements.txt000066400000000000000000000004211527070072300367660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncpgasgiref==3.8.1 async-timeout==4.0.3 asyncpg==0.30.0 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-asyncpg opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncpg/tests/000077500000000000000000000000001527070072300337515ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300357710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncpg/teststest_asyncpg_wrapper.py000066400000000000000000000505621527070072300405170ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-asyncpg/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio import contextlib from unittest import mock import asyncpg import asyncpg.connection import pytest from asyncpg import Connection, Record, cursor from asyncpg.prepared_stmt import PreparedStatement try: # wrapt 2.0.0+ from wrapt import BaseObjectProxy # pylint: disable=no-name-in-module except ImportError: from wrapt import ObjectProxy as BaseObjectProxy from opentelemetry import trace as trace_api from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.asyncpg import ( _PREPARED_STMT_METHODS, AsyncPGInstrumentor, ) from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_STATEMENT, DB_SYSTEM, DB_USER, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, NET_TRANSPORT, NetTransportValues, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_NAMESPACE, DB_QUERY_TEXT, DB_SYSTEM_NAME, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.test.test_base import TestBase @contextlib.contextmanager def use_semconv_opt_in(sem_conv_mode): env_patch = mock.patch.dict( "os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode}, ) _OpenTelemetrySemanticConventionStability._initialized = False env_patch.start() try: yield finally: env_patch.stop() _OpenTelemetrySemanticConventionStability._initialized = False class TestAsyncPGInstrumentation(TestBase): def tearDown(self): super().tearDown() AsyncPGInstrumentor().uninstrument() def test_duplicated_instrumentation_can_be_uninstrumented(self): AsyncPGInstrumentor().instrument() AsyncPGInstrumentor().instrument() AsyncPGInstrumentor().instrument() AsyncPGInstrumentor().uninstrument() for method_name in ["execute", "fetch"]: method = getattr(Connection, method_name, None) self.assertFalse(hasattr(method, "_opentelemetry_ext_asyncpg_applied")) def test_duplicated_instrumentation_works(self): first = AsyncPGInstrumentor() first.instrument() second = AsyncPGInstrumentor() second.instrument() self.assertIsNotNone(first._tracer) self.assertIsNotNone(second._tracer) def test_duplicated_uninstrumentation(self): AsyncPGInstrumentor().instrument() AsyncPGInstrumentor().uninstrument() AsyncPGInstrumentor().uninstrument() AsyncPGInstrumentor().uninstrument() for method_name in ["execute", "fetch"]: method = getattr(Connection, method_name, None) self.assertFalse(hasattr(method, "_opentelemetry_ext_asyncpg_applied")) def test_cursor_instrumentation(self): def assert_wrapped(assert_fnc): for cls, methods in [ (cursor.Cursor, ("forward", "fetch", "fetchrow")), (cursor.CursorIterator, ("__anext__",)), ]: for method_name in methods: method = getattr(cls, method_name, None) assert_fnc( isinstance(method, BaseObjectProxy), f"{method} isinstance {type(method)}", ) assert_wrapped(self.assertFalse) AsyncPGInstrumentor().instrument() assert_wrapped(self.assertTrue) AsyncPGInstrumentor().uninstrument() assert_wrapped(self.assertFalse) def test_cursor_span_creation(self): """Test the cursor wrapper if it creates spans correctly.""" # Mock out all interaction with postgres async def bind_mock(*args, **kwargs): return [] async def exec_mock(*args, **kwargs): return [], None, True conn = mock.Mock() conn.is_closed = lambda: False conn._protocol = mock.Mock() conn._protocol.bind = bind_mock conn._protocol.execute = exec_mock conn._protocol.bind_execute = exec_mock conn._protocol.close_portal = bind_mock state = mock.Mock() state.closed = False apg = AsyncPGInstrumentor() apg.instrument(tracer_provider=self.tracer_provider) # init the cursor and fetch a single record crs = cursor.Cursor(conn, "SELECT * FROM test", state, [], Record) asyncio.run(crs._init(1)) with suppress_instrumentation(): self.assertEqual(asyncio.run(crs.fetch(1)), []) self.assertEqual(self.memory_exporter.get_finished_spans(), ()) asyncio.run(crs.fetch(1)) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].name, "CURSOR: SELECT") self.assertTrue(spans[0].status.is_ok) # Now test that the StopAsyncIteration of the cursor does not get recorded as an ERROR crs_iter = cursor.CursorIterator(conn, "SELECT * FROM test", state, [], Record, 1, 1) with suppress_instrumentation(), pytest.raises(StopAsyncIteration): asyncio.run(anext(crs_iter)) self.assertEqual(len(self.memory_exporter.get_finished_spans()), 1) with pytest.raises(StopAsyncIteration): asyncio.run(anext(crs_iter)) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) self.assertEqual([span.status.is_ok for span in spans], [True, True]) def test_no_op_tracer_provider(self): AsyncPGInstrumentor().uninstrument() AsyncPGInstrumentor().instrument(tracer_provider=trace_api.NoOpTracerProvider()) # Mock out all interaction with postgres async def bind_mock(*args, **kwargs): return [] async def exec_mock(*args, **kwargs): return [], None, True conn = mock.Mock() conn.is_closed = lambda: False conn._protocol = mock.Mock() conn._protocol.bind = bind_mock conn._protocol.execute = exec_mock conn._protocol.bind_execute = exec_mock conn._protocol.close_portal = bind_mock state = mock.Mock() state.closed = False # init the cursor and fetch a single record crs = cursor.Cursor(conn, "SELECT * FROM test", state, [], Record) asyncio.run(crs._init(1)) asyncio.run(crs.fetch(1)) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_prepared_statement_instrumentation(self): methods = [m for m in _PREPARED_STMT_METHODS if hasattr(PreparedStatement, m)] for method_name in methods: with self.subTest(method=method_name, phase="before"): self.assertFalse( isinstance( getattr(PreparedStatement, method_name), BaseObjectProxy, ) ) AsyncPGInstrumentor().instrument() for method_name in methods: with self.subTest(method=method_name, phase="instrumented"): self.assertTrue( isinstance( getattr(PreparedStatement, method_name), BaseObjectProxy, ) ) AsyncPGInstrumentor().uninstrument() for method_name in methods: with self.subTest(method=method_name, phase="uninstrumented"): self.assertFalse( isinstance( getattr(PreparedStatement, method_name), BaseObjectProxy, ) ) @staticmethod def _make_prepared_stmt_conn(): async def bind_execute_mock(*args, **kwargs): return [], b"SELECT 1", True async def bind_execute_many_mock(*args, **kwargs): return None conn = mock.Mock() conn._pool_release_ctr = 0 conn.is_closed = lambda: False conn._protocol = mock.Mock() conn._protocol.bind_execute = bind_execute_mock conn._protocol.bind_execute_many = bind_execute_many_mock state = mock.Mock() state.closed = False return conn, state def test_prepared_statement_span(self): # Per-method: (query, call_args, expected_span_name) method_cases = { "fetch": ("SELECT * FROM users", (), "SELECT"), "fetchval": ("SELECT id FROM users WHERE id=$1", (1,), "SELECT"), "fetchrow": ("SELECT * FROM t WHERE v=$1", ("x",), "SELECT"), "executemany": ( "INSERT INTO t (v) VALUES ($1)", ([("a",), ("b",)],), "INSERT", ), "fetchmany": ("SELECT * FROM t", ([],), "SELECT"), } for method_name in _PREPARED_STMT_METHODS: if not hasattr(PreparedStatement, method_name): continue query, call_args, expected_name = method_cases[method_name] with self.subTest(method=method_name): self.memory_exporter.clear() conn, state = self._make_prepared_stmt_conn() apg = AsyncPGInstrumentor() apg.instrument(tracer_provider=self.tracer_provider) stmt = PreparedStatement(conn, query, state) with suppress_instrumentation(): suppressed_result = asyncio.run(getattr(stmt, method_name)(*call_args)) self.assertEqual(self.memory_exporter.get_finished_spans(), ()) result = asyncio.run(getattr(stmt, method_name)(*call_args)) self.assertEqual(result, suppressed_result) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].name, expected_name) self.assertTrue(spans[0].status.is_ok) self.assertEqual(spans[0].attributes.get("db.statement"), query) self.assertEqual(spans[0].attributes.get("db.system"), "postgresql") apg.uninstrument() def test_prepared_statement_error_span(self): async def bind_execute_error(*args, **kwargs): raise RuntimeError("db error") conn = mock.Mock() conn._pool_release_ctr = 0 conn.is_closed = lambda: False conn._protocol = mock.Mock() conn._protocol.bind_execute = bind_execute_error state = mock.Mock() state.closed = False apg = AsyncPGInstrumentor() apg.instrument(tracer_provider=self.tracer_provider) stmt = PreparedStatement(conn, "SELECT 1", state) with suppress_instrumentation(), self.assertRaisesRegex(RuntimeError, "db error"): asyncio.run(stmt.fetch()) self.assertEqual(self.memory_exporter.get_finished_spans(), ()) with self.assertRaises(RuntimeError): asyncio.run(stmt.fetch()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertFalse(spans[0].status.is_ok) class TestAsyncPGSemconvMigration(TestBase): def setUp(self): super().setUp() _OpenTelemetrySemanticConventionStability._initialized = False def tearDown(self): super().tearDown() AsyncPGInstrumentor().uninstrument() _OpenTelemetrySemanticConventionStability._initialized = False @staticmethod def _make_execute_conn(addr=("testhost", 5432)): async def query_mock(*args, **kwargs): return b"SELECT 1", [], True async def execute_mock(*args, **kwargs): return [], b"SELECT 1", True conn = mock.Mock() conn.is_closed = lambda: False conn._pool_release_ctr = 0 conn._query_loggers = [] conn._execute = execute_mock conn._protocol = mock.Mock() conn._protocol.query = query_mock conn._addr = addr params = mock.Mock() params.database = "testdb" params.user = "testuser" conn._params = params return conn def _run_execute(self, conn, query="SELECT 1"): apg = AsyncPGInstrumentor() apg.instrument(tracer_provider=self.tracer_provider) async def do_execute(): # Use __get__ to bind conn as the instance so wrapt correctly # receives instance=conn instead of instance=None. execute = asyncpg.connection.Connection.execute.__get__( # pylint: disable=no-value-for-parameter conn, asyncpg.connection.Connection ) await execute(query) asyncio.run(do_execute()) return self.memory_exporter.get_finished_spans() def test_schema_url(self): apg = AsyncPGInstrumentor() apg.instrument(tracer_provider=self.tracer_provider) self.assertEqual( apg._tracer._instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.11.0", ) def test_schema_url_new_semconv(self): with use_semconv_opt_in("database"): apg = AsyncPGInstrumentor() apg.instrument(tracer_provider=self.tracer_provider) self.assertEqual( apg._tracer._instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.25.0", ) def test_suppress_instrumentation(self) -> None: conn = self._make_execute_conn() conn._protocol.query = mock.AsyncMock(return_value="SELECT 1") with suppress_instrumentation(): spans = self._run_execute(conn) self.assertEqual(spans, ()) conn._protocol.query.assert_awaited_once_with("SELECT 1", None) AsyncPGInstrumentor().uninstrument() self.assertEqual(len(self._run_execute(conn)), 1) def test_suppress_instrumentation_error(self) -> None: conn = self._make_execute_conn() error = RuntimeError("db error") conn._protocol.query = mock.AsyncMock(side_effect=error) with suppress_instrumentation(), self.assertRaises(RuntimeError) as raised: self._run_execute(conn) self.assertIs(raised.exception, error) self.assertEqual(self.memory_exporter.get_finished_spans(), ()) def test_span(self): conn = self._make_execute_conn() spans = self._run_execute(conn) self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes[DB_SYSTEM], "postgresql") self.assertEqual(span.attributes[DB_NAME], "testdb") self.assertEqual(span.attributes[DB_USER], "testuser") self.assertEqual(span.attributes[NET_PEER_NAME], "testhost") self.assertEqual(span.attributes[NET_PEER_PORT], 5432) self.assertEqual(span.attributes[DB_STATEMENT], "SELECT 1") def test_span_database_only_new_semconv(self): with use_semconv_opt_in("database"): conn = self._make_execute_conn() spans = self._run_execute(conn) self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes[DB_SYSTEM_NAME], "postgresql") self.assertEqual(span.attributes[DB_NAMESPACE], "testdb") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT 1") self.assertEqual(span.attributes[SERVER_ADDRESS], "testhost") self.assertEqual(span.attributes[SERVER_PORT], 5432) self.assertNotIn(NET_PEER_NAME, span.attributes) self.assertNotIn(NET_PEER_PORT, span.attributes) def test_span_both_semconv(self): with use_semconv_opt_in("database/dup,http/dup"): conn = self._make_execute_conn() spans = self._run_execute(conn) self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes[DB_SYSTEM], "postgresql") self.assertEqual(span.attributes[DB_NAME], "testdb") self.assertEqual(span.attributes[DB_STATEMENT], "SELECT 1") self.assertEqual(span.attributes[DB_USER], "testuser") self.assertEqual(span.attributes[NET_PEER_NAME], "testhost") self.assertEqual(span.attributes[NET_PEER_PORT], 5432) self.assertEqual(span.attributes[DB_SYSTEM_NAME], "postgresql") self.assertEqual(span.attributes[DB_NAMESPACE], "testdb") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT 1") self.assertEqual(span.attributes[SERVER_ADDRESS], "testhost") self.assertEqual(span.attributes[SERVER_PORT], 5432) def test_span_new_semconv(self): with use_semconv_opt_in("database,http"): conn = self._make_execute_conn() spans = self._run_execute(conn) self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes[DB_SYSTEM_NAME], "postgresql") self.assertEqual(span.attributes[DB_NAMESPACE], "testdb") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT 1") self.assertEqual(span.attributes[SERVER_ADDRESS], "testhost") self.assertEqual(span.attributes[SERVER_PORT], 5432) def test_span_unix_socket_default_semconv(self): conn = self._make_execute_conn(addr="/var/run/postgresql/.s.PGSQL.5432") spans = self._run_execute(conn) self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual( span.attributes[NET_PEER_NAME], "/var/run/postgresql/.s.PGSQL.5432", ) self.assertEqual(span.attributes[NET_TRANSPORT], NetTransportValues.OTHER.value) self.assertNotIn(NET_PEER_PORT, span.attributes) self.assertNotIn(SERVER_ADDRESS, span.attributes) def test_span_capture_parameters(self): apg = AsyncPGInstrumentor(capture_parameters=True) apg.instrument(tracer_provider=self.tracer_provider) conn = self._make_execute_conn() async def do_execute(): execute = asyncpg.connection.Connection.execute.__get__( # pylint: disable=no-value-for-parameter conn, asyncpg.connection.Connection ) await execute("SELECT $1", "hello") asyncio.run(do_execute()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertIn("db.statement.parameters", spans[0].attributes) self.assertIn("hello", spans[0].attributes["db.statement.parameters"]) def _run_execute_error(self, semconv_mode): async def error_mock(*args, **kwargs): raise RuntimeError("db error") conn = mock.Mock() conn.is_closed = lambda: False conn._pool_release_ctr = 0 conn._query_loggers = [] conn._execute = error_mock conn._protocol = mock.Mock() conn._protocol.query = error_mock conn._addr = ("testhost", 5432) params = mock.Mock() params.database = "testdb" params.user = "testuser" conn._params = params with use_semconv_opt_in(semconv_mode): apg = AsyncPGInstrumentor() apg.instrument(tracer_provider=self.tracer_provider) async def do_execute(): execute = asyncpg.connection.Connection.execute.__get__( # pylint: disable=no-value-for-parameter conn, asyncpg.connection.Connection ) await execute("SELECT 1") with self.assertRaises(RuntimeError): asyncio.run(do_execute()) return self.memory_exporter.get_finished_spans() def test_error_type_not_set_on_default_semconv(self): spans = self._run_execute_error("") self.assertEqual(len(spans), 1) span = spans[0] self.assertFalse(span.status.is_ok) self.assertNotIn(ERROR_TYPE, span.attributes) def test_error_type_set_on_new_db_semconv(self): spans = self._run_execute_error("database") self.assertEqual(len(spans), 1) span = spans[0] self.assertFalse(span.status.is_ok) self.assertEqual(span.attributes[ERROR_TYPE], "RuntimeError") def test_error_type_set_on_both_semconv(self): spans = self._run_execute_error("database/dup,http/dup") self.assertEqual(len(spans), 1) span = spans[0] self.assertFalse(span.status.is_ok) self.assertEqual(span.attributes[ERROR_TYPE], "RuntimeError") opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/000077500000000000000000000000001527070072300331535ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/LICENSE000066400000000000000000000261351527070072300341670ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000022331527070072300345630ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambdaOpenTelemetry AWS Lambda Tracing ================================ |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-aws-lambda.svg :target: https://pypi.org/project/opentelemetry-instrumentation-aws-lambda/ This library provides an Instrumentor used to trace requests made by the Lambda functions on the AWS Lambda service. It also provides scripts used by AWS Lambda Layers to automatically initialize the OpenTelemetry SDK. Learn more on the AWS Distro for OpenTelemetry (ADOT) `documentation for the Python Lambda Layer `_. Installation ------------ :: pip install opentelemetry-instrumentation-aws-lambda References ---------- * `OpenTelemetry AWS Lambda Tracing `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ * `ADOT Python Lambda Layer Documentation `_ pyproject.toml000066400000000000000000000031471527070072300360150ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-aws-lambda" dynamic = ["version"] description = "OpenTelemetry AWS Lambda instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-propagator-aws-xray ~= 1.0", ] [project.optional-dependencies] instruments = [] [project.entry-points.opentelemetry_instrumentor] aws-lambda = "opentelemetry.instrumentation.aws_lambda:AwsLambdaInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-aws-lambda" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/aws_lambda/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/src/000077500000000000000000000000001527070072300337425ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300365575ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/srcinstrumentation/000077500000000000000000000000001527070072300420225ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetryaws_lambda/000077500000000000000000000000001527070072300441145ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation__init__.py000066400000000000000000000445711527070072300462400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The opentelemetry-instrumentation-aws-lambda package provides an Instrumentor to traces calls within a Python AWS Lambda function. Usage ----- .. code:: python # Copy this snippet into an AWS Lambda function import boto3 from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor # Enable instrumentation BotocoreInstrumentor().instrument() # Lambda function def lambda_handler(event, context): s3 = boto3.resource('s3') for bucket in s3.buckets.all(): print(bucket.name) return "200 OK" AwsLambdaInstrumentor().instrument() API --- The `instrument` method accepts the following keyword args: tracer_provider (TracerProvider) - an optional tracer provider meter_provider (MeterProvider) - an optional meter provider event_context_extractor (Callable) - a function that returns an OTel Trace Context given the Lambda Event the AWS Lambda was invoked with this function signature is: def event_context_extractor(lambda_event: Any) -> Context for example: .. code:: python from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor from opentelemetry.propagate import get_global_textmap def custom_event_context_extractor(lambda_event): # If the `TraceContextTextMapPropagator` is the global propagator, we # can use it to parse out the context from the HTTP Headers. return get_global_textmap().extract(lambda_event["foo"]["headers"]) AwsLambdaInstrumentor().instrument( event_context_extractor=custom_event_context_extractor ) --- """ from __future__ import annotations import logging import os import time from collections.abc import Callable, Collection from enum import Enum from importlib import import_module from typing import TYPE_CHECKING, Any from urllib.parse import urlencode from wrapt import wrap_function_wrapper from opentelemetry import context as context_api from opentelemetry.context.context import Context from opentelemetry.instrumentation.aws_lambda._sqs import ( _is_sqs_event, _run_sqs_handler, ) from opentelemetry.instrumentation.aws_lambda.package import _instruments from opentelemetry.instrumentation.aws_lambda.version import __version__ from opentelemetry.instrumentation.cidict import CIDict from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap from opentelemetry.metrics import MeterProvider, get_meter_provider from opentelemetry.propagate import get_global_textmap from opentelemetry.semconv._incubating.attributes.cloud_attributes import ( CLOUD_ACCOUNT_ID, CLOUD_RESOURCE_ID, ) from opentelemetry.semconv._incubating.attributes.faas_attributes import ( FAAS_INVOCATION_ID, FAAS_TRIGGER, FaasTriggerValues, ) from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_METHOD, HTTP_ROUTE, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_TARGET, HTTP_USER_AGENT, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_HOST_NAME, ) from opentelemetry.trace import ( SpanKind, TracerProvider, get_tracer, get_tracer_provider, ) from opentelemetry.trace.status import Status, StatusCode logger = logging.getLogger(__name__) _HANDLER = "_HANDLER" _X_AMZN_TRACE_ID = "_X_AMZN_TRACE_ID" ORIG_HANDLER = "ORIG_HANDLER" OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT = "OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT" if TYPE_CHECKING: import typing from collections.abc import MutableMapping class LambdaContext(typing.Protocol): """Type definition for AWS Lambda context object. This Protocol defines the interface for the context object passed to Lambda function handlers, providing information about the invocation, function, and execution environment. See Also: AWS Lambda Context Object documentation: https://docs.aws.amazon.com/lambda/latest/dg/python-context.html """ function_name: str function_version: str invoked_function_arn: str memory_limit_in_mb: int aws_request_id: str log_group_name: str log_stream_name: str class _LambdaEventType(Enum): SQS = "sqs" API_GATEWAY = "api_gateway" UNKNOWN = "unknown" def _get_lambda_event_type(lambda_event: Any) -> _LambdaEventType: if _is_sqs_event(lambda_event): return _LambdaEventType.SQS if isinstance(lambda_event, dict) and lambda_event.get("requestContext"): return _LambdaEventType.API_GATEWAY return _LambdaEventType.UNKNOWN def _default_event_context_extractor(lambda_event: Any) -> Context: """Default way of extracting the context from the Lambda Event. Assumes the Lambda Event is a map with the headers under the 'headers' key. This is the mapping to use when the Lambda is invoked by an API Gateway REST API where API Gateway is acting as a pure proxy for the request. Protects headers from being something other than dictionary, as this is what downstream propagators expect. See more: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format Args: lambda_event: user-defined, so it could be anything, but this method counts on it being a map with a 'headers' key Returns: A Context with configuration found in the event. """ return get_global_textmap().extract( CIDict(_extract_http_headers(lambda_event)), ) def _extract_http_headers(lambda_event: Any) -> dict[str, Any]: if not isinstance(lambda_event, dict): return {} headers = lambda_event.get("headers") multi_value_headers = lambda_event.get("multiValueHeaders") normalized_multi_value_headers = {} if isinstance(multi_value_headers, dict): normalized_multi_value_headers = { key: values[0] for key, values in multi_value_headers.items() if isinstance(values, list) and values } if not isinstance(headers, dict): return normalized_multi_value_headers return { **headers, **normalized_multi_value_headers, } def _determine_parent_context( lambda_event: Any, event_context_extractor: Callable[[Any], Context], ) -> Context: """Determine the parent context for the current Lambda invocation. See more: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#determining-the-parent-of-a-span Args: lambda_event: user-defined, so it could be anything, but this method counts it being a map with a 'headers' key event_context_extractor: a method which takes the Lambda Event as input and extracts an OTel Context from it. By default, the context is extracted from the HTTP headers of an API Gateway request. Returns: A Context with configuration found in the carrier. """ if event_context_extractor is None: return _default_event_context_extractor(lambda_event) return event_context_extractor(lambda_event) def _get_api_gateway_v1_proxy_attributes( lambda_event: Any, ) -> MutableMapping[str, Any]: """Sets HTTP attributes for REST APIs and v1 HTTP APIs More info: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format """ attributes = {} attributes[HTTP_METHOD] = lambda_event.get("httpMethod") if headers := _extract_http_headers(lambda_event): headers = CIDict(headers) if "User-Agent" in headers: attributes[HTTP_USER_AGENT] = headers["User-Agent"] if "X-Forwarded-Proto" in headers: attributes[HTTP_SCHEME] = headers["X-Forwarded-Proto"] if "Host" in headers: attributes[NET_HOST_NAME] = headers["Host"] if "resource" in lambda_event: attributes[HTTP_ROUTE] = lambda_event["resource"] if lambda_event.get("queryStringParameters"): attributes[HTTP_TARGET] = f"{lambda_event['resource']}?{urlencode(lambda_event['queryStringParameters'])}" else: attributes[HTTP_TARGET] = lambda_event["resource"] return attributes def _get_api_gateway_v2_proxy_attributes( lambda_event: Any, ) -> MutableMapping[str, Any]: """Sets HTTP attributes for v2 HTTP APIs More info: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html """ attributes = {} if "domainName" in lambda_event["requestContext"]: attributes[NET_HOST_NAME] = lambda_event["requestContext"]["domainName"] if lambda_event["requestContext"].get("http"): http = lambda_event["requestContext"]["http"] if "method" in http: attributes[HTTP_METHOD] = http["method"] if "userAgent" in http: attributes[HTTP_USER_AGENT] = http["userAgent"] if "path" in http: attributes[HTTP_ROUTE] = http["path"] if lambda_event.get("rawQueryString"): attributes[HTTP_TARGET] = f"{http['path']}?{lambda_event['rawQueryString']}" else: attributes[HTTP_TARGET] = http["path"] return attributes def _get_lambda_context_attributes( lambda_context: LambdaContext, ) -> dict[str, str]: """Extracts OpenTelemetry span attributes from AWS Lambda context. Extract FaaS specific attributes from the AWS Lambda context according to OpenTelemetry semantic conventions for FaaS & AWS Lambda. Args: lambda_context: The AWS Lambda context object. Returns: A dictionary mapping of OpenTelemetry attribute names to their values. """ function_arn_parts: list[str] = lambda_context.invoked_function_arn.split(":") # NOTE: `cloud.account.id` can be parsed from the ARN as the fifth item when splitting on `:` # # See more: # https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#all-triggers aws_account_id: str = function_arn_parts[4] # NOTE: The unmodified function ARN may contain an alias extension e.g. # `arn:aws:lambda:region:account:function:name:alias`. We can ensure # the alias extension is not included in the `cloud.resource_id` by keeping # only the first 7 parts of the original ARN. # # See more: # https://docs.aws.amazon.com/lambda/latest/dg/python-context.html formatted_function_arn: str = ":".join(function_arn_parts[:7]) # NOTE: The specs mention an exception here, allowing the # `SpanAttributes.CLOUD_RESOURCE_ID` attribute to be set as a span # attribute instead of a resource attribute. # # See more: # https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#resource-detector return { CLOUD_ACCOUNT_ID: aws_account_id, CLOUD_RESOURCE_ID: formatted_function_arn, FAAS_INVOCATION_ID: lambda_context.aws_request_id, } # pylint: disable=too-many-statements def _instrument( wrapped_module_name, wrapped_function_name, flush_timeout, event_context_extractor: Callable[[Any], Context], tracer_provider: TracerProvider = None, meter_provider: MeterProvider = None, ): # pylint: disable=too-many-locals # pylint: disable=too-many-statements # pylint: disable=too-many-branches def _instrumented_lambda_handler_call(call_wrapped, instance, args, kwargs): lambda_event: Any = args[0] lambda_context: LambdaContext = args[1] parent_context = _determine_parent_context( lambda_event, event_context_extractor, ) tracer = get_tracer( __name__, __version__, tracer_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) token = context_api.attach(parent_context) event_type = _get_lambda_event_type(lambda_event) span_attributes: MutableMapping[str, Any] = _get_lambda_context_attributes(lambda_context) if event_type is _LambdaEventType.SQS: span_attributes[FAAS_TRIGGER] = FaasTriggerValues.PUBSUB.value elif event_type is _LambdaEventType.API_GATEWAY: # If the request came from an API Gateway, extract http attributes from the event # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#api-gateway # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-server-semantic-conventions span_attributes[FAAS_TRIGGER] = "http" if lambda_event.get("version") == "2.0": span_attributes |= _get_api_gateway_v2_proxy_attributes(lambda_event) else: span_attributes |= _get_api_gateway_v1_proxy_attributes(lambda_event) try: with tracer.start_as_current_span( name=lambda_context.function_name, kind=SpanKind.SERVER, attributes=span_attributes, ) as span: exception = None result = None try: if event_type is _LambdaEventType.SQS: result = _run_sqs_handler(tracer, lambda_event, call_wrapped, args, kwargs) else: result = call_wrapped(*args, **kwargs) # pylint: disable-next=broad-exception-caught except Exception as exc: exception = exc span.set_status(Status(StatusCode.ERROR)) span.record_exception(exception) if event_type is _LambdaEventType.API_GATEWAY and isinstance(result, dict) and result.get("statusCode"): span.set_attribute( HTTP_STATUS_CODE, result.get("statusCode"), ) finally: if token: context_api.detach(token) now = time.time() _tracer_provider = tracer_provider or get_tracer_provider() if hasattr(_tracer_provider, "force_flush"): try: # NOTE: `force_flush` before function quit in case of Lambda freeze. _tracer_provider.force_flush(flush_timeout) except Exception: # pylint: disable=broad-except logger.exception("TracerProvider failed to flush traces") else: logger.warning( "TracerProvider was missing `force_flush` method. This is necessary in case of a Lambda freeze and would exist in the OTel SDK implementation." ) _meter_provider = meter_provider or get_meter_provider() if hasattr(_meter_provider, "force_flush"): rem = flush_timeout - (time.time() - now) * 1000 if rem > 0: try: # NOTE: `force_flush` before function quit in case of Lambda freeze. _meter_provider.force_flush(rem) except Exception: # pylint: disable=broad-except logger.exception("MeterProvider failed to flush metrics") else: logger.warning( "MeterProvider was missing `force_flush` method. This is necessary in case of a Lambda freeze and would exist in the OTel SDK implementation." ) if exception is not None: raise exception.with_traceback(exception.__traceback__) return result wrap_function_wrapper( wrapped_module_name, wrapped_function_name, _instrumented_lambda_handler_call, ) class AwsLambdaInstrumentor(BaseInstrumentor): def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): """Instruments Lambda Handlers on AWS Lambda. See more: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md Args: **kwargs: Optional arguments ``tracer_provider``: a TracerProvider, defaults to global ``meter_provider``: a MeterProvider, defaults to global ``event_context_extractor``: a method which takes the Lambda Event as input and extracts an OTel Context from it. By default, the context is extracted from the HTTP headers of an API Gateway request. """ # Don't try if we are not running on AWS Lambda if "AWS_LAMBDA_FUNCTION_NAME" not in os.environ: return lambda_handler = os.environ.get(ORIG_HANDLER, os.environ.get(_HANDLER)) if not lambda_handler: logger.warning( ( "Could not find the ORIG_HANDLER or _HANDLER in the environment variables. ", "This instrumentation requires the OpenTelemetry Lambda extension installed.", ) ) return # Convert slash-delimited paths to dot-delimited for valid Python imports lambda_handler = lambda_handler.replace("/", ".") ( self._wrapped_module_name, self._wrapped_function_name, ) = lambda_handler.rsplit(".", 1) flush_timeout_env = os.environ.get(OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT, None) flush_timeout = 30000 try: if flush_timeout_env is not None: flush_timeout = int(flush_timeout_env) except ValueError: logger.warning( "Could not convert OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT value %s to int", flush_timeout_env, ) _instrument( self._wrapped_module_name, self._wrapped_function_name, flush_timeout, event_context_extractor=kwargs.get("event_context_extractor", _default_event_context_extractor), tracer_provider=kwargs.get("tracer_provider"), meter_provider=kwargs.get("meter_provider"), ) def _uninstrument(self, **kwargs): unwrap( import_module(self._wrapped_module_name), self._wrapped_function_name, ) _sqs.py000066400000000000000000000072341527070072300454410ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging from collections.abc import Callable, Mapping from typing import Any from opentelemetry.instrumentation.cidict import CIDict from opentelemetry.propagate import get_global_textmap from opentelemetry.propagators.textmap import Getter from opentelemetry.semconv._incubating.attributes.messaging_attributes import ( MESSAGING_BATCH_MESSAGE_COUNT, MESSAGING_DESTINATION_NAME, MESSAGING_MESSAGE_ID, MESSAGING_OPERATION_TYPE, MESSAGING_SYSTEM, MessagingOperationTypeValues, MessagingSystemValues, ) from opentelemetry.trace import Link, SpanKind, get_current_span logger = logging.getLogger(__name__) CarrierT = dict[str, Any] class _SqsMessageAttributesGetter(Getter[CarrierT]): """Extracts W3C trace context from SQS message attribute dicts. SQS message attributes have the structure: ``{"traceparent": {"dataType": "String", "stringValue": "00-..."}}``. """ def get(self, carrier: CarrierT, key: str) -> list[str] | None: msg_attr = carrier.get(key) if not isinstance(msg_attr, Mapping): return None value = msg_attr.get("stringValue") return [value] if isinstance(value, str) else None def keys(self, carrier: CarrierT) -> list[str]: return list(carrier.keys()) _sqs_getter = _SqsMessageAttributesGetter() def _is_sqs_event(lambda_event: Any) -> bool: try: return bool( isinstance(lambda_event, dict) and isinstance(lambda_event.get("Records"), list) and lambda_event["Records"] and lambda_event["Records"][0].get("eventSource", "").strip().lower() == "aws:sqs" ) except Exception as exc: # pylint: disable=broad-except logger.warning("An unexpected error occurred while checking SQS event: %s", exc) return False def _extract_sqs_queue_name(arn: str) -> str: parts = arn.split(":") return parts[5] if len(parts) >= 6 else arn def _get_sqs_span_links(records: list[dict[str, Any]]) -> list[Link]: links = [] for record in records: msg_attrs = record.get("messageAttributes") or {} if not msg_attrs or not isinstance(msg_attrs, dict): continue # Wrap in CIDict so propagation header name lookup is case-insensitive # (e.g. "TRACEPARENT" and "traceparent" are both found). ctx = get_global_textmap().extract(CIDict(msg_attrs), getter=_sqs_getter) span_ctx = get_current_span(ctx).get_span_context() if span_ctx and span_ctx.is_valid: links.append( Link( context=span_ctx, attributes={MESSAGING_MESSAGE_ID: record.get("messageId", "")}, ) ) return links def _run_sqs_handler( tracer: Any, lambda_event: dict[str, Any], handler: Callable[..., Any], args: tuple[Any, ...], kwargs: dict[str, Any], ) -> Any: records: list[dict[str, Any]] = lambda_event["Records"] queue_name = _extract_sqs_queue_name(records[0].get("eventSourceARN", "")) links = _get_sqs_span_links(records) attrs: dict[str, Any] = { MESSAGING_SYSTEM: MessagingSystemValues.AWS_SQS.value, MESSAGING_DESTINATION_NAME: queue_name, MESSAGING_OPERATION_TYPE: MessagingOperationTypeValues.PROCESS.value, } if len(records) > 1: attrs[MESSAGING_BATCH_MESSAGE_COUNT] = len(records) with tracer.start_as_current_span( f"process {queue_name}".strip(), kind=SpanKind.CONSUMER, links=links, attributes=attrs, ): return handler(*args, **kwargs) package.py000066400000000000000000000001451527070072300460610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = tuple() version.py000066400000000000000000000001441527070072300461520ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000005001527070072300373300ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambdaasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e propagator/opentelemetry-propagator-aws-xray -e instrumentation/opentelemetry-instrumentation-aws-lambda opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/000077500000000000000000000000001527070072300343155ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300363350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/testsmocks/000077500000000000000000000000001527070072300353525ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/tests__init__.py000066400000000000000000000000001527070072300374510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocksalb_conventional_headers_event.py000066400000000000000000000016631527070072300441430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 MOCK_LAMBDA_ALB_EVENT = { "requestContext": { "elb": { "targetGroupArn": "arn:aws:elasticloadbalancing:region:123456789012:targetgroup/my-target-group/6d0ecf831eec9f09" } }, "httpMethod": "GET", "path": "/", "queryStringParameters": {"foo": "bar"}, "headers": { "accept": "text/html,application/xhtml+xml", "accept-language": "en-US,en;q=0.8", "content-type": "text/plain", "cookie": "cookies", "host": "lambda-846800462-us-east-2.elb.amazonaws.com", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6)", "x-amzn-trace-id": "Root=1-5bdb40ca-556d8b0c50dc66f0511bf520", "x-forwarded-for": "72.21.198.66", "x-forwarded-port": "443", "x-forwarded-proto": "https", }, "isBase64Encoded": False, "body": "request_body", } alb_multi_value_headers_event.py000066400000000000000000000023531527070072300437670ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers When an ALB is configured to send multi-value headers, the headers are sent as a list of values under the key in the multiValueHeaders object. """ MOCK_LAMBDA_ALB_MULTI_VALUE_HEADER_EVENT = { "requestContext": { "elb": { "targetGroupArn": "arn:aws:elasticloadbalancing:region:123456789012:targetgroup/my-target-group/6d0ecf831eec9f09" } }, "httpMethod": "GET", "path": "/", "queryStringParameters": {"foo": "bar"}, "multiValueHeaders": { "accept": ["text/html,application/xhtml+xml"], "accept-language": ["en-US,en;q=0.8"], "content-type": ["text/plain"], "cookie": ["cookies"], "host": ["lambda-846800462-us-east-2.elb.amazonaws.com"], "user-agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6)"], "x-amzn-trace-id": ["Root=1-5bdb40ca-556d8b0c50dc66f0511bf520"], "x-forwarded-for": ["72.21.198.66"], "x-forwarded-port": ["443"], "x-forwarded-proto": ["https"], }, "isBase64Encoded": False, "body": "request_body", } api_gateway_http_api_event.py000066400000000000000000000037141527070072300433140ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Generated via `sam local generate-event apigateway http-api-proxy` MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT = { "version": "2.0", "routeKey": "$default", "rawPath": "/path/to/resource", "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", "cookies": ["cookie1", "cookie2"], "headers": {"header1": "value1", "Header2": "value1,value2"}, "queryStringParameters": { "parameter1": "value1,value2", "parameter2": "value", }, "requestContext": { "accountId": "123456789012", "apiId": "api-id", "authentication": { "clientCert": { "clientCertPem": "CERT_CONTENT", "subjectDN": "www.example.com", "issuerDN": "Example issuer", "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", "validity": { "notBefore": "May 28 12:30:02 2019 GMT", "notAfter": "Aug 5 09:36:04 2021 GMT", }, } }, "authorizer": { "jwt": { "claims": {"claim1": "value1", "claim2": "value2"}, "scopes": ["scope1", "scope2"], } }, "domainName": "id.execute-api.us-east-1.amazonaws.com", "domainPrefix": "id", "http": { "method": "POST", "path": "/path/to/resource", "protocol": "HTTP/1.1", "sourceIp": "192.168.0.1/32", "userAgent": "agent", }, "requestId": "id", "routeKey": "$default", "stage": "$default", "time": "12/Mar/2020:19:03:58 +0000", "timeEpoch": 1583348638390, }, "body": "eyJ0ZXN0IjoiYm9keSJ9", "pathParameters": {"parameter1": "value1"}, "isBase64Encoded": True, "stageVariables": {"stageVariable1": "value1", "stageVariable2": "value2"}, } api_gateway_proxy_event.py000066400000000000000000000065671527070072300426760ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Generated via `sam local generate-event apigateway aws-proxy` MOCK_LAMBDA_API_GATEWAY_PROXY_EVENT = { "body": "eyJ0ZXN0IjoiYm9keSJ9", "resource": "/{proxy+}", "path": "/path/to/resource", "httpMethod": "POST", "isBase64Encoded": True, "queryStringParameters": {"foo": "bar"}, "multiValueQueryStringParameters": {"foo": ["bar"]}, "pathParameters": {"proxy": "/path/to/resource"}, "stageVariables": {"baz": "qux"}, "headers": { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Encoding": "gzip, deflate, sdch", "Accept-Language": "en-US,en;q=0.8", "Cache-Control": "max-age=0", "CloudFront-Forwarded-Proto": "https", "CloudFront-Is-Desktop-Viewer": "true", "CloudFront-Is-Mobile-Viewer": "false", "CloudFront-Is-SmartTV-Viewer": "false", "CloudFront-Is-Tablet-Viewer": "false", "CloudFront-Viewer-Country": "US", "Host": "1234567890.execute-api.us-east-1.amazonaws.com", "Upgrade-Insecure-Requests": "1", "User-Agent": "Custom User Agent String", "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", "X-Forwarded-For": "127.0.0.1, 127.0.0.2", "X-Forwarded-Port": "443", "X-Forwarded-Proto": "https", }, "multiValueHeaders": { "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"], "Accept-Encoding": ["gzip, deflate, sdch"], "Accept-Language": ["en-US,en;q=0.8"], "Cache-Control": ["max-age=0"], "CloudFront-Forwarded-Proto": ["https"], "CloudFront-Is-Desktop-Viewer": ["true"], "CloudFront-Is-Mobile-Viewer": ["false"], "CloudFront-Is-SmartTV-Viewer": ["false"], "CloudFront-Is-Tablet-Viewer": ["false"], "CloudFront-Viewer-Country": ["US"], "Host": ["0123456789.execute-api.us-east-1.amazonaws.com"], "Upgrade-Insecure-Requests": ["1"], "User-Agent": ["Custom User Agent String"], "Via": ["1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"], "X-Amz-Cf-Id": ["cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="], "X-Forwarded-For": ["127.0.0.1, 127.0.0.2"], "X-Forwarded-Port": ["443"], "X-Forwarded-Proto": ["https"], }, "requestContext": { "accountId": "123456789012", "resourceId": "123456", "stage": "prod", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "requestTime": "09/Apr/2015:12:34:56 +0000", "requestTimeEpoch": 1428582896000, "identity": { "cognitoIdentityPoolId": None, "accountId": None, "cognitoIdentityId": None, "caller": None, "accessKey": None, "sourceIp": "127.0.0.1", "cognitoAuthenticationType": None, "cognitoAuthenticationProvider": None, "userArn": None, "userAgent": "Custom User Agent String", "user": None, }, "path": "/prod/path/to/resource", "resourcePath": "/{proxy+}", "httpMethod": "POST", "apiId": "1234567890", "protocol": "HTTP/1.1", }, } dynamo_db_event.py000066400000000000000000000032521527070072300410630ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html """ MOCK_LAMBDA_DYNAMO_DB_EVENT = { "Records": [ { "eventID": "1", "eventVersion": "1.0", "dynamodb": { "Keys": {"Id": {"N": "101"}}, "NewImage": { "Message": {"S": "New item!"}, "Id": {"N": "101"}, }, "StreamViewType": "NEW_AND_OLD_IMAGES", "SequenceNumber": "111", "SizeBytes": 26, }, "awsRegion": "us-west-2", "eventName": "INSERT", "eventSourceARN": "arn:aws:dynamodb:us-east-2:123456789012:table/my-table/stream/2023-06-10T19:26:16.525", "eventSource": "aws:dynamodb", }, { "eventID": "2", "eventVersion": "1.0", "dynamodb": { "OldImage": { "Message": {"S": "New item!"}, "Id": {"N": "101"}, }, "SequenceNumber": "222", "Keys": {"Id": {"N": "101"}}, "SizeBytes": 59, "NewImage": { "Message": {"S": "This item has changed"}, "Id": {"N": "101"}, }, "StreamViewType": "NEW_AND_OLD_IMAGES", }, "awsRegion": "us-west-2", "eventName": "MODIFY", "eventSourceARN": "arn:aws:dynamodb:us-east-2:123456789012:table/my-table/stream/2023-06-10T19:26:16.525", "eventSource": "aws:dynamodb", }, ] } lambda_function.py000066400000000000000000000007721527070072300410570ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json from opentelemetry import baggage as baggage_api def handler(event, context): baggage_content = dict(baggage_api.get_all().items()) return json.dumps({"baggage_content": baggage_content}) def rest_api_handler(event, context): return {"statusCode": 200, "body": "200 ok"} def handler_exc(event, context): # pylint: disable=broad-exception-raised raise Exception("500 internal server error") s3_event.py000066400000000000000000000026511527070072300374560ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ https://docs.aws.amazon.com/lambda/latest/dg/with-s3.html """ MOCK_LAMBDA_S3_EVENT = { "Records": [ { "eventVersion": "2.1", "eventSource": "aws:s3", "awsRegion": "us-east-2", "eventTime": "2019-09-03T19:37:27.192Z", "eventName": "ObjectCreated:Put", "userIdentity": {"principalId": "AWS:AIDAINPONIXQXHT3IKHL2"}, "requestParameters": {"sourceIPAddress": "205.255.255.255"}, "responseElements": { "x-amz-request-id": "D82B88E5F771F645", "x-amz-id-2": "vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj5NeC6qMa0emYBDXOo6QBU0Wo=", }, "s3": { "s3SchemaVersion": "1.0", "configurationId": "828aa6fc-f7b5-4305-8584-487c791949c1", "bucket": { "name": "DOC-EXAMPLE-BUCKET", "ownerIdentity": {"principalId": "A3I5XTEXAMAI3E"}, "arn": "arn:aws:s3:::lambda-artifacts-deafc19498e3f2df", }, "object": { "key": "b21b84d653bb07b05b1e6b33684dc11b", "size": 1305107, "eTag": "b21b84d653bb07b05b1e6b33684dc11b", "sequencer": "0C0F6F405D6ED209E1", }, }, } ] } sns_event.py000066400000000000000000000025741527070072300377400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html """ MOCK_LAMBDA_SNS_EVENT = { "Records": [ { "EventVersion": "1.0", "EventSubscriptionArn": "arn:aws:sns:us-east-1:123456789012:sns-lambda:21be56ed-a058-49f5-8c98-aedd2564c486", "EventSource": "aws:sns", "Sns": { "SignatureVersion": "1", "Timestamp": "2019-01-02T12:45:07.000Z", "Signature": "mock-signature", "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-ac565b8b1a6c5d002d285f9598aa1d9b.pem", "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e", "Message": "Hello from SNS!", "MessageAttributes": { "Test": {"Type": "String", "Value": "TestString"}, "TestBinary": {"Type": "Binary", "Value": "TestBinary"}, }, "Type": "Notification", "UnsubscribeURL": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:123456789012:test-lambda:21be56ed-a058-49f5-8c98-aedd2564c486", "TopicArn": "arn:aws:sns:us-east-1:123456789012:sns-lambda", "Subject": "TestInvoke", }, } ] } sqs_event.py000066400000000000000000000357371527070072300377520ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html """ MOCK_LAMBDA_SQS_EVENT = { "Records": [ { "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", "body": "Test message.", "attributes": { "ApproximateReceiveCount": "1", "SentTimestamp": "1545082649183", "SenderId": "AIDAIENQZJOLO23YVJ4VO", "ApproximateFirstReceiveTimestamp": "1545082649185", }, "messageAttributes": {}, "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", "eventSource": "aws:sqs", "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue", "awsRegion": "us-east-2", } ] } # SQS event with a W3C traceparent in messageAttributes to test span link extraction. MOCK_LAMBDA_SQS_EVENT_WITH_TRACE_CONTEXT = { "Records": [ { "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", "body": "Test message.", "attributes": { "ApproximateReceiveCount": "1", "SentTimestamp": "1545082649183", "SenderId": "AIDAIENQZJOLO23YVJ4VO", "ApproximateFirstReceiveTimestamp": "1545082649185", }, "messageAttributes": { "traceparent": { "dataType": "String", "stringValue": "00-5ce0e9a56015fec5aadfa328ae398115-ab54a98ceb1f0ad2-01", } }, "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", "eventSource": "aws:sqs", "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue", "awsRegion": "us-east-2", } ] } # SQS event with an uppercase TRACEPARENT key to test case-insensitive extraction. MOCK_LAMBDA_SQS_EVENT_UPPERCASE_ATTRS = { "Records": [ { "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", "body": "Test message.", "attributes": { "ApproximateReceiveCount": "1", "SentTimestamp": "1545082649183", "SenderId": "AIDAIENQZJOLO23YVJ4VO", "ApproximateFirstReceiveTimestamp": "1545082649185", }, "messageAttributes": { "TRACEPARENT": { "dataType": "String", "stringValue": "00-5ce0e9a56015fec5aadfa328ae398115-ab54a98ceb1f0ad2-01", } }, "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", "eventSource": "aws:sqs", "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue", "awsRegion": "us-east-2", } ] } # SQS event where messageAttributes is explicitly None. MOCK_LAMBDA_SQS_EVENT_NULL_MESSAGE_ATTRS = { "Records": [ { "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", "body": "Test message.", "attributes": { "ApproximateReceiveCount": "1", "SentTimestamp": "1545082649183", "SenderId": "AIDAIENQZJOLO23YVJ4VO", "ApproximateFirstReceiveTimestamp": "1545082649185", }, "messageAttributes": None, "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", "eventSource": "aws:sqs", "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue", "awsRegion": "us-east-2", } ] } # SQS event where the messageAttributes key is entirely absent from the record. MOCK_LAMBDA_SQS_EVENT_ABSENT_MESSAGE_ATTRS = { "Records": [ { "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", "body": "Test message.", "attributes": { "ApproximateReceiveCount": "1", "SentTimestamp": "1545082649183", "SenderId": "AIDAIENQZJOLO23YVJ4VO", "ApproximateFirstReceiveTimestamp": "1545082649185", }, "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", "eventSource": "aws:sqs", "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue", "awsRegion": "us-east-2", } ] } # Three record batch: record 1 has no context, record 2 uses a title case key, record 3 lowercase. # Exactly two span links should be extracted (records 2 and 3). MOCK_LAMBDA_SQS_BATCH_EVENT_PARTIAL_CONTEXT = { "Records": [ { "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", "body": "Test message 1 — no trace context.", "attributes": { "ApproximateReceiveCount": "1", "SentTimestamp": "1545082649183", "SenderId": "AIDAIENQZJOLO23YVJ4VO", "ApproximateFirstReceiveTimestamp": "1545082649185", }, "messageAttributes": {}, "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", "eventSource": "aws:sqs", "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue", "awsRegion": "us-east-2", }, { "messageId": "2e1424d4-f796-459a-8184-9c92662be6da", "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0b...", "body": "Test message 2 — title-case Traceparent.", "attributes": { "ApproximateReceiveCount": "1", "SentTimestamp": "1545082650636", "SenderId": "AIDAIENQZJOLO23YVJ4VO", "ApproximateFirstReceiveTimestamp": "1545082650649", }, "messageAttributes": { "Traceparent": { "dataType": "String", "stringValue": "00-aabbccddeeff00112233445566778899-aabbccddeeff0011-01", } }, "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b4", "eventSource": "aws:sqs", "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue", "awsRegion": "us-east-2", }, { "messageId": "7a3b5c9d-1234-5678-abcd-ef0123456789", "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0c...", "body": "Test message 3 — lowercase traceparent.", "attributes": { "ApproximateReceiveCount": "1", "SentTimestamp": "1545082651700", "SenderId": "AIDAIENQZJOLO23YVJ4VO", "ApproximateFirstReceiveTimestamp": "1545082651710", }, "messageAttributes": { "traceparent": { "dataType": "String", "stringValue": "00-cafebabe12345678cafebabe12345678-cafebabe12345678-01", } }, "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b5", "eventSource": "aws:sqs", "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue", "awsRegion": "us-east-2", }, ] } # Malformed: Records list is empty, _is_sqs_event should return False. MOCK_MALFORMED_SQS_EVENT_EMPTY_RECORDS = {"Records": []} # Malformed: Records is a string, not a list, _is_sqs_event should return False. MOCK_MALFORMED_SQS_EVENT_RECORDS_NOT_LIST = {"Records": "not-a-list"} # Malformed: eventSource is "aws:s3", not "aws:sqs", _is_sqs_event should return False. MOCK_MALFORMED_SQS_EVENT_WRONG_SOURCE = { "Records": [ { "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", "body": "Test message.", "attributes": {}, "messageAttributes": {}, "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", "eventSource": "aws:s3", "eventSourceARN": "arn:aws:s3:::my-bucket", "awsRegion": "us-east-2", } ] } # Malformed: no Records key at all, _is_sqs_event should return False. MOCK_MALFORMED_SQS_EVENT_NO_RECORDS_KEY = {"body": "oops, no Records key"} # SQS event with a traceparent whose stringValue is not a valid W3C traceparent. # The propagator will parse it as an invalid context, no span link should be extracted. MOCK_LAMBDA_SQS_EVENT_INVALID_TRACEPARENT = { "Records": [ { "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", "body": "Test message.", "attributes": { "ApproximateReceiveCount": "1", "SentTimestamp": "1545082649183", "SenderId": "AIDAIENQZJOLO23YVJ4VO", "ApproximateFirstReceiveTimestamp": "1545082649185", }, "messageAttributes": { "traceparent": { "dataType": "String", "stringValue": "not-a-valid-traceparent", } }, "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", "eventSource": "aws:sqs", "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue", "awsRegion": "us-east-2", } ] } # SQS event where the messageAttributes value is a bare string rather than a dict. # _SqsMessageAttributesGetter checks isinstance(msg_attr, Mapping), so no link is extracted. MOCK_LAMBDA_SQS_EVENT_ATTR_NOT_DICT = { "Records": [ { "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", "body": "Test message.", "attributes": { "ApproximateReceiveCount": "1", "SentTimestamp": "1545082649183", "SenderId": "AIDAIENQZJOLO23YVJ4VO", "ApproximateFirstReceiveTimestamp": "1545082649185", }, "messageAttributes": { "traceparent": "bare-string-value", }, "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", "eventSource": "aws:sqs", "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue", "awsRegion": "us-east-2", } ] } # SQS event where the attribute dict has dataType but no stringValue key. # The getter returns None for missing stringValue, no link is extracted. MOCK_LAMBDA_SQS_EVENT_MISSING_STRING_VALUE = { "Records": [ { "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", "body": "Test message.", "attributes": { "ApproximateReceiveCount": "1", "SentTimestamp": "1545082649183", "SenderId": "AIDAIENQZJOLO23YVJ4VO", "ApproximateFirstReceiveTimestamp": "1545082649185", }, "messageAttributes": { "traceparent": {"dataType": "String"}, }, "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", "eventSource": "aws:sqs", "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue", "awsRegion": "us-east-2", } ] } # SQS event with no eventSourceARN key, _extract_sqs_queue_name falls back to "". # The CONSUMER span name becomes "process". MOCK_LAMBDA_SQS_EVENT_MISSING_ARN = { "Records": [ { "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", "body": "Test message.", "attributes": { "ApproximateReceiveCount": "1", "SentTimestamp": "1545082649183", "SenderId": "AIDAIENQZJOLO23YVJ4VO", "ApproximateFirstReceiveTimestamp": "1545082649185", }, "messageAttributes": {}, "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", "eventSource": "aws:sqs", "awsRegion": "us-east-2", } ] } # SQS event with a malformed ARN (no colons, fewer than 6 parts). # _extract_sqs_queue_name returns the full string, CONSUMER span name becomes "process bad-arn". MOCK_LAMBDA_SQS_EVENT_SHORT_ARN = { "Records": [ { "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", "body": "Test message.", "attributes": { "ApproximateReceiveCount": "1", "SentTimestamp": "1545082649183", "SenderId": "AIDAIENQZJOLO23YVJ4VO", "ApproximateFirstReceiveTimestamp": "1545082649185", }, "messageAttributes": {}, "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", "eventSource": "aws:sqs", "eventSourceARN": "bad-arn", "awsRegion": "us-east-2", } ] } # SQS event with two records to test batch attribute handling. MOCK_LAMBDA_SQS_BATCH_EVENT = { "Records": [ { "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", "body": "Test message 1.", "attributes": { "ApproximateReceiveCount": "1", "SentTimestamp": "1545082649183", "SenderId": "AIDAIENQZJOLO23YVJ4VO", "ApproximateFirstReceiveTimestamp": "1545082649185", }, "messageAttributes": { "traceparent": { "dataType": "String", "stringValue": "00-5ce0e9a56015fec5aadfa328ae398115-ab54a98ceb1f0ad2-01", } }, "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", "eventSource": "aws:sqs", "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue", "awsRegion": "us-east-2", }, { "messageId": "2e1424d4-f796-459a-8184-9c92662be6da", "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0b...", "body": "Test message 2.", "attributes": { "ApproximateReceiveCount": "1", "SentTimestamp": "1545082650636", "SenderId": "AIDAIENQZJOLO23YVJ4VO", "ApproximateFirstReceiveTimestamp": "1545082650649", }, "messageAttributes": { "traceparent": { "dataType": "String", "stringValue": "00-8a3c60f7d188f8fa79d48a391a778fa6-53995c3f42cd8ad8-01", } }, "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b4", "eventSource": "aws:sqs", "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue", "awsRegion": "us-east-2", }, ] } test_aws_lambda_instrumentation_manual.py000066400000000000000000001331501527070072300446240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-aws-lambda/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines import logging import os from collections.abc import Callable from copy import deepcopy from dataclasses import dataclass from importlib import import_module, reload from typing import Any from unittest import mock from opentelemetry import propagate from opentelemetry.baggage.propagation import W3CBaggagePropagator from opentelemetry.environment_variables import OTEL_PROPAGATORS from opentelemetry.instrumentation.aws_lambda import ( _HANDLER, _X_AMZN_TRACE_ID, OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT, AwsLambdaInstrumentor, _default_event_context_extractor, ) from opentelemetry.propagate import get_global_textmap from opentelemetry.propagators.aws.aws_xray_propagator import ( TRACE_ID_FIRST_PART_LENGTH, TRACE_ID_VERSION, ) from opentelemetry.semconv._incubating.attributes.cloud_attributes import ( CLOUD_ACCOUNT_ID, CLOUD_RESOURCE_ID, ) from opentelemetry.semconv._incubating.attributes.faas_attributes import ( FAAS_INVOCATION_ID, FAAS_TRIGGER, FaasTriggerValues, ) from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_METHOD, HTTP_ROUTE, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_TARGET, HTTP_USER_AGENT, ) from opentelemetry.semconv._incubating.attributes.messaging_attributes import ( MESSAGING_BATCH_MESSAGE_COUNT, MESSAGING_DESTINATION_NAME, MESSAGING_MESSAGE_ID, MESSAGING_OPERATION_TYPE, MESSAGING_SYSTEM, MessagingOperationTypeValues, MessagingSystemValues, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_HOST_NAME, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import NoOpTracerProvider, SpanKind, StatusCode from opentelemetry.trace.propagation.tracecontext import ( TraceContextTextMapPropagator, ) from opentelemetry.util._importlib_metadata import entry_points from .mocks.alb_conventional_headers_event import MOCK_LAMBDA_ALB_EVENT from .mocks.alb_multi_value_headers_event import ( MOCK_LAMBDA_ALB_MULTI_VALUE_HEADER_EVENT, ) from .mocks.api_gateway_http_api_event import ( MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT, ) from .mocks.api_gateway_proxy_event import MOCK_LAMBDA_API_GATEWAY_PROXY_EVENT from .mocks.dynamo_db_event import MOCK_LAMBDA_DYNAMO_DB_EVENT from .mocks.s3_event import MOCK_LAMBDA_S3_EVENT from .mocks.sns_event import MOCK_LAMBDA_SNS_EVENT from .mocks.sqs_event import ( MOCK_LAMBDA_SQS_BATCH_EVENT, MOCK_LAMBDA_SQS_BATCH_EVENT_PARTIAL_CONTEXT, MOCK_LAMBDA_SQS_EVENT, MOCK_LAMBDA_SQS_EVENT_ABSENT_MESSAGE_ATTRS, MOCK_LAMBDA_SQS_EVENT_ATTR_NOT_DICT, MOCK_LAMBDA_SQS_EVENT_INVALID_TRACEPARENT, MOCK_LAMBDA_SQS_EVENT_MISSING_ARN, MOCK_LAMBDA_SQS_EVENT_MISSING_STRING_VALUE, MOCK_LAMBDA_SQS_EVENT_NULL_MESSAGE_ATTRS, MOCK_LAMBDA_SQS_EVENT_SHORT_ARN, MOCK_LAMBDA_SQS_EVENT_UPPERCASE_ATTRS, MOCK_LAMBDA_SQS_EVENT_WITH_TRACE_CONTEXT, MOCK_MALFORMED_SQS_EVENT_EMPTY_RECORDS, MOCK_MALFORMED_SQS_EVENT_NO_RECORDS_KEY, MOCK_MALFORMED_SQS_EVENT_RECORDS_NOT_LIST, MOCK_MALFORMED_SQS_EVENT_WRONG_SOURCE, ) class MockLambdaContext: def __init__(self, function_name, aws_request_id, invoked_function_arn): self.function_name = function_name self.invoked_function_arn = invoked_function_arn self.aws_request_id = aws_request_id MOCK_LAMBDA_CONTEXT = MockLambdaContext( function_name="myfunction", aws_request_id="mock_aws_request_id", invoked_function_arn="arn:aws:lambda:us-east-1:123456:function:myfunction:myalias", ) MOCK_LAMBDA_CONTEXT_ATTRIBUTES = { CLOUD_RESOURCE_ID: ":".join(MOCK_LAMBDA_CONTEXT.invoked_function_arn.split(":")[:7]), FAAS_INVOCATION_ID: MOCK_LAMBDA_CONTEXT.aws_request_id, CLOUD_ACCOUNT_ID: MOCK_LAMBDA_CONTEXT.invoked_function_arn.split(":")[4], } MOCK_XRAY_TRACE_ID = 0x5FB7331105E8BB83207FA31D4D9CDB4C MOCK_XRAY_TRACE_ID_STR = f"{MOCK_XRAY_TRACE_ID:x}" MOCK_XRAY_PARENT_SPAN_ID = 0x3328B8445A6DBAD2 MOCK_XRAY_TRACE_CONTEXT_COMMON = f"Root={TRACE_ID_VERSION}-{MOCK_XRAY_TRACE_ID_STR[:TRACE_ID_FIRST_PART_LENGTH]}-{MOCK_XRAY_TRACE_ID_STR[TRACE_ID_FIRST_PART_LENGTH:]};Parent={MOCK_XRAY_PARENT_SPAN_ID:x}" MOCK_XRAY_TRACE_CONTEXT_SAMPLED = f"{MOCK_XRAY_TRACE_CONTEXT_COMMON};Sampled=1" MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED = f"{MOCK_XRAY_TRACE_CONTEXT_COMMON};Sampled=0" # See more: # https://www.w3.org/TR/trace-context/#examples-of-http-traceparent-headers MOCK_W3C_TRACE_ID = 0x5CE0E9A56015FEC5AADFA328AE398115 MOCK_W3C_PARENT_SPAN_ID = 0xAB54A98CEB1F0AD2 MOCK_W3C_TRACE_CONTEXT_SAMPLED = f"00-{MOCK_W3C_TRACE_ID:x}-{MOCK_W3C_PARENT_SPAN_ID:x}-01" MOCK_W3C_TRACE_STATE_KEY = "vendor_specific_key" MOCK_W3C_TRACE_STATE_VALUE = "test_value" MOCK_W3C_BAGGAGE_KEY = "baggage_key" MOCK_W3C_BAGGAGE_VALUE = "baggage_value" def mock_execute_lambda(event=None, context=None): """Mocks the AWS Lambda execution. NOTE: We don't use `moto`'s `mock_lambda` because we are not instrumenting calls to AWS Lambda using the AWS SDK. Instead, we are instrumenting AWS Lambda itself. See more: https://docs.aws.amazon.com/lambda/latest/dg/runtimes-modify.html#runtime-wrapper Args: event: The Lambda event which may or may not be used by instrumentation. context: The AWS Lambda context to call the handler with """ module_name, handler_name = os.environ[_HANDLER].rsplit(".", 1) handler_module = import_module(module_name.replace("/", ".")) return getattr(handler_module, handler_name)(event, context or MOCK_LAMBDA_CONTEXT) class TestAwsLambdaInstrumentorBase(TestBase): """AWS Lambda Instrumentation Testsuite""" def setUp(self): super().setUp() self.common_env_patch = mock.patch.dict( "os.environ", { _HANDLER: "tests.mocks.lambda_function.handler", "AWS_LAMBDA_FUNCTION_NAME": "mylambda", }, ) self.common_env_patch.start() # NOTE: Whether AwsLambdaInstrumentor().instrument() is run is decided # by each test case. It depends on if the test is for auto or manual # instrumentation. def tearDown(self): super().tearDown() self.common_env_patch.stop() AwsLambdaInstrumentor().uninstrument() class TestAwsLambdaInstrumentor(TestAwsLambdaInstrumentorBase): def test_default_event_context_extractor_does_not_log_for_missing_headers( self, ): with mock.patch("opentelemetry.instrumentation.aws_lambda.logger.debug") as debug_mock: _default_event_context_extractor({}) self.assertEqual(debug_mock.call_count, 0) def test_active_tracing(self): test_env_patch = mock.patch.dict( "os.environ", { **os.environ, # Using Active tracing OTEL_PROPAGATORS: "xray-lambda", _X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_SAMPLED, }, ) test_env_patch.start() reload(propagate) AwsLambdaInstrumentor().instrument() mock_execute_lambda() spans = self.memory_exporter.get_finished_spans() assert spans self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, MOCK_LAMBDA_CONTEXT.function_name) self.assertEqual(span.get_span_context().trace_id, MOCK_XRAY_TRACE_ID) self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes( span, MOCK_LAMBDA_CONTEXT_ATTRIBUTES, ) parent_context = span.parent self.assertEqual(parent_context.trace_id, span.get_span_context().trace_id) self.assertEqual(parent_context.span_id, MOCK_XRAY_PARENT_SPAN_ID) self.assertTrue(parent_context.is_remote) test_env_patch.stop() def test_parent_context_from_lambda_event(self): @dataclass class TestCase: name: str custom_extractor: Callable[[Any], None] context: dict expected_traceid: int expected_parentid: int xray_traceid: str expected_state_value: str = None expected_trace_state_len: int = 0 propagators: str = "tracecontext" expected_baggage: str = None disable_aws_context_propagation: bool = False disable_aws_context_propagation_envvar: str = "" def custom_event_context_extractor(lambda_event): return get_global_textmap().extract(lambda_event["foo"]["headers"]) tests = [ TestCase( name="no_custom_extractor", custom_extractor=None, context={ "headers": { TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME: MOCK_W3C_TRACE_CONTEXT_SAMPLED, TraceContextTextMapPropagator._TRACESTATE_HEADER_NAME: f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2", } }, expected_traceid=MOCK_W3C_TRACE_ID, expected_parentid=MOCK_W3C_PARENT_SPAN_ID, expected_trace_state_len=3, expected_state_value=MOCK_W3C_TRACE_STATE_VALUE, xray_traceid=MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED, ), TestCase( name="custom_extractor_not_sampled_xray", custom_extractor=custom_event_context_extractor, context={ "foo": { "headers": { TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME: MOCK_W3C_TRACE_CONTEXT_SAMPLED, TraceContextTextMapPropagator._TRACESTATE_HEADER_NAME: f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2", } } }, expected_traceid=MOCK_W3C_TRACE_ID, expected_parentid=MOCK_W3C_PARENT_SPAN_ID, expected_trace_state_len=3, expected_state_value=MOCK_W3C_TRACE_STATE_VALUE, xray_traceid=MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED, ), TestCase( name="custom_extractor_sampled_xray", custom_extractor=custom_event_context_extractor, context={ "foo": { "headers": { TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME: MOCK_W3C_TRACE_CONTEXT_SAMPLED, TraceContextTextMapPropagator._TRACESTATE_HEADER_NAME: f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2", } } }, expected_traceid=MOCK_XRAY_TRACE_ID, expected_parentid=MOCK_XRAY_PARENT_SPAN_ID, xray_traceid=MOCK_XRAY_TRACE_CONTEXT_SAMPLED, propagators="xray-lambda", ), TestCase( name="custom_extractor_sampled_xray", custom_extractor=custom_event_context_extractor, context={ "foo": { "headers": { TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME: MOCK_W3C_TRACE_CONTEXT_SAMPLED, TraceContextTextMapPropagator._TRACESTATE_HEADER_NAME: f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2", } } }, expected_traceid=MOCK_W3C_TRACE_ID, expected_parentid=MOCK_W3C_PARENT_SPAN_ID, expected_trace_state_len=3, expected_state_value=MOCK_W3C_TRACE_STATE_VALUE, xray_traceid=MOCK_XRAY_TRACE_CONTEXT_SAMPLED, ), TestCase( name="no_custom_extractor_xray", custom_extractor=None, context={ "headers": { TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME: MOCK_W3C_TRACE_CONTEXT_SAMPLED, TraceContextTextMapPropagator._TRACESTATE_HEADER_NAME: f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2", } }, expected_traceid=MOCK_W3C_TRACE_ID, expected_parentid=MOCK_W3C_PARENT_SPAN_ID, expected_trace_state_len=3, expected_state_value=MOCK_W3C_TRACE_STATE_VALUE, xray_traceid=MOCK_XRAY_TRACE_CONTEXT_SAMPLED, ), TestCase( name="baggage_propagation", custom_extractor=None, context={ "headers": { TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME: MOCK_W3C_TRACE_CONTEXT_SAMPLED, TraceContextTextMapPropagator._TRACESTATE_HEADER_NAME: f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2", W3CBaggagePropagator._BAGGAGE_HEADER_NAME: f"{MOCK_W3C_BAGGAGE_KEY}={MOCK_W3C_BAGGAGE_VALUE}", } }, expected_traceid=MOCK_W3C_TRACE_ID, expected_parentid=MOCK_W3C_PARENT_SPAN_ID, expected_trace_state_len=3, expected_state_value=MOCK_W3C_TRACE_STATE_VALUE, xray_traceid=MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED, expected_baggage=MOCK_W3C_BAGGAGE_VALUE, propagators="tracecontext,baggage", ), TestCase( name="case_insensitive_headers_uppercase", custom_extractor=None, context={ "headers": { TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME.upper(): MOCK_W3C_TRACE_CONTEXT_SAMPLED, TraceContextTextMapPropagator._TRACESTATE_HEADER_NAME.upper(): f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2", } }, expected_traceid=MOCK_W3C_TRACE_ID, expected_parentid=MOCK_W3C_PARENT_SPAN_ID, expected_trace_state_len=3, expected_state_value=MOCK_W3C_TRACE_STATE_VALUE, xray_traceid=MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED, ), TestCase( name="case_insensitive_headers_mixedcase", custom_extractor=None, context={ "headers": { "TraceParent": MOCK_W3C_TRACE_CONTEXT_SAMPLED, "tRaCeStAtE": f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2", } }, expected_traceid=MOCK_W3C_TRACE_ID, expected_parentid=MOCK_W3C_PARENT_SPAN_ID, expected_trace_state_len=3, expected_state_value=MOCK_W3C_TRACE_STATE_VALUE, xray_traceid=MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED, ), ] for test in tests: with self.subTest(test_name=test.name): test_env_patch = mock.patch.dict( "os.environ", { **os.environ, # NOT Active Tracing _X_AMZN_TRACE_ID: test.xray_traceid, OTEL_PROPAGATORS: test.propagators, }, ) test_env_patch.start() reload(propagate) AwsLambdaInstrumentor().instrument( event_context_extractor=test.custom_extractor, ) mock_execute_lambda(test.context) spans = self.memory_exporter.get_finished_spans() assert spans self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.get_span_context().trace_id, test.expected_traceid) parent_context = span.parent self.assertEqual(parent_context.trace_id, span.get_span_context().trace_id) self.assertEqual(parent_context.span_id, test.expected_parentid) self.assertEqual( len(parent_context.trace_state), test.expected_trace_state_len, ) self.assertEqual( parent_context.trace_state.get(MOCK_W3C_TRACE_STATE_KEY), test.expected_state_value, ) self.assertTrue(parent_context.is_remote) self.memory_exporter.clear() AwsLambdaInstrumentor().uninstrument() test_env_patch.stop() def test_lambda_no_error_with_invalid_flush_timeout(self): test_env_patch = mock.patch.dict( "os.environ", { **os.environ, # NOT Active Tracing _X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED, # NOT using the X-Ray Propagator OTEL_PROPAGATORS: "tracecontext", OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT: "invalid-timeout-string", }, ) test_env_patch.start() AwsLambdaInstrumentor().instrument() mock_execute_lambda() spans = self.memory_exporter.get_finished_spans() assert spans self.assertEqual(len(spans), 1) test_env_patch.stop() def test_api_gateway_v1_attributes_case_insensitivity(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda( { "httpMethod": "GET", "headers": { "user-agent": "lowercase-agent", "host": "lowercase-host", "x-forwarded-proto": "http", }, "resource": "/test", "requestContext": { "version": "1.0", }, } ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes.get(HTTP_USER_AGENT), "lowercase-agent") self.assertEqual(span.attributes.get(NET_HOST_NAME), "lowercase-host") self.assertEqual(span.attributes.get(HTTP_SCHEME), "http") self.memory_exporter.clear() mock_execute_lambda( { "httpMethod": "GET", "headers": { "uSeR-aGeNt": "mixed-agent", "hOsT": "mixed-host", "X-fOrWaRdEd-PrOtO": "https", }, "resource": "/test", "requestContext": { "version": "1.0", }, } ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes.get(HTTP_USER_AGENT), "mixed-agent") self.assertEqual(span.attributes.get(NET_HOST_NAME), "mixed-host") self.assertEqual(span.attributes.get(HTTP_SCHEME), "https") def test_lambda_handles_multiple_consumers(self): test_env_patch = mock.patch.dict( "os.environ", { **os.environ, # NOT Active Tracing _X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED, # NOT using the X-Ray Propagator OTEL_PROPAGATORS: "tracecontext", }, ) test_env_patch.start() AwsLambdaInstrumentor().instrument() mock_execute_lambda({"Records": [{"eventSource": "aws:sqs"}]}) mock_execute_lambda({"Records": [{"eventSource": "aws:s3"}]}) mock_execute_lambda({"Records": [{"EventSource": "aws:sns"}]}) mock_execute_lambda({"Records": [{"eventSource": "aws:dynamodb"}]}) spans = self.memory_exporter.get_finished_spans() assert spans # SQS produces 2 spans (SERVER invocation + CONSUMER process), others produce 1 each assert len(spans) == 5 server_spans = [s for s in spans if s.kind == SpanKind.SERVER] consumer_spans = [s for s in spans if s.kind == SpanKind.CONSUMER] assert len(server_spans) == 4 assert len(consumer_spans) == 1 test_env_patch.stop() def test_lambda_handles_invalid_event_source(self): test_env_patch = mock.patch.dict( "os.environ", { **os.environ, # NOT Active Tracing _X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED, # NOT using the X-Ray Propagator OTEL_PROPAGATORS: "tracecontext", }, ) test_env_patch.start() reload(propagate) AwsLambdaInstrumentor().instrument() mock_execute_lambda({"Records": [{"eventSource": "invalid_source"}]}) spans = self.memory_exporter.get_finished_spans() assert spans assert len(spans) == 1 # Default to SERVER for unknown sources assert spans[0].kind == SpanKind.SERVER for span in spans: self.assertSpanHasAttributes( span, MOCK_LAMBDA_CONTEXT_ATTRIBUTES, ) test_env_patch.stop() def test_lambda_handles_list_event(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda([{"message": "test"}]) spans = self.memory_exporter.get_finished_spans() assert spans def test_lambda_handles_handler_exception(self): exc_env_patch = mock.patch.dict( "os.environ", {_HANDLER: "tests.mocks.lambda_function.handler_exc"}, ) exc_env_patch.start() AwsLambdaInstrumentor().instrument() # instrumentor re-raises the exception with self.assertRaises(Exception): mock_execute_lambda() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(len(span.events), 1) event = span.events[0] self.assertEqual(event.name, "exception") exc_env_patch.stop() @mock.patch("opentelemetry.instrumentation.aws_lambda.logger") def test_lambda_handles_should_do_nothing_when_aws_lambda_environment_variables_not_present(self, logger_mock): exc_env_patch = mock.patch.dict( "os.environ", {_HANDLER: "tests.mocks.lambda_function.handler"}, clear=True, ) exc_env_patch.start() AwsLambdaInstrumentor().instrument() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) exc_env_patch.stop() logger_mock.warnings.assert_not_called() def test_lambda_handles_should_warn_when_handler_environment_variable_not_present( self, ): exc_env_patch = mock.patch.dict( "os.environ", {"AWS_LAMBDA_FUNCTION_NAME": "mylambda"}, clear=True, ) exc_env_patch.start() with self.assertLogs(level=logging.WARNING) as warning: AwsLambdaInstrumentor().instrument() self.assertEqual(len(warning.records), 1) self.assertIn( "This instrumentation requires the OpenTelemetry Lambda extension installed", warning.records[0].message, ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) exc_env_patch.stop() def test_uninstrument(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.memory_exporter.clear() AwsLambdaInstrumentor().uninstrument() mock_execute_lambda(MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_no_op_tracer_provider(self): tracer_provider = NoOpTracerProvider() AwsLambdaInstrumentor().instrument(tracer_provider=tracer_provider) mock_execute_lambda(MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT) spans = self.memory_exporter.get_finished_spans() assert spans is not None self.assertEqual(len(spans), 0) def test_load_entry_point(self): self.assertIs( next(iter(entry_points(group="opentelemetry_instrumentor", name="aws-lambda"))).load(), AwsLambdaInstrumentor, ) # pylint: disable-next=too-many-public-methods class TestAwsLambdaInstrumentorMocks(TestAwsLambdaInstrumentorBase): def test_api_gateway_proxy_event_sets_attributes(self): handler_patch = mock.patch.dict( "os.environ", {_HANDLER: "tests.mocks.lambda_function.rest_api_handler"}, ) handler_patch.start() AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_API_GATEWAY_PROXY_EVENT) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span, *_ = spans self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes( span, MOCK_LAMBDA_CONTEXT_ATTRIBUTES, ) self.assertSpanHasAttributes( span, { FAAS_TRIGGER: "http", HTTP_METHOD: "POST", HTTP_ROUTE: "/{proxy+}", HTTP_TARGET: "/{proxy+}?foo=bar", NET_HOST_NAME: "0123456789.execute-api.us-east-1.amazonaws.com", HTTP_USER_AGENT: "Custom User Agent String", HTTP_SCHEME: "https", HTTP_STATUS_CODE: 200, }, ) def test_api_gateway_http_api_proxy_event_sets_attributes(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span, *_ = spans self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes( span, MOCK_LAMBDA_CONTEXT_ATTRIBUTES, ) self.assertSpanHasAttributes( span, { FAAS_TRIGGER: "http", HTTP_METHOD: "POST", HTTP_ROUTE: "/path/to/resource", HTTP_TARGET: "/path/to/resource?parameter1=value1¶meter1=value2¶meter2=value", NET_HOST_NAME: "id.execute-api.us-east-1.amazonaws.com", HTTP_USER_AGENT: "agent", }, ) def test_alb_conventional_event_sets_attributes(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_ALB_EVENT) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span, *_ = spans self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes( span, MOCK_LAMBDA_CONTEXT_ATTRIBUTES, ) self.assertSpanHasAttributes( span, { FAAS_TRIGGER: "http", HTTP_METHOD: "GET", }, ) def test_alb_multi_value_header_event_sets_attributes(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_ALB_MULTI_VALUE_HEADER_EVENT) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span, *_ = spans self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes( span, MOCK_LAMBDA_CONTEXT_ATTRIBUTES, ) self.assertSpanHasAttributes( span, { FAAS_TRIGGER: "http", HTTP_METHOD: "GET", HTTP_SCHEME: "https", HTTP_USER_AGENT: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6)", NET_HOST_NAME: "lambda-846800462-us-east-2.elb.amazonaws.com", }, ) def test_alb_mixed_header_event_sets_attributes(self): AwsLambdaInstrumentor().instrument() event = deepcopy(MOCK_LAMBDA_ALB_MULTI_VALUE_HEADER_EVENT) event["headers"] = {"accept": "text/html,application/xhtml+xml"} mock_execute_lambda(event) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span, *_ = spans self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes( span, { HTTP_SCHEME: "https", HTTP_USER_AGENT: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6)", NET_HOST_NAME: "lambda-846800462-us-east-2.elb.amazonaws.com", }, ) def test_alb_mixed_header_event_prefers_multi_value_headers(self): AwsLambdaInstrumentor().instrument() event = deepcopy(MOCK_LAMBDA_ALB_MULTI_VALUE_HEADER_EVENT) event["headers"] = { "host": "wrong.example.com", "user-agent": "wrong-agent", "x-forwarded-proto": "http", } mock_execute_lambda(event) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span, *_ = spans self.assertSpanHasAttributes( span, { HTTP_SCHEME: "https", HTTP_USER_AGENT: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6)", NET_HOST_NAME: "lambda-846800462-us-east-2.elb.amazonaws.com", }, ) @mock.patch.dict( "os.environ", { _X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED, OTEL_PROPAGATORS: "tracecontext", }, ) def test_alb_multi_value_header_event_extracts_parent_context(self): reload(propagate) AwsLambdaInstrumentor().instrument() event = deepcopy(MOCK_LAMBDA_ALB_MULTI_VALUE_HEADER_EVENT) event["multiValueHeaders"][TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME] = [ MOCK_W3C_TRACE_CONTEXT_SAMPLED ] mock_execute_lambda(event) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span, *_ = spans self.assertEqual(span.get_span_context().trace_id, MOCK_W3C_TRACE_ID) parent_context = span.parent self.assertEqual(parent_context.trace_id, span.get_span_context().trace_id) self.assertEqual(parent_context.span_id, MOCK_W3C_PARENT_SPAN_ID) self.assertTrue(parent_context.is_remote) @mock.patch.dict( "os.environ", { _X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED, OTEL_PROPAGATORS: "tracecontext", }, ) def test_alb_mixed_header_event_extracts_parent_context(self): reload(propagate) AwsLambdaInstrumentor().instrument() event = deepcopy(MOCK_LAMBDA_ALB_MULTI_VALUE_HEADER_EVENT) event["headers"] = {"accept": "text/html,application/xhtml+xml"} event["multiValueHeaders"][TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME] = [ MOCK_W3C_TRACE_CONTEXT_SAMPLED ] mock_execute_lambda(event) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span, *_ = spans self.assertEqual(span.get_span_context().trace_id, MOCK_W3C_TRACE_ID) parent_context = span.parent self.assertEqual(parent_context.trace_id, span.get_span_context().trace_id) self.assertEqual(parent_context.span_id, MOCK_W3C_PARENT_SPAN_ID) self.assertTrue(parent_context.is_remote) @mock.patch.dict( "os.environ", { _X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED, OTEL_PROPAGATORS: "tracecontext", }, ) def test_alb_mixed_header_event_prefers_multi_value_traceparent(self): reload(propagate) AwsLambdaInstrumentor().instrument() event = deepcopy(MOCK_LAMBDA_ALB_MULTI_VALUE_HEADER_EVENT) event["headers"] = { TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME: ( "00-11111111111111111111111111111111-2222222222222222-01" ) } event["multiValueHeaders"][TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME] = [ MOCK_W3C_TRACE_CONTEXT_SAMPLED ] mock_execute_lambda(event) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span, *_ = spans self.assertEqual(span.get_span_context().trace_id, MOCK_W3C_TRACE_ID) parent_context = span.parent self.assertEqual(parent_context.trace_id, span.get_span_context().trace_id) self.assertEqual(parent_context.span_id, MOCK_W3C_PARENT_SPAN_ID) self.assertTrue(parent_context.is_remote) def test_dynamo_db_event_sets_attributes(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_DYNAMO_DB_EVENT) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span, *_ = spans self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes( span, MOCK_LAMBDA_CONTEXT_ATTRIBUTES, ) def test_s3_event_sets_attributes(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_S3_EVENT) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span, *_ = spans self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes( span, MOCK_LAMBDA_CONTEXT_ATTRIBUTES, ) def test_sns_event_sets_attributes(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_SNS_EVENT) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span, *_ = spans self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes( span, MOCK_LAMBDA_CONTEXT_ATTRIBUTES, ) def test_sqs_event_sets_attributes(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_SQS_EVENT) spans = self.memory_exporter.get_finished_spans() # SQS produces two spans: inner CONSUMER first, outer SERVER second self.assertEqual(len(spans), 2) consumer_span, server_span, *_ = spans self.assertEqual(consumer_span.kind, SpanKind.CONSUMER) self.assertEqual(server_span.kind, SpanKind.SERVER) self.assertSpanHasAttributes( server_span, { **MOCK_LAMBDA_CONTEXT_ATTRIBUTES, FAAS_TRIGGER: FaasTriggerValues.PUBSUB.value, }, ) self.assertSpanHasAttributes( consumer_span, { MESSAGING_SYSTEM: MessagingSystemValues.AWS_SQS.value, MESSAGING_DESTINATION_NAME: "my-queue", MESSAGING_OPERATION_TYPE: MessagingOperationTypeValues.PROCESS.value, }, ) self.assertEqual(consumer_span.name, "process my-queue") # Single record event should have no batch count attribute self.assertNotIn(MESSAGING_BATCH_MESSAGE_COUNT, consumer_span.attributes) # No span links when messageAttributes is empty self.assertEqual(len(consumer_span.links), 0) # CONSUMER span is a child of the SERVER span self.assertEqual( consumer_span.parent.span_id, server_span.get_span_context().span_id, ) def test_sqs_event_span_links(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_SQS_EVENT_WITH_TRACE_CONTEXT) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) consumer_span, _server_span, *_ = spans self.assertEqual(consumer_span.kind, SpanKind.CONSUMER) # Exactly one link corresponding to the producer's trace context self.assertEqual(len(consumer_span.links), 1) link = consumer_span.links[0] self.assertEqual( link.context.trace_id, 0x5CE0E9A56015FEC5AADFA328AE398115, ) self.assertEqual( link.context.span_id, 0xAB54A98CEB1F0AD2, ) self.assertEqual( link.attributes.get(MESSAGING_MESSAGE_ID), "059f36b4-87a3-44ab-83d2-661975830a7d", ) def test_sqs_batch_event_attributes(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_SQS_BATCH_EVENT) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) consumer_span, _server_span, *_ = spans self.assertEqual(consumer_span.kind, SpanKind.CONSUMER) self.assertEqual(consumer_span.attributes.get(MESSAGING_BATCH_MESSAGE_COUNT), 2) self.assertEqual(len(consumer_span.links), 2) def test_sqs_message_attributes_case_insensitive(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_SQS_EVENT_UPPERCASE_ATTRS) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) consumer_span, _server_span, *_ = spans self.assertEqual(consumer_span.kind, SpanKind.CONSUMER) # Uppercase TRACEPARENT key should still produce a span link self.assertEqual(len(consumer_span.links), 1) link = consumer_span.links[0] self.assertEqual( link.context.trace_id, 0x5CE0E9A56015FEC5AADFA328AE398115, ) self.assertEqual( link.context.span_id, 0xAB54A98CEB1F0AD2, ) @mock.patch.dict("os.environ", {_HANDLER: "tests.mocks.lambda_function.handler_exc"}) def test_sqs_event_exception(self): AwsLambdaInstrumentor().instrument() with self.assertRaises(Exception): mock_execute_lambda(MOCK_LAMBDA_SQS_EVENT) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) consumer_span, server_span, *_ = spans self.assertEqual(consumer_span.status.status_code, StatusCode.ERROR) self.assertEqual(server_span.status.status_code, StatusCode.ERROR) # Both spans should have an exception event recorded self.assertTrue(any(e.name == "exception" for e in consumer_span.events)) self.assertTrue(any(e.name == "exception" for e in server_span.events)) def test_sqs_event_null_message_attrs(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_SQS_EVENT_NULL_MESSAGE_ATTRS) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) consumer_span, _, *_ = spans self.assertEqual(consumer_span.kind, SpanKind.CONSUMER) self.assertEqual(len(consumer_span.links), 0) def test_sqs_event_absent_message_attrs(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_SQS_EVENT_ABSENT_MESSAGE_ATTRS) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) consumer_span, _, *_ = spans self.assertEqual(consumer_span.kind, SpanKind.CONSUMER) self.assertEqual(len(consumer_span.links), 0) def test_sqs_batch_event_partial_context(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_SQS_BATCH_EVENT_PARTIAL_CONTEXT) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) consumer_span, _, *_ = spans self.assertEqual(consumer_span.kind, SpanKind.CONSUMER) # Record 1 has no context records 2 and 3 do self.assertEqual(len(consumer_span.links), 2) trace_ids = {link.context.trace_id for link in consumer_span.links} self.assertIn(0xAABBCCDDEEFF00112233445566778899, trace_ids) self.assertIn(0xCAFEBABE12345678CAFEBABE12345678, trace_ids) def test_malformed_sqs_event_empty_records(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_MALFORMED_SQS_EVENT_EMPTY_RECORDS) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].kind, SpanKind.SERVER) def test_malformed_sqs_event_records_not_list(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_MALFORMED_SQS_EVENT_RECORDS_NOT_LIST) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].kind, SpanKind.SERVER) def test_malformed_sqs_event_wrong_source(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_MALFORMED_SQS_EVENT_WRONG_SOURCE) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].kind, SpanKind.SERVER) def test_malformed_sqs_event_no_records_key(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_MALFORMED_SQS_EVENT_NO_RECORDS_KEY) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].kind, SpanKind.SERVER) def test_sqs_event_invalid_traceparent(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_SQS_EVENT_INVALID_TRACEPARENT) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) consumer_span, _, *_ = spans self.assertEqual(consumer_span.kind, SpanKind.CONSUMER) self.assertEqual(len(consumer_span.links), 0) def test_sqs_event_attr_not_dict(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_SQS_EVENT_ATTR_NOT_DICT) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) consumer_span, _, *_ = spans self.assertEqual(consumer_span.kind, SpanKind.CONSUMER) self.assertEqual(len(consumer_span.links), 0) def test_sqs_event_missing_string_value(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_SQS_EVENT_MISSING_STRING_VALUE) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) consumer_span, _, *_ = spans self.assertEqual(consumer_span.kind, SpanKind.CONSUMER) self.assertEqual(len(consumer_span.links), 0) def test_sqs_event_missing_arn_span_name(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_SQS_EVENT_MISSING_ARN) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) consumer_span, _, *_ = spans self.assertEqual(consumer_span.kind, SpanKind.CONSUMER) self.assertEqual(consumer_span.name, "process") def test_sqs_event_short_arn_span_name(self): AwsLambdaInstrumentor().instrument() mock_execute_lambda(MOCK_LAMBDA_SQS_EVENT_SHORT_ARN) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) consumer_span, _, *_ = spans self.assertEqual(consumer_span.kind, SpanKind.CONSUMER) self.assertEqual(consumer_span.name, "process bad-arn") def test_slash_delimited_handler_path(self): """Test that slash-delimited handler paths work correctly. AWS Lambda accepts both slash-delimited (python/functions/api.handler) and dot-delimited (python.functions.api.handler) handler paths. This test ensures the instrumentation handles both formats. """ # Test slash-delimited format slash_env_patch = mock.patch.dict( "os.environ", {_HANDLER: "tests/mocks/lambda_function.handler"}, ) slash_env_patch.start() AwsLambdaInstrumentor().instrument() mock_execute_lambda() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertSpanHasAttributes( spans[0], MOCK_LAMBDA_CONTEXT_ATTRIBUTES, ) slash_env_patch.stop() AwsLambdaInstrumentor().uninstrument() self.memory_exporter.clear() # Test dot-delimited format (should still work) dot_env_patch = mock.patch.dict( "os.environ", {_HANDLER: "tests.mocks.lambda_function.handler"}, ) dot_env_patch.start() AwsLambdaInstrumentor().instrument() mock_execute_lambda() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertSpanHasAttributes( spans[0], MOCK_LAMBDA_CONTEXT_ATTRIBUTES, ) dot_env_patch.stop() def test_lambda_handles_handler_exception_with_api_gateway_proxy_event( self, ): exc_env_patch = mock.patch.dict( "os.environ", {_HANDLER: "tests.mocks.lambda_function.handler_exc"}, ) exc_env_patch.start() AwsLambdaInstrumentor().instrument() # instrumentor re-raises the exception with self.assertRaises(Exception): mock_execute_lambda({"requestContext": {"http": {"method": "GET"}}}) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span, *_ = spans self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(len(span.events), 1) event, *_ = span.events self.assertEqual(event.name, "exception") exc_env_patch.stop() opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-boto3sqs/000077500000000000000000000000001527070072300327205ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-boto3sqs/LICENSE000066400000000000000000000261351527070072300337340ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000012031527070072300343240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-boto3sqsOpenTelemetry Boto3 SQS Instrumentation ======================================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-boto3sqs.svg :target: https://pypi.org/project/opentelemetry-instrumentation-boto3sqs/ This library allows tracing requests made by the Boto3 library to the SQS service. Installation ------------ :: pip install opentelemetry-instrumentation-boto3sqs References ---------- * `OpenTelemetry boto3sqs/ Tracing `_ * `OpenTelemetry Project `_ pyproject.toml000066400000000000000000000031751527070072300355630ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-boto3sqs[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-boto3sqs" dynamic = ["version"] description = "Boto3 SQS service tracing for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [ "boto3 ~= 1.0", ] [project.entry-points.opentelemetry_instrumentor] boto3 = "opentelemetry.instrumentation.boto3sqs:Boto3SQSInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-boto3sqs" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/boto3sqs/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-boto3sqs/src/000077500000000000000000000000001527070072300335075ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300363245ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-boto3sqs/srcinstrumentation/000077500000000000000000000000001527070072300415675ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetryboto3sqs/000077500000000000000000000000001527070072300433445ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation__init__.py000066400000000000000000000365061527070072300454670ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation/boto3sqs# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Instrument `boto3sqs`_ to trace SQS applications. .. _boto3sqs: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sqs.html Usage ----- .. code-block:: python import boto3 from opentelemetry.instrumentation.boto3sqs import Boto3SQSInstrumentor Boto3SQSInstrumentor().instrument() --- """ import logging from collections.abc import Collection, Generator, Mapping from typing import Any import boto3.session import botocore.client from wrapt import wrap_function_wrapper from opentelemetry import context, propagate, trace from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import ( is_instrumentation_enabled, unwrap, ) from opentelemetry.propagators.textmap import CarrierT, Getter, Setter from opentelemetry.semconv._incubating.attributes import messaging_attributes from opentelemetry.semconv.trace import ( MessagingDestinationKindValues, MessagingOperationValues, SpanAttributes, ) from opentelemetry.trace import Link, Span, SpanKind, Tracer, TracerProvider from .package import _instruments from .version import __version__ _logger = logging.getLogger(__name__) _IS_SQS_INSTRUMENTED_ATTRIBUTE = "_otel_boto3sqs_instrumented" class Boto3SQSGetter(Getter[CarrierT]): def get(self, carrier: CarrierT, key: str) -> list[str] | None: msg_attr = carrier.get(key) if not isinstance(msg_attr, Mapping): return None value = msg_attr.get("StringValue") if value is None: return None return [value] def keys(self, carrier: CarrierT) -> list[str]: return list(carrier.keys()) class Boto3SQSSetter(Setter[CarrierT]): def set(self, carrier: CarrierT, key: str, value: str) -> None: # This is a limitation defined by AWS for SQS MessageAttributes size if len(carrier.items()) < 10: carrier[key] = { "StringValue": value, "DataType": "String", } else: _logger.warning( "Boto3 SQS instrumentation: cannot set context propagation on SQS/SNS message due to maximum amount of " "MessageAttributes" ) boto3sqs_getter = Boto3SQSGetter() boto3sqs_setter = Boto3SQSSetter() class Boto3SQSInstrumentor(BaseInstrumentor): received_messages_spans: dict[str, Span] = {} current_span_related_to_token: Span = None current_context_token = None class ContextableList(list): """ Since the classic way to process SQS messages is using a `for` loop, without a well defined scope like a callback - we are doing something similar to the instrumentation of Kafka-python and instrumenting the `__iter__` functions and the `__getitem__` functions to set the span context of the addressed message. Since the return value from an `SQS.ReceiveMessage` returns a builtin list, we cannot wrap it and change all of the calls for `list.__iter__` and `list.__getitem__` - therefore we use ContextableList. It is bound to the received_messages_spans dict """ def __getitem__(self, key: int) -> Any: retval = super().__getitem__(key) if not isinstance(retval, dict): return retval receipt_handle = retval.get("ReceiptHandle") if not receipt_handle: return retval started_span = Boto3SQSInstrumentor.received_messages_spans.get(receipt_handle) if started_span is None: return retval if Boto3SQSInstrumentor.current_context_token: context.detach(Boto3SQSInstrumentor.current_context_token) Boto3SQSInstrumentor.current_context_token = context.attach(trace.set_span_in_context(started_span)) Boto3SQSInstrumentor.current_span_related_to_token = started_span return retval def __iter__(self) -> Generator: index = 0 while index < len(self): yield self[index] index = index + 1 def instrumentation_dependencies(self) -> Collection[str]: return _instruments @staticmethod def _enrich_span( span: Span, queue_name: str, queue_url: str, conversation_id: str | None = None, operation: MessagingOperationValues | None = None, message_id: str | None = None, ) -> None: if not span.is_recording(): return span.set_attribute(messaging_attributes.MESSAGING_SYSTEM, "aws.sqs") span.set_attribute(SpanAttributes.MESSAGING_DESTINATION, queue_name) span.set_attribute( SpanAttributes.MESSAGING_DESTINATION_KIND, MessagingDestinationKindValues.QUEUE.value, ) span.set_attribute(SpanAttributes.MESSAGING_URL, queue_url) if operation: span.set_attribute(SpanAttributes.MESSAGING_OPERATION, operation.value) if conversation_id: span.set_attribute(SpanAttributes.MESSAGING_CONVERSATION_ID, conversation_id) if message_id: span.set_attribute(messaging_attributes.MESSAGING_MESSAGE_ID, message_id) @staticmethod def _safe_end_processing_span(receipt_handle: str) -> None: started_span: Span = Boto3SQSInstrumentor.received_messages_spans.pop(receipt_handle, None) if started_span: if Boto3SQSInstrumentor.current_span_related_to_token == started_span: if Boto3SQSInstrumentor.current_context_token: context.detach(Boto3SQSInstrumentor.current_context_token) Boto3SQSInstrumentor.current_context_token = None started_span.end() @staticmethod def _extract_queue_name_from_url(queue_url: str) -> str: # A Queue name cannot have the `/` char, therefore we can return the part after the last / return queue_url.rsplit("/", maxsplit=1)[-1] def _create_processing_span( self, queue_name: str, queue_url: str, receipt_handle: str, message: dict[str, Any], ) -> None: message_attributes = message.get("MessageAttributes", {}) links = [] ctx = propagate.extract(message_attributes, getter=boto3sqs_getter) parent_span_ctx = trace.get_current_span(ctx).get_span_context() if parent_span_ctx.is_valid: links.append(Link(context=parent_span_ctx)) span = self._tracer.start_span(name=f"{queue_name} process", links=links, kind=SpanKind.CONSUMER) with trace.use_span(span): message_id = message.get("MessageId") Boto3SQSInstrumentor.received_messages_spans[receipt_handle] = span Boto3SQSInstrumentor._enrich_span( span, queue_name, queue_url, message_id=message_id, operation=MessagingOperationValues.PROCESS, ) def _wrap_send_message(self, sqs_class: type) -> None: def send_wrapper(wrapped, instance, args, kwargs): if not is_instrumentation_enabled(): return wrapped(*args, **kwargs) queue_url = kwargs.get("QueueUrl") # The method expect QueueUrl and Entries params, so if they are None, we call wrapped to receive the # original exception queue_name = Boto3SQSInstrumentor._extract_queue_name_from_url(queue_url) with self._tracer.start_as_current_span( name=f"{queue_name} send", kind=SpanKind.PRODUCER, end_on_exit=True, ) as span: Boto3SQSInstrumentor._enrich_span(span, queue_name, queue_url) attributes = kwargs.pop("MessageAttributes", {}) propagate.inject(attributes, setter=boto3sqs_setter) retval = wrapped(*args, MessageAttributes=attributes, **kwargs) message_id = retval.get("MessageId") if message_id: if span.is_recording(): span.set_attribute( messaging_attributes.MESSAGING_MESSAGE_ID, message_id, ) return retval wrap_function_wrapper(sqs_class, "send_message", send_wrapper) def _wrap_send_message_batch(self, sqs_class: type) -> None: def send_batch_wrapper(wrapped, instance, args, kwargs): queue_url = kwargs.get("QueueUrl") entries = kwargs.get("Entries") # The method expect QueueUrl and Entries params, so if they are None, we call wrapped to receive the # original exception if not is_instrumentation_enabled() or not queue_url or not entries: return wrapped(*args, **kwargs) queue_name = Boto3SQSInstrumentor._extract_queue_name_from_url(queue_url) ids_to_spans: dict[str, Span] = {} for entry in entries: entry_id = entry["Id"] span = self._tracer.start_span(name=f"{queue_name} send", kind=SpanKind.PRODUCER) ids_to_spans[entry_id] = span Boto3SQSInstrumentor._enrich_span(span, queue_name, queue_url, conversation_id=entry_id) with trace.use_span(span): if "MessageAttributes" not in entry: entry["MessageAttributes"] = {} propagate.inject(entry["MessageAttributes"], setter=boto3sqs_setter) retval = wrapped(*args, **kwargs) for successful_messages in retval.get("Successful", []): message_identifier = successful_messages["Id"] message_span = ids_to_spans.get(message_identifier) if message_span: if message_span.is_recording(): message_span.set_attribute( messaging_attributes.MESSAGING_MESSAGE_ID, successful_messages.get("MessageId"), ) for span in ids_to_spans.values(): span.end() return retval wrap_function_wrapper(sqs_class, "send_message_batch", send_batch_wrapper) def _wrap_receive_message(self, sqs_class: type) -> None: def receive_message_wrapper(wrapped, instance, args, kwargs): queue_url = kwargs.get("QueueUrl") message_attribute_names = kwargs.pop("MessageAttributeNames", []) message_attribute_names.extend(propagate.get_global_textmap().fields) queue_name = Boto3SQSInstrumentor._extract_queue_name_from_url(queue_url) with self._tracer.start_as_current_span( name=f"{queue_name} receive", end_on_exit=True, kind=SpanKind.CONSUMER, ) as span: Boto3SQSInstrumentor._enrich_span( span, queue_name, queue_url, operation=MessagingOperationValues.RECEIVE, ) retval = wrapped( *args, MessageAttributeNames=message_attribute_names, **kwargs, ) messages = retval.get("Messages", []) if not messages: return retval for message in messages: receipt_handle = message.get("ReceiptHandle") if not receipt_handle: continue Boto3SQSInstrumentor._safe_end_processing_span(receipt_handle) self._create_processing_span(queue_name, queue_url, receipt_handle, message) retval["Messages"] = Boto3SQSInstrumentor.ContextableList(messages) return retval wrap_function_wrapper(sqs_class, "receive_message", receive_message_wrapper) @staticmethod def _wrap_delete_message(sqs_class: type) -> None: def delete_message_wrapper(wrapped, instance, args, kwargs): receipt_handle = kwargs.get("ReceiptHandle") if receipt_handle: Boto3SQSInstrumentor._safe_end_processing_span(receipt_handle) return wrapped(*args, **kwargs) wrap_function_wrapper(sqs_class, "delete_message", delete_message_wrapper) @staticmethod def _wrap_delete_message_batch(sqs_class: type) -> None: def delete_message_wrapper_batch(wrapped, instance, args, kwargs): entries = kwargs.get("Entries") for entry in entries: receipt_handle = entry.get("ReceiptHandle") if receipt_handle: Boto3SQSInstrumentor._safe_end_processing_span(receipt_handle) return wrapped(*args, **kwargs) wrap_function_wrapper(sqs_class, "delete_message_batch", delete_message_wrapper_batch) def _wrap_client_creation(self) -> None: """ Since botocore creates classes on the fly using schemas, the SQS class is not necesraily created upon the call of `instrument()`. Therefore we need to wrap the creation of the boto3 client, which triggers the creation of the SQS client. """ def client_wrapper(wrapped, instance, args, kwargs): retval = wrapped(*args, **kwargs) self._decorate_sqs(type(retval)) return retval wrap_function_wrapper(boto3.session.Session, "client", client_wrapper) def _decorate_sqs(self, sqs_class: type) -> None: """ Since botocore creates classes on the fly using schemas, we try to find the class that inherits from the base class and is SQS to wrap. """ # We define SQS client as the only client that implements send_message_batch if not hasattr(sqs_class, "send_message_batch"): return if getattr(sqs_class, _IS_SQS_INSTRUMENTED_ATTRIBUTE, False): return setattr(sqs_class, _IS_SQS_INSTRUMENTED_ATTRIBUTE, True) self._wrap_send_message(sqs_class) self._wrap_send_message_batch(sqs_class) self._wrap_receive_message(sqs_class) self._wrap_delete_message(sqs_class) self._wrap_delete_message_batch(sqs_class) @staticmethod def _un_decorate_sqs(sqs_class: type) -> None: if not getattr(sqs_class, _IS_SQS_INSTRUMENTED_ATTRIBUTE, False): return unwrap(sqs_class, "send_message") unwrap(sqs_class, "send_message_batch") unwrap(sqs_class, "receive_message") unwrap(sqs_class, "delete_message") unwrap(sqs_class, "delete_message_batch") setattr(sqs_class, _IS_SQS_INSTRUMENTED_ATTRIBUTE, False) def _instrument(self, **kwargs: dict[str, Any]) -> None: self._tracer_provider: TracerProvider | None = kwargs.get("tracer_provider") self._tracer: Tracer = trace.get_tracer( __name__, __version__, self._tracer_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) self._wrap_client_creation() for client_cls in botocore.client.BaseClient.__subclasses__(): self._decorate_sqs(client_cls) def _uninstrument(self, **kwargs: dict[str, Any]) -> None: unwrap(boto3.session.Session, "client") for client_cls in botocore.client.BaseClient.__subclasses__(): self._un_decorate_sqs(client_cls) package.py000066400000000000000000000002461527070072300453130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation/boto3sqs# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Collection _instruments: Collection[str] = ("boto3 ~= 1.0",) version.py000066400000000000000000000001441527070072300454020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation/boto3sqs# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000006061527070072300371040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-boto3sqsasgiref==3.8.1 boto3==1.34.44 botocore==1.34.44 Deprecated==1.2.14 iniconfig==2.0.0 jmespath==1.0.1 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 python-dateutil==2.8.2 s3transfer==0.10.0 six==1.16.0 tomli==2.0.1 typing_extensions==4.12.2 urllib3==1.26.19 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-boto3sqs opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-boto3sqs/tests/000077500000000000000000000000001527070072300340625ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300361020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-boto3sqs/teststest_boto3sqs_instrumentation.py000066400000000000000000000356011527070072300425210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-boto3sqs/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=no-name-in-module from contextlib import contextmanager from typing import Any from unittest import TestCase, mock import boto3 from botocore.awsrequest import AWSResponse from wrapt import BoundFunctionWrapper from opentelemetry.instrumentation.boto3sqs import ( Boto3SQSGetter, Boto3SQSInstrumentor, Boto3SQSSetter, ) from opentelemetry.semconv._incubating.attributes import messaging_attributes from opentelemetry.semconv.trace import ( MessagingDestinationKindValues, MessagingOperationValues, SpanAttributes, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind, TraceFlags from opentelemetry.trace.span import Span, format_span_id, format_trace_id def _make_sqs_client(*, session=False): return (boto3.Session() if session else boto3).client( "sqs", region_name="us-east-1", aws_access_key_id="dummy", aws_secret_access_key="dummy", ) def _make_sqs_resource(*, session=False): return (boto3.Session() if session else boto3).resource( "sqs", region_name="us-east-1", aws_access_key_id="dummy", aws_secret_access_key="dummy", ) class TestBoto3SQSInstrumentor(TestCase): def _assert_instrumented(self, client): self.assertIsInstance(client.send_message, BoundFunctionWrapper) self.assertIsInstance(client.send_message_batch, BoundFunctionWrapper) self.assertIsInstance(client.receive_message, BoundFunctionWrapper) self.assertIsInstance(client.delete_message, BoundFunctionWrapper) self.assertIsInstance(client.delete_message_batch, BoundFunctionWrapper) def _assert_uninstrumented(self, client): self.assertNotIsInstance(client.send_message, BoundFunctionWrapper) self.assertNotIsInstance(client.send_message_batch, BoundFunctionWrapper) self.assertNotIsInstance(client.receive_message, BoundFunctionWrapper) self.assertNotIsInstance(client.delete_message, BoundFunctionWrapper) self.assertNotIsInstance(client.delete_message_batch, BoundFunctionWrapper) @staticmethod @contextmanager def _active_instrumentor(): Boto3SQSInstrumentor().instrument() try: yield finally: Boto3SQSInstrumentor().uninstrument() def test_instrument_api_before_client_init(self) -> None: for session in (False, True): with self._active_instrumentor(): client = _make_sqs_client(session=session) self._assert_instrumented(client) self._assert_uninstrumented(client) def test_instrument_api_after_client_init(self) -> None: for session in (False, True): client = _make_sqs_client(session=session) with self._active_instrumentor(): self._assert_instrumented(client) self._assert_uninstrumented(client) def test_instrument_multiple_clients(self): for session in (False, True): with self._active_instrumentor(): self._assert_instrumented(_make_sqs_client(session=session)) self._assert_instrumented(_make_sqs_client(session=session)) def test_instrument_api_before_resource_init(self) -> None: for session in (False, True): with self._active_instrumentor(): sqs = _make_sqs_resource(session=session) self._assert_instrumented(sqs.meta.client) self._assert_uninstrumented(sqs.meta.client) def test_instrument_api_after_resource_init(self) -> None: for session in (False, True): sqs = _make_sqs_resource(session=session) with self._active_instrumentor(): self._assert_instrumented(sqs.meta.client) self._assert_uninstrumented(sqs.meta.client) def test_instrument_multiple_resources(self): for session in (False, True): with self._active_instrumentor(): self._assert_instrumented(_make_sqs_resource(session=session).meta.client) self._assert_instrumented(_make_sqs_resource(session=session).meta.client) class TestBoto3SQSGetter(TestCase): def setUp(self) -> None: self.getter = Boto3SQSGetter() def test_get_none(self) -> None: carrier = {} value = self.getter.get(carrier, "test") self.assertIsNone(value) def test_get_value(self) -> None: key = "test" value = "value" carrier = {key: {"StringValue": value, "DataType": "String"}} val = self.getter.get(carrier, key) self.assertEqual(val, [value]) def test_keys(self): carrier = { "test1": {"StringValue": "value1", "DataType": "String"}, "test2": {"StringValue": "value2", "DataType": "String"}, } keys = self.getter.keys(carrier) self.assertEqual(keys, list(carrier.keys())) def test_keys_empty(self): keys = self.getter.keys({}) self.assertEqual(keys, []) class TestBoto3SQSSetter(TestCase): def setUp(self) -> None: self.setter = Boto3SQSSetter() def test_simple(self): original_key = "SomeHeader" original_value = {"NumberValue": 1, "DataType": "Number"} carrier = {original_key: original_value.copy()} key = "test" value = "value" self.setter.set(carrier, key, value) # Ensure the original value is not harmed for dict_key, dict_val in carrier[original_key].items(): self.assertEqual(original_value[dict_key], dict_val) # Ensure the new key is added well self.assertEqual(carrier[key]["StringValue"], value) class TestBoto3SQSInstrumentation(TestBase): def setUp(self): super().setUp() self._reset_instrumentor() Boto3SQSInstrumentor().instrument() self._client = _make_sqs_client() self._queue_name = "MyQueue" self._queue_url = f"https://sqs.us-east-1.amazonaws.com/123456789012/{self._queue_name}" def tearDown(self): super().tearDown() Boto3SQSInstrumentor().uninstrument() self._reset_instrumentor() @staticmethod def _reset_instrumentor(): Boto3SQSInstrumentor.received_messages_spans.clear() Boto3SQSInstrumentor.current_span_related_to_token = None Boto3SQSInstrumentor.current_context_token = None @staticmethod def _make_aws_response_func(response): def _response_func(*args, **kwargs): return AWSResponse("http://127.0.0.1", 200, {}, "{}"), response return _response_func @contextmanager def _mocked_endpoint(self, response): response_func = self._make_aws_response_func(response) with mock.patch("botocore.endpoint.Endpoint.make_request", new=response_func): yield def _assert_injected_span(self, msg_attrs: dict[str, Any], span: Span): trace_parent = msg_attrs["traceparent"]["StringValue"] ctx = span.get_span_context() self.assertEqual( self._to_trace_parent(ctx.trace_id, ctx.span_id, ctx.trace_flags), trace_parent.lower(), ) def _default_span_attrs(self): return { messaging_attributes.MESSAGING_SYSTEM: "aws.sqs", SpanAttributes.MESSAGING_DESTINATION: self._queue_name, SpanAttributes.MESSAGING_DESTINATION_KIND: MessagingDestinationKindValues.QUEUE.value, SpanAttributes.MESSAGING_URL: self._queue_url, } @staticmethod def _to_trace_parent(trace_id: int, span_id: int, trace_flags: TraceFlags) -> str: return f"00-{format_trace_id(trace_id)}-{format_span_id(span_id)}-{trace_flags:02x}".lower() def _get_only_span(self): spans = self.get_finished_spans() self.assertEqual(1, len(spans)) return spans[0] @staticmethod def _make_message(message_id: str, body: str, receipt: str): return { "MessageId": message_id, "ReceiptHandle": receipt, "MD5OfBody": "777", "Body": body, "Attributes": {}, "MD5OfMessageAttributes": "111", "MessageAttributes": {}, } def _add_trace_parent(self, message: dict[str, Any], trace_id: int, span_id: int): message["MessageAttributes"]["traceparent"] = { "StringValue": self._to_trace_parent(trace_id, span_id, TraceFlags.get_default()), "DataType": "String", } def test_send_message(self): message_id = "123456789" mock_response = { "MD5OfMessageBody": "1234", "MD5OfMessageAttributes": "5678", "MD5OfMessageSystemAttributes": "9012", "MessageId": message_id, "SequenceNumber": "0", } message_attrs = {} with self._mocked_endpoint(mock_response): self._client.send_message( QueueUrl=self._queue_url, MessageBody="hello msg", MessageAttributes=message_attrs, ) span = self._get_only_span() self.assertEqual(f"{self._queue_name} send", span.name) self.assertEqual(SpanKind.PRODUCER, span.kind) self.assertEqual( { messaging_attributes.MESSAGING_MESSAGE_ID: message_id, **self._default_span_attrs(), }, span.attributes, ) self._assert_injected_span(message_attrs, span) def test_send_message_batch(self): expected_message_ids = {"1": "msg-1", "2": "msg-2"} mock_response = { "Successful": [ {"Id": "1", "MessageId": "msg-1", "MD5OfMessageBody": "11"}, {"Id": "2", "MessageId": "msg-2", "MD5OfMessageBody": "22"}, ], "Failed": [], } entries = [ {"Id": "1", "MessageBody": "hello 1"}, {"Id": "2", "MessageBody": "hello 2"}, ] with self._mocked_endpoint(mock_response): self._client.send_message_batch(QueueUrl=self._queue_url, Entries=entries) spans = self.get_finished_spans() self.assertEqual(2, len(spans)) spans_by_entry_id = {span.attributes[SpanAttributes.MESSAGING_CONVERSATION_ID]: span for span in spans} for entry in entries: entry_id = entry["Id"] span = spans_by_entry_id[entry_id] self.assertEqual(f"{self._queue_name} send", span.name) self.assertEqual(SpanKind.PRODUCER, span.kind) self.assertEqual( { SpanAttributes.MESSAGING_CONVERSATION_ID: entry_id, messaging_attributes.MESSAGING_MESSAGE_ID: expected_message_ids[entry_id], **self._default_span_attrs(), }, span.attributes, ) self._assert_injected_span(entry["MessageAttributes"], span) def test_send_message_batch_all_failed(self): mock_response = { "Failed": [ { "Id": "1", "SenderFault": True, "Code": "InvalidParameterValue", "Message": "boom", } ] } entries = [{"Id": "1", "MessageBody": "hello 1"}] with self._mocked_endpoint(mock_response): self._client.send_message_batch(QueueUrl=self._queue_url, Entries=entries) span = self._get_only_span() self.assertEqual(f"{self._queue_name} send", span.name) self.assertEqual(SpanKind.PRODUCER, span.kind) self.assertEqual( { SpanAttributes.MESSAGING_CONVERSATION_ID: "1", **self._default_span_attrs(), }, span.attributes, ) self.assertNotIn(messaging_attributes.MESSAGING_MESSAGE_ID, span.attributes) self._assert_injected_span(entries[0]["MessageAttributes"], span) def test_receive_message(self): msg_def = { "1": {"receipt": "01", "trace_id": 10, "span_id": 1}, "2": {"receipt": "02", "trace_id": 20, "span_id": 2}, } mock_response = {"Messages": []} for msg_id, attrs in msg_def.items(): message = self._make_message(msg_id, f"hello {msg_id}", attrs["receipt"]) self._add_trace_parent(message, attrs["trace_id"], attrs["span_id"]) mock_response["Messages"].append(message) message_attr_names = [] with self._mocked_endpoint(mock_response): response = self._client.receive_message( QueueUrl=self._queue_url, MessageAttributeNames=message_attr_names, ) self.assertIn("traceparent", message_attr_names) # receive span span = self._get_only_span() self.assertEqual(f"{self._queue_name} receive", span.name) self.assertEqual(SpanKind.CONSUMER, span.kind) self.assertEqual( { SpanAttributes.MESSAGING_OPERATION: MessagingOperationValues.RECEIVE.value, **self._default_span_attrs(), }, span.attributes, ) self.memory_exporter.clear() # processing spans self.assertEqual(2, len(response["Messages"])) for msg in response["Messages"]: msg_id = msg["MessageId"] attrs = msg_def[msg_id] with self._mocked_endpoint(None): self._client.delete_message(QueueUrl=self._queue_url, ReceiptHandle=attrs["receipt"]) span = self._get_only_span() self.assertEqual(f"{self._queue_name} process", span.name) # processing span attributes self.assertEqual( { messaging_attributes.MESSAGING_MESSAGE_ID: msg_id, SpanAttributes.MESSAGING_OPERATION: MessagingOperationValues.PROCESS.value, **self._default_span_attrs(), }, span.attributes, ) # processing span links self.assertEqual(1, len(span.links)) link = span.links[0] self.assertEqual(attrs["trace_id"], link.context.trace_id) self.assertEqual(attrs["span_id"], link.context.span_id) self.memory_exporter.clear() def test_uninstrument(self): mock_response = { "MessageId": "123456789", } with self._mocked_endpoint(mock_response): self._client.send_message( QueueUrl=self._queue_url, MessageBody="test", ) spans = self.get_finished_spans() self.assertEqual(1, len(spans)) self.memory_exporter.clear() Boto3SQSInstrumentor().uninstrument() self._client.send_message( QueueUrl=self._queue_url, MessageBody="test", ) spans = self.get_finished_spans() self.assertEqual(0, len(spans)) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/000077500000000000000000000000001527070072300327575ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/LICENSE000066400000000000000000000261351527070072300337730ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000033511527070072300343710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocoreOpenTelemetry Botocore Tracing ============================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-botocore.svg :target: https://pypi.org/project/opentelemetry-instrumentation-botocore/ This library allows tracing requests made by the botocore and aiobotocore libraries. Extensions ---------- The instrumentation supports creating extensions for AWS services for enriching what is collected. We have extensions for the following AWS services: - Bedrock Runtime (botocore only) - DynamoDB - Lambda - SNS - SQS Bedrock Runtime (botocore only) ******************************* This extension implements the GenAI semantic conventions for the following API calls: - Converse - ConverseStream - InvokeModel - InvokeModelWithResponseStream For the Converse and ConverseStream APIs tracing, events and metrics are implemented. For the InvokeModel and InvokeModelWithResponseStream APIs tracing, events and metrics implemented only for a subset of the available models, namely: - Amazon Titan models - Amazon Nova models - Anthropic Claude Tool calls with InvokeModel and InvokeModelWithResponseStream APIs are supported with: - Amazon Nova models - Anthropic Claude 3+ If you don't have an application using Bedrock APIs yet, try our `zero-code examples `_. Installation ------------ :: pip install opentelemetry-instrumentation-botocore References ---------- * `OpenTelemetry Botocore Tracing `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/examples/000077500000000000000000000000001527070072300345755ustar00rootroot00000000000000bedrock-runtime/000077500000000000000000000000001527070072300376105ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/exampleszero-code/000077500000000000000000000000001527070072300414775ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime.env000066400000000000000000000007141527070072300422720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code# Update this with your real values AWS_ACCESS_KEY_ID=key AWS_SECRET_ACCESS_KEY=secret AWS_DEFAULT_REGION=eu-central-1 # Uncomment and set if your credentials are temporary # AWS_SESSION_TOKEN= # Uncomment and change to your OTLP endpoint # OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 # OTEL_EXPORTER_OTLP_PROTOCOL=grpc OTEL_SERVICE_NAME=opentelemetry-python-bedrock # Uncomment if your OTLP endpoint doesn't support logs # OTEL_LOGS_EXPORTER=console README.rst000066400000000000000000000040111527070072300431620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-codeBedrock Zero-Code Instrumentation Example ========================================= This is an example of how to instrument Bedrock calls with zero code changes, using `opentelemetry-instrument`. When examples are run, it exports traces and logs to an OTLP compatible endpoint. Traces include details such as the model used and the duration of the chat request. Logs capture the chat request and the generated response, providing a comprehensive view of the performance and behavior of your OpenAI requests. Note: `.env <.env>`_ file configures additional environment variables: - `OTEL_LOGS_EXPORTER=otlp` to specify exporter type. Available examples ------------------ - `converse.py` uses `bedrock-runtime` `Converse API _`. - `converse_stream.py` uses `bedrock-runtime` `ConverseStream API _`. - `invoke_model.py` uses `bedrock-runtime` `InvokeModel API _`. - `invoke_model_stream.py` uses `bedrock-runtime` `InvokeModelWithResponseStrea API _`. Setup ----- Minimally, update the `.env <.env>`_ file with your "AWS_SECRET_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY", "AWS_DEFAULT_REGION" and if you are using temporary credentials "AWS_SESSION_TOKEN". An OTLP compatible endpoint should be listening for traces and logs on http://localhost:4317. If not, update "OTEL_EXPORTER_OTLP_ENDPOINT" as well. Next, set up a virtual environment like this: :: python3 -m venv .venv source .venv/bin/activate pip install "python-dotenv[cli]" pip install -r requirements.txt Run --- Run the example like this: :: dotenv run -- opentelemetry-instrument python converse.py You should see a poem generated by Bedrock while traces exported to your configured observability tool. converse.py000066400000000000000000000010361527070072300436750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import boto3 def main(): client = boto3.client("bedrock-runtime") response = client.converse( modelId=os.getenv("CHAT_MODEL", "amazon.titan-text-lite-v1"), messages=[ { "role": "user", "content": [{"text": "Write a short poem on OpenTelemetry."}], }, ], ) print(response["output"]["message"]["content"][0]["text"]) if __name__ == "__main__": main() converse_stream.py000066400000000000000000000012321527070072300452460ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import boto3 def main(): client = boto3.client("bedrock-runtime") stream = client.converse_stream( modelId=os.getenv("CHAT_MODEL", "amazon.titan-text-lite-v1"), messages=[ { "role": "user", "content": [{"text": "Write a short poem on OpenTelemetry."}], }, ], ) response = "" for event in stream["stream"]: if "contentBlockDelta" in event: response += event["contentBlockDelta"]["delta"]["text"] print(response) if __name__ == "__main__": main() invoke_model.py000066400000000000000000000012041527070072300445210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import os import boto3 def main(): client = boto3.client("bedrock-runtime") response = client.invoke_model( modelId=os.getenv("CHAT_MODEL", "amazon.titan-text-lite-v1"), body=json.dumps( { "inputText": "Write a short poem on OpenTelemetry.", "textGenerationConfig": {}, }, ), ) body = response["body"].read() response_data = json.loads(body.decode("utf-8")) print(response_data["results"][0]["outputText"]) if __name__ == "__main__": main() invoke_model_stream.py000066400000000000000000000030011527070072300460710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import os import boto3 def main(): chat_model = os.getenv("CHAT_MODEL", "amazon.titan-text-lite-v1") prompt = "Write a short poem on OpenTelemetry." if "amazon.titan" in chat_model: body = { "inputText": prompt, "textGenerationConfig": {}, } elif "amazon.nova" in chat_model: body = { "messages": [{"role": "user", "content": [{"text": prompt}]}], "schemaVersion": "messages-v1", } elif "anthropic.claude" in chat_model: body = { "messages": [{"role": "user", "content": [{"text": prompt, "type": "text"}]}], "anthropic_version": "bedrock-2023-05-31", "max_tokens": 200, } else: raise ValueError() client = boto3.client("bedrock-runtime") response = client.invoke_model_with_response_stream( modelId=chat_model, body=json.dumps(body), ) answer = "" for event in response["body"]: json_bytes = event.get("chunk", {}).get("bytes", b"") decoded = json_bytes.decode("utf-8") chunk = json.loads(decoded) if "outputText" in chunk: answer += chunk["outputText"] elif "completion" in chunk: answer += chunk["completion"] elif "contentBlockDelta" in chunk: answer += chunk["contentBlockDelta"]["delta"]["text"] print(answer) if __name__ == "__main__": main() requirements.txt000066400000000000000000000002451527070072300447640ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-codeboto3~=1.35.99 opentelemetry-sdk~=1.30.0 opentelemetry-exporter-otlp-proto-grpc~=1.30.0 opentelemetry-distro~=0.51b0 opentelemetry-instrumentation-botocore~=0.51b0 pyproject.toml000066400000000000000000000034561527070072300356240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-botocore" dynamic = ["version"] description = "OpenTelemetry Botocore instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.37", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-propagator-aws-xray ~= 1.0", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [] instruments-any = [ "botocore ~= 1.0", "aiobotocore >= 2.0, < 4.0" ] [project.entry-points.opentelemetry_instrumentor] botocore = "opentelemetry.instrumentation.botocore:BotocoreInstrumentor" aiobotocore = "opentelemetry.instrumentation.botocore:AiobotocoreInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-botocore" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/botocore/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/000077500000000000000000000000001527070072300335465ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300363635ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/srcinstrumentation/000077500000000000000000000000001527070072300416265ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetrybotocore/000077500000000000000000000000001527070072300434425ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation__init__.py000066400000000000000000000462061527070072300455630ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Instrument `botocore`_ and `aiobotocore`_ to trace service requests. There are two options for instrumenting code. The first option is to use the ``opentelemetry-instrument`` executable which will automatically instrument your botocore or aiobotocore client. The second is to programmatically enable instrumentation via the following code: .. _botocore: https://pypi.org/project/botocore/ .. _aiobotocore: https://pypi.org/project/aiobotocore/ Usage ----- .. code:: python from opentelemetry.instrumentation.botocore import BotocoreInstrumentor import botocore.session # Instrument botocore BotocoreInstrumentor().instrument() # This will create a span with botocore-specific attributes session = botocore.session.get_session() session.set_credentials( access_key="access-key", secret_key="secret-key" ) ec2 = session.create_client("ec2", region_name="us-west-2") ec2.describe_instances() Async Usage ----------- .. code:: python from opentelemetry.instrumentation.botocore import AiobotocoreInstrumentor import aiobotocore.session import asyncio async def main(): # Instrument Aiobotocore AiobotocoreInstrumentor().instrument() # This will create a span with aiobotocore-specific attributes session = aiobotocore.session.get_session() async with session.create_client("ec2") as client: await client.describe_instances() asyncio.run(main()) Thread Context Propagation -------------------------- boto3's S3 ``upload_file`` and ``download_file`` methods use background threads for multipart transfers. To ensure trace context is propagated to these threads, also enable the threading instrumentation: .. code:: python from opentelemetry.instrumentation.threading import ThreadingInstrumentor from opentelemetry.instrumentation.botocore import BotocoreInstrumentor ThreadingInstrumentor().instrument() BotocoreInstrumentor().instrument() When using auto-instrumentation (``opentelemetry-instrument``), both instrumentors are enabled automatically if their packages are installed. API --- The `instrument` method (for both ``BotocoreInstrumentor`` and ``AiobotocoreInstrumentor``) accepts the following keyword args: * tracer_provider (``TracerProvider``) - an optional tracer provider * request_hook (``Callable[[Span, str, str, dict], None]``) - a function with extra user-defined logic to be performed before performing the request * response_hook (``Callable[[Span, str, str, dict], None]``) - a function with extra user-defined logic to be performed after performing the request for example: .. code:: python from opentelemetry.instrumentation.botocore import BotocoreInstrumentor import botocore.session def request_hook(span, service_name, operation_name, api_params): # request hook logic pass def response_hook(span, service_name, operation_name, result): # response hook logic pass # Instrument botocore with hooks BotocoreInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook) # This will create a span with botocore-specific attributes, including custom attributes added from the hooks session = botocore.session.get_session() session.set_credentials( access_key="access-key", secret_key="secret-key" ) ec2 = session.create_client("ec2", region_name="us-west-2") ec2.describe_instances() """ import logging from collections.abc import Collection from typing import Any from botocore.client import BaseClient from botocore.endpoint import Endpoint from botocore.exceptions import ClientError from wrapt import wrap_function_wrapper from opentelemetry.instrumentation.botocore.extensions import ( _AIOBOTOCORE_EXTENSIONS, _BOTOCORE_EXTENSIONS, ) from opentelemetry.instrumentation.botocore.extensions.registry import ( ExtensionRegistry, ) from opentelemetry.instrumentation.botocore.extensions.types import ( _AwsSdkCallContext, _BotocoreInstrumentorContext, ) from opentelemetry.instrumentation.botocore.package import ( _instruments_aiobotocore, _instruments_botocore, ) from opentelemetry.instrumentation.botocore.utils import ( _safe_invoke, get_server_attributes, ) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import ( http_status_to_status_code, is_instrumentation_enabled, suppress_http_instrumentation, unwrap, ) from opentelemetry.metrics import Instrument from opentelemetry.propagators.aws.aws_xray_propagator import ( TRACE_HEADER_KEY, AwsXRayPropagator, ) from opentelemetry.semconv._incubating.attributes.cloud_attributes import ( CLOUD_REGION, ) from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_STATUS_CODE, ) from opentelemetry.semconv._incubating.attributes.rpc_attributes import ( RPC_METHOD, RPC_SERVICE, RPC_SYSTEM, ) from opentelemetry.trace.span import Span from opentelemetry.trace.status import StatusCode logger = logging.getLogger(__name__) class BotocoreInstrumentor(BaseInstrumentor): """An instrumentor for Botocore. See `BaseInstrumentor` """ def __init__(self): super().__init__() if not hasattr(self, "request_hook"): self.request_hook = None if not hasattr(self, "response_hook"): self.response_hook = None if not hasattr(self, "extension_registry"): self.extension_registry = None if not hasattr(self, "propagator"): self.propagator = AwsXRayPropagator() def instrumentation_dependencies(self) -> Collection[str]: return _instruments_botocore def _instrument(self, **kwargs): # tracers are lazy initialized per-extension in _get_tracer self._tracers = {} # loggers are lazy initialized per-extension in _get_logger self._loggers = {} # meters are lazy initialized per-extension in _get_meter self._meters = {} # metrics are lazy initialized per-extension in _get_metrics self._metrics: dict[str, dict[str, Instrument]] = {} self.request_hook = kwargs.get("request_hook") self.response_hook = kwargs.get("response_hook") propagator = kwargs.get("propagator") if propagator is not None: self.propagator = propagator self.extension_registry = ExtensionRegistry( __name__, _BOTOCORE_EXTENSIONS, kwargs.get("tracer_provider"), kwargs.get("logger_provider"), kwargs.get("meter_provider"), ) wrap_function_wrapper( "botocore.client", "BaseClient._make_api_call", self._patched_api_call, ) wrap_function_wrapper( "botocore.endpoint", "Endpoint.prepare_request", self._patched_endpoint_prepare_request, ) def _uninstrument(self, **kwargs): unwrap(BaseClient, "_make_api_call") unwrap(Endpoint, "prepare_request") # pylint: disable=unused-argument def _patched_endpoint_prepare_request(self, wrapped, instance, args, kwargs): request = args[0] headers = request.headers # There may be situations where both Botocore and Aiobotocore are # instrumented at the same time. To avoid double-injection of headers, # we add a check to see if the header is already present. If it is, # we skip injection. if TRACE_HEADER_KEY in headers: return wrapped(*args, **kwargs) # Only the x-ray header is propagated by AWS services. Using any # other propagator will lose the trace context. self.propagator.inject(headers) return wrapped(*args, **kwargs) # pylint: disable=too-many-branches def _patched_api_call(self, original_func, instance, args, kwargs): if not is_instrumentation_enabled(): return original_func(*args, **kwargs) call_context = _determine_call_context(instance, args) if call_context is None: return original_func(*args, **kwargs) extension = self.extension_registry.get_extension(call_context) if not extension.should_trace_service_call(): return original_func(*args, **kwargs) attributes = { RPC_SYSTEM: "aws-api", RPC_SERVICE: call_context.service_id, RPC_METHOD: call_context.operation, CLOUD_REGION: call_context.region, **get_server_attributes(call_context.endpoint_url), } _safe_invoke(extension.extract_attributes, attributes) end_span_on_exit = extension.should_end_span_on_exit() tracer = self.extension_registry.get_tracer(extension) metrics = self.extension_registry.get_metrics(extension) instrumentor_ctx = _BotocoreInstrumentorContext( logger=self.extension_registry.get_logger(extension), metrics=metrics, ) with tracer.start_as_current_span( call_context.span_name, kind=call_context.span_kind, attributes=attributes, # tracing streaming services require to close the span manually # at a later time after the stream has been consumed end_on_exit=end_span_on_exit, set_status_on_exception=False, ) as span: _safe_invoke(extension.before_service_call, span, instrumentor_ctx) self._call_request_hook(span, call_context) try: with suppress_http_instrumentation(): result = None try: result = original_func(*args, **kwargs) except ClientError as error: result = getattr(error, "response", None) _apply_response_attributes(span, result) http_status = (result or {}).get("ResponseMetadata", {}).get("HTTPStatusCode", 0) if http_status_to_status_code(http_status) is StatusCode.ERROR: span.set_status(StatusCode.ERROR, str(error)) _safe_invoke(extension.on_error, span, error, instrumentor_ctx) else: _safe_invoke(extension.on_success, span, result, instrumentor_ctx) raise except Exception as error: span.set_status(StatusCode.ERROR, str(error)) raise _apply_response_attributes(span, result) _safe_invoke(extension.on_success, span, result, instrumentor_ctx) finally: _safe_invoke(extension.after_service_call, instrumentor_ctx) self._call_response_hook(span, call_context, result) return result def _call_request_hook(self, span: Span, call_context: _AwsSdkCallContext): if not callable(self.request_hook): return self.request_hook( span, call_context.service, call_context.operation, call_context.params, ) def _call_response_hook(self, span: Span, call_context: _AwsSdkCallContext, result): if not callable(self.response_hook): return self.response_hook(span, call_context.service, call_context.operation, result) class AiobotocoreInstrumentor(BaseInstrumentor): """An instrumentor for Aiobotocore. See `BaseInstrumentor` """ def __init__(self): super().__init__() if not hasattr(self, "request_hook"): self.request_hook = None if not hasattr(self, "response_hook"): self.response_hook = None if not hasattr(self, "extension_registry"): self.extension_registry = None if not hasattr(self, "propagator"): self.propagator = AwsXRayPropagator() def instrumentation_dependencies(self) -> Collection[str]: return _instruments_aiobotocore def _instrument(self, **kwargs): # Verify that aiobotocore is present # pylint: disable-next=import-outside-toplevel, unused-import import aiobotocore.client # noqa: PLC0415, F401 # pylint: disable=attribute-defined-outside-init self.request_hook = kwargs.get("request_hook") self.response_hook = kwargs.get("response_hook") propagator = kwargs.get("propagator") if propagator is not None: self.propagator = propagator self.extension_registry = ExtensionRegistry( __name__, _AIOBOTOCORE_EXTENSIONS, kwargs.get("tracer_provider"), kwargs.get("logger_provider"), kwargs.get("meter_provider"), ) wrap_function_wrapper( "aiobotocore.client", "AioBaseClient._make_api_call", self._patched_api_call, ) wrap_function_wrapper( "botocore.endpoint", "Endpoint.prepare_request", self._patched_endpoint_prepare_request, ) def _uninstrument(self, **kwargs): unwrap("aiobotocore.client.AioBaseClient", "_make_api_call") unwrap(Endpoint, "prepare_request") # pylint: disable=unused-argument def _patched_endpoint_prepare_request(self, wrapped, instance, args, kwargs): request = args[0] headers = request.headers # There may be situations where both Botocore and Aiobotocore are # instrumented at the same time. To avoid double-injection of headers, # we add a check to see if the header is already present. If it is, # we skip injection. if TRACE_HEADER_KEY in headers: return wrapped(*args, **kwargs) # Only the x-ray header is propagated by AWS services. Using any # other propagator will lose the trace context. self.propagator.inject(headers) return wrapped(*args, **kwargs) # pylint: disable=too-many-branches async def _patched_api_call(self, original_func, instance, args, kwargs): if not is_instrumentation_enabled(): return await original_func(*args, **kwargs) call_context = _determine_call_context(instance, args) if call_context is None: return await original_func(*args, **kwargs) extension = self.extension_registry.get_extension(call_context) if not extension.should_trace_service_call(): return await original_func(*args, **kwargs) attributes = { RPC_SYSTEM: "aws-api", RPC_SERVICE: call_context.service_id, RPC_METHOD: call_context.operation, CLOUD_REGION: call_context.region, **get_server_attributes(call_context.endpoint_url), } _safe_invoke(extension.extract_attributes, attributes) end_span_on_exit = extension.should_end_span_on_exit() tracer = self.extension_registry.get_tracer(extension) metrics = self.extension_registry.get_metrics(extension) instrumentor_ctx = _BotocoreInstrumentorContext( logger=self.extension_registry.get_logger(extension), metrics=metrics, ) with tracer.start_as_current_span( call_context.span_name, kind=call_context.span_kind, attributes=attributes, # tracing streaming services require to close the span manually # at a later time after the stream has been consumed end_on_exit=end_span_on_exit, set_status_on_exception=False, ) as span: _safe_invoke(extension.before_service_call, span, instrumentor_ctx) self._call_request_hook(span, call_context) try: with suppress_http_instrumentation(): result = None try: result = await original_func(*args, **kwargs) except ClientError as error: result = getattr(error, "response", None) _apply_response_attributes(span, result) http_status = (result or {}).get("ResponseMetadata", {}).get("HTTPStatusCode", 0) if http_status_to_status_code(http_status) is StatusCode.ERROR: span.set_status(StatusCode.ERROR, str(error)) _safe_invoke(extension.on_error, span, error, instrumentor_ctx) else: _safe_invoke(extension.on_success, span, result, instrumentor_ctx) raise except Exception as error: span.set_status(StatusCode.ERROR, str(error)) raise _apply_response_attributes(span, result) _safe_invoke(extension.on_success, span, result, instrumentor_ctx) finally: _safe_invoke(extension.after_service_call, instrumentor_ctx) self._call_response_hook(span, call_context, result) return result def _call_request_hook(self, span: Span, call_context: _AwsSdkCallContext): if not callable(self.request_hook): return self.request_hook( span, call_context.service, call_context.operation, call_context.params, ) def _call_response_hook(self, span: Span, call_context: _AwsSdkCallContext, result): if not callable(self.response_hook): return self.response_hook(span, call_context.service, call_context.operation, result) def _apply_response_attributes(span: Span, result): if result is None or not span.is_recording(): return metadata = result.get("ResponseMetadata") if metadata is None: return request_id = metadata.get("RequestId") if request_id is None: headers = metadata.get("HTTPHeaders") if headers is not None: request_id = headers.get("x-amzn-RequestId") or headers.get("x-amz-request-id") or headers.get("x-amz-id-2") if request_id: # TODO: update when semantic conventions exist span.set_attribute("aws.request_id", request_id) retry_attempts = metadata.get("RetryAttempts") if retry_attempts is not None: # TODO: update when semantic conventions exists span.set_attribute("retry_attempts", retry_attempts) status_code = metadata.get("HTTPStatusCode") if status_code is not None: span.set_attribute(HTTP_STATUS_CODE, status_code) def _determine_call_context(client: BaseClient, args: tuple[str, dict[str, Any]]) -> _AwsSdkCallContext | None: try: call_context = _AwsSdkCallContext(client, args) logger.debug( "AWS SDK invocation: %s %s", call_context.service, call_context.operation, ) return call_context except Exception as ex: # pylint:disable=broad-except # this shouldn't happen actually unless internals of botocore changed and # extracting essential attributes ('service' and 'operation') failed. logger.error("Error when initializing call context", exc_info=ex) return None environment_variables.py000066400000000000000000000002671527070072300504150ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" extensions/000077500000000000000000000000001527070072300456415ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore__init__.py000066400000000000000000000023301527070072300477500ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import importlib import logging _logger = logging.getLogger(__name__) def _lazy_load(module, cls): def loader(): imported_mod = importlib.import_module(module, __name__) return getattr(imported_mod, cls, None) return loader _BOTOCORE_EXTENSIONS = { "bedrock-runtime": _lazy_load(".bedrock", "_BedrockRuntimeExtension"), "dynamodb": _lazy_load(".dynamodb", "_DynamoDbExtension"), "lambda": _lazy_load(".lmbd", "_LambdaExtension"), "secretsmanager": _lazy_load(".secretsmanager", "_SecretsManagerExtension"), "stepfunctions": _lazy_load(".sfns", "_StepFunctionsExtension"), "sns": _lazy_load(".sns", "_SnsExtension"), "sqs": _lazy_load(".sqs", "_SqsExtension"), } _AIOBOTOCORE_EXTENSIONS = { # TODO: Add Bedrock support for aiobotocore "dynamodb": _lazy_load(".dynamodb", "_DynamoDbExtension"), "lambda": _lazy_load(".lmbd", "_LambdaExtension"), "secretsmanager": _lazy_load(".secretsmanager", "_SecretsManagerExtension"), "stepfunctions": _lazy_load(".sfns", "_StepFunctionsExtension"), "sns": _lazy_load(".sns", "_SnsExtension"), "sqs": _lazy_load(".sqs", "_SqsExtension"), } _messaging.py000066400000000000000000000022031527070072300503240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging from collections.abc import MutableMapping from typing import Any from opentelemetry.propagate import get_global_textmap, inject from opentelemetry.propagators.textmap import CarrierT, Setter _logger = logging.getLogger(__name__) _MAX_MESSAGE_ATTRIBUTES = 10 class MessageAttributesSetter(Setter[CarrierT]): def set(self, carrier: CarrierT, key: str, value: str): carrier[key] = { "DataType": "String", "StringValue": value, } message_attributes_setter = MessageAttributesSetter() def inject_propagation_context( carrier: MutableMapping[str, Any], ) -> MutableMapping[str, Any]: if carrier is None: carrier = {} fields = get_global_textmap().fields if len(carrier.keys()) + len(fields) <= _MAX_MESSAGE_ATTRIBUTES: inject(carrier, setter=message_attributes_setter) else: _logger.warning( "botocore instrumentation: cannot set context propagation on " "SQS/SNS message due to maximum amount of MessageAttributes" ) return carrier bedrock.py000066400000000000000000001042031527070072300476240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines from __future__ import annotations import io import json import logging from timeit import default_timer from typing import Any from botocore.eventstream import EventStream from botocore.response import StreamingBody from opentelemetry.context import get_current from opentelemetry.instrumentation.botocore.extensions.bedrock_utils import ( ConverseStreamWrapper, InvokeModelWithResponseStreamWrapper, _Choice, estimate_token_count, genai_capture_message_content, message_to_event, ) from opentelemetry.instrumentation.botocore.extensions.types import ( _AttributeMapT, _AwsSdkExtension, _BotoClientErrorT, _BotocoreInstrumentorContext, ) from opentelemetry.instrumentation.botocore.utils import get_server_attributes from opentelemetry.metrics import Instrument, Meter from opentelemetry.semconv._incubating.attributes.error_attributes import ( ERROR_TYPE, ) from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import ( GEN_AI_OPERATION_NAME, GEN_AI_REQUEST_MAX_TOKENS, GEN_AI_REQUEST_MODEL, GEN_AI_REQUEST_STOP_SEQUENCES, GEN_AI_REQUEST_TEMPERATURE, GEN_AI_REQUEST_TOP_P, GEN_AI_RESPONSE_FINISH_REASONS, GEN_AI_SYSTEM, GEN_AI_TOKEN_TYPE, GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GenAiOperationNameValues, GenAiSystemValues, GenAiTokenTypeValues, ) from opentelemetry.semconv._incubating.metrics.gen_ai_metrics import ( GEN_AI_CLIENT_OPERATION_DURATION, GEN_AI_CLIENT_TOKEN_USAGE, ) from opentelemetry.trace.propagation import set_span_in_context from opentelemetry.trace.span import Span from opentelemetry.trace.status import Status, StatusCode _logger = logging.getLogger(__name__) _GEN_AI_CLIENT_OPERATION_DURATION_BUCKETS = [ 0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.56, 5.12, 10.24, 20.48, 40.96, 81.92, ] _GEN_AI_CLIENT_TOKEN_USAGE_BUCKETS = [ 1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864, ] _MODEL_ID_KEY: str = "modelId" class _BedrockRuntimeExtension(_AwsSdkExtension): """ This class is an extension for Amazon Bedrock Runtime. """ _HANDLED_OPERATIONS = { "Converse", "ConverseStream", "InvokeModel", "InvokeModelWithResponseStream", } _DONT_CLOSE_SPAN_ON_END_OPERATIONS = { "ConverseStream", "InvokeModelWithResponseStream", } def should_end_span_on_exit(self): return self._call_context.operation not in self._DONT_CLOSE_SPAN_ON_END_OPERATIONS def setup_metrics(self, meter: Meter, metrics: dict[str, Instrument]): metrics[GEN_AI_CLIENT_OPERATION_DURATION] = meter.create_histogram( name=GEN_AI_CLIENT_OPERATION_DURATION, description="GenAI operation duration", unit="s", explicit_bucket_boundaries_advisory=_GEN_AI_CLIENT_OPERATION_DURATION_BUCKETS, ) metrics[GEN_AI_CLIENT_TOKEN_USAGE] = meter.create_histogram( name=GEN_AI_CLIENT_TOKEN_USAGE, description="Measures number of input and output tokens used", unit="{token}", explicit_bucket_boundaries_advisory=_GEN_AI_CLIENT_TOKEN_USAGE_BUCKETS, ) def _extract_metrics_attributes(self) -> _AttributeMapT: attributes = { GEN_AI_SYSTEM: GenAiSystemValues.AWS_BEDROCK.value, **get_server_attributes(self._call_context.endpoint_url), } model_id = self._call_context.params.get(_MODEL_ID_KEY) if not model_id: return attributes attributes[GEN_AI_REQUEST_MODEL] = model_id # titan in invoke model is a text completion one if "body" in self._call_context.params and "amazon.titan" in model_id: attributes[GEN_AI_OPERATION_NAME] = GenAiOperationNameValues.TEXT_COMPLETION.value else: attributes[GEN_AI_OPERATION_NAME] = GenAiOperationNameValues.CHAT.value return attributes def extract_attributes(self, attributes: _AttributeMapT): if self._call_context.operation not in self._HANDLED_OPERATIONS: return attributes[GEN_AI_SYSTEM] = GenAiSystemValues.AWS_BEDROCK.value model_id = self._call_context.params.get(_MODEL_ID_KEY) if model_id: attributes[GEN_AI_REQUEST_MODEL] = model_id attributes[GEN_AI_OPERATION_NAME] = GenAiOperationNameValues.CHAT.value # Converse / ConverseStream if inference_config := self._call_context.params.get("inferenceConfig"): self._set_if_not_none( attributes, GEN_AI_REQUEST_TEMPERATURE, inference_config.get("temperature"), ) self._set_if_not_none( attributes, GEN_AI_REQUEST_TOP_P, inference_config.get("topP"), ) self._set_if_not_none( attributes, GEN_AI_REQUEST_MAX_TOKENS, inference_config.get("maxTokens"), ) self._set_if_not_none( attributes, GEN_AI_REQUEST_STOP_SEQUENCES, inference_config.get("stopSequences"), ) # InvokeModel # Get the request body if it exists body = self._call_context.params.get("body") if body: try: request_body = json.loads(body) if "amazon.titan" in model_id: # titan interface is a text completion one attributes[GEN_AI_OPERATION_NAME] = GenAiOperationNameValues.TEXT_COMPLETION.value self._extract_titan_attributes(attributes, request_body) elif "amazon.nova" in model_id: self._extract_nova_attributes(attributes, request_body) elif "anthropic.claude" in model_id: self._extract_claude_attributes(attributes, request_body) elif "cohere.command-r" in model_id: self._extract_command_r_attributes(attributes, request_body) elif "cohere.command" in model_id: self._extract_command_attributes(attributes, request_body) elif "meta.llama" in model_id: self._extract_llama_attributes(attributes, request_body) elif "mistral" in model_id: self._extract_mistral_attributes(attributes, request_body) except json.JSONDecodeError: _logger.debug("Error: Unable to parse the body as JSON") def _extract_titan_attributes(self, attributes, request_body): config = request_body.get("textGenerationConfig", {}) self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, config.get("temperature")) self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, config.get("topP")) self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, config.get("maxTokenCount")) self._set_if_not_none( attributes, GEN_AI_REQUEST_STOP_SEQUENCES, config.get("stopSequences"), ) def _extract_nova_attributes(self, attributes, request_body): config = request_body.get("inferenceConfig", {}) self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, config.get("temperature")) self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, config.get("topP")) self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, config.get("max_new_tokens")) self._set_if_not_none( attributes, GEN_AI_REQUEST_STOP_SEQUENCES, config.get("stopSequences"), ) def _extract_claude_attributes(self, attributes, request_body): self._set_if_not_none( attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_tokens"), ) self._set_if_not_none( attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature"), ) self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("top_p")) self._set_if_not_none( attributes, GEN_AI_REQUEST_STOP_SEQUENCES, request_body.get("stop_sequences"), ) def _extract_command_r_attributes(self, attributes, request_body): prompt = request_body.get("message") self._set_if_not_none(attributes, GEN_AI_USAGE_INPUT_TOKENS, estimate_token_count(prompt)) self._set_if_not_none( attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_tokens"), ) self._set_if_not_none( attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature"), ) self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("p")) self._set_if_not_none( attributes, GEN_AI_REQUEST_STOP_SEQUENCES, request_body.get("stop_sequences"), ) def _extract_command_attributes(self, attributes, request_body): prompt = request_body.get("prompt") self._set_if_not_none(attributes, GEN_AI_USAGE_INPUT_TOKENS, estimate_token_count(prompt)) self._set_if_not_none( attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_tokens"), ) self._set_if_not_none( attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature"), ) self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("p")) self._set_if_not_none( attributes, GEN_AI_REQUEST_STOP_SEQUENCES, request_body.get("stop_sequences"), ) def _extract_llama_attributes(self, attributes, request_body): self._set_if_not_none( attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_gen_len"), ) self._set_if_not_none( attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature"), ) self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("top_p")) # request for meta llama models does not contain stop_sequences field def _extract_mistral_attributes(self, attributes, request_body): prompt = request_body.get("prompt") if prompt: self._set_if_not_none( attributes, GEN_AI_USAGE_INPUT_TOKENS, estimate_token_count(prompt), ) self._set_if_not_none( attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_tokens"), ) self._set_if_not_none( attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature"), ) self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("top_p")) self._set_if_not_none(attributes, GEN_AI_REQUEST_STOP_SEQUENCES, request_body.get("stop")) @staticmethod def _set_if_not_none(attributes, key, value): if value is not None: attributes[key] = value def _get_request_messages(self): """Extracts and normalize system and user / assistant messages""" if system := self._call_context.params.get("system", []): system_messages = [{"role": "system", "content": system}] else: system_messages = [] if not (messages := self._call_context.params.get("messages", [])): if body := self._call_context.params.get("body"): decoded_body = json.loads(body) if system := decoded_body.get("system"): if isinstance(system, str): content = [{"text": system}] else: content = system system_messages = [{"role": "system", "content": content}] messages = decoded_body.get("messages", []) # if no messages interface, convert to messages format from generic API if not messages: model_id = self._call_context.params.get(_MODEL_ID_KEY) if "amazon.titan" in model_id: messages = self._get_messages_from_input_text(decoded_body, "inputText") elif "cohere.command-r" in model_id: # chat_history can be converted to messages; for now, just use message messages = self._get_messages_from_input_text(decoded_body, "message") elif "cohere.command" in model_id or "meta.llama" in model_id or "mistral.mistral" in model_id: messages = self._get_messages_from_input_text(decoded_body, "prompt") return system_messages + messages # pylint: disable=no-self-use def _get_messages_from_input_text(self, decoded_body: dict[str, Any], input_name: str): if input_text := decoded_body.get(input_name): return [{"role": "user", "content": [{"text": input_text}]}] return [] def before_service_call(self, span: Span, instrumentor_context: _BotocoreInstrumentorContext): if self._call_context.operation not in self._HANDLED_OPERATIONS: return capture_content = genai_capture_message_content() messages = self._get_request_messages() for message in messages: logger = instrumentor_context.logger for event in message_to_event(message, capture_content): logger.emit(event) if span.is_recording(): operation_name = span.attributes.get(GEN_AI_OPERATION_NAME, "") request_model = span.attributes.get(GEN_AI_REQUEST_MODEL, "") # avoid setting to an empty string if are not available if operation_name and request_model: span.update_name(f"{operation_name} {request_model}") # this is used to calculate the operation duration metric, duration may be skewed by request_hook self._operation_start = default_timer() # pylint: disable=no-self-use,too-many-locals,too-many-branches def _converse_on_success( self, span: Span, result: dict[str, Any], instrumentor_context: _BotocoreInstrumentorContext, capture_content, ): if span.is_recording(): if usage := result.get("usage"): if input_tokens := usage.get("inputTokens"): span.set_attribute( GEN_AI_USAGE_INPUT_TOKENS, input_tokens, ) if output_tokens := usage.get("outputTokens"): span.set_attribute( GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens, ) if cache_read := usage.get("cacheReadInputTokens"): span.set_attribute( GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, cache_read, ) if cache_write := usage.get("cacheWriteInputTokens"): span.set_attribute( GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, cache_write, ) if stop_reason := result.get("stopReason"): span.set_attribute( GEN_AI_RESPONSE_FINISH_REASONS, [stop_reason], ) # In case of an early stream closure, the result may not contain outputs if self._stream_has_output_content(result): logger = instrumentor_context.logger choice = _Choice.from_converse(result, capture_content) # this path is used by streaming apis, in that case we are already out of the span # context so need to add the span context manually context = set_span_in_context(span, get_current()) logger.emit(choice.to_choice_event(context=context)) metrics = instrumentor_context.metrics metrics_attributes = self._extract_metrics_attributes() if operation_duration_histogram := metrics.get(GEN_AI_CLIENT_OPERATION_DURATION): duration = max((default_timer() - self._operation_start), 0) operation_duration_histogram.record( duration, attributes=metrics_attributes, ) if token_usage_histogram := metrics.get(GEN_AI_CLIENT_TOKEN_USAGE): if usage := result.get("usage"): if input_tokens := usage.get("inputTokens"): input_attributes = { **metrics_attributes, GEN_AI_TOKEN_TYPE: GenAiTokenTypeValues.INPUT.value, } token_usage_histogram.record(input_tokens, input_attributes) if output_tokens := usage.get("outputTokens"): output_attributes = { **metrics_attributes, GEN_AI_TOKEN_TYPE: GenAiTokenTypeValues.COMPLETION.value, } token_usage_histogram.record(output_tokens, output_attributes) def _invoke_model_on_success( self, span: Span, result: dict[str, Any], model_id: str, instrumentor_context: _BotocoreInstrumentorContext, capture_content, ): original_body = None try: original_body = result["body"] body_content = original_body.read() # Replenish stream for downstream application use new_stream = io.BytesIO(body_content) result["body"] = StreamingBody(new_stream, len(body_content)) response_body = json.loads(body_content.decode("utf-8")) if "amazon.titan" in model_id: self._handle_amazon_titan_response(span, response_body, instrumentor_context, capture_content) elif "amazon.nova" in model_id: self._handle_amazon_nova_response(span, response_body, instrumentor_context, capture_content) elif "anthropic.claude" in model_id: self._handle_anthropic_claude_response(span, response_body, instrumentor_context, capture_content) elif "cohere.command-r" in model_id: self._handle_cohere_command_r_response(span, response_body, instrumentor_context, capture_content) elif "cohere.command" in model_id: self._handle_cohere_command_response(span, response_body, instrumentor_context, capture_content) elif "meta.llama" in model_id: self._handle_meta_llama_response(span, response_body, instrumentor_context, capture_content) elif "mistral" in model_id: self._handle_mistral_ai_response(span, response_body, instrumentor_context, capture_content) except json.JSONDecodeError: _logger.debug("Error: Unable to parse the response body as JSON") except Exception as exc: # pylint: disable=broad-exception-caught _logger.debug("Error processing response: %s", exc) finally: if original_body is not None: original_body.close() def _on_stream_error_callback( self, span: Span, exception, instrumentor_context: _BotocoreInstrumentorContext, span_ended: bool, ): span.set_status(Status(StatusCode.ERROR, str(exception))) if span.is_recording(): span.set_attribute(ERROR_TYPE, type(exception).__qualname__) if not span_ended: span.end() metrics = instrumentor_context.metrics metrics_attributes = { **self._extract_metrics_attributes(), ERROR_TYPE: type(exception).__qualname__, } if operation_duration_histogram := metrics.get(GEN_AI_CLIENT_OPERATION_DURATION): duration = max((default_timer() - self._operation_start), 0) operation_duration_histogram.record( duration, attributes=metrics_attributes, ) def on_success( self, span: Span, result: dict[str, Any], instrumentor_context: _BotocoreInstrumentorContext, ): if self._call_context.operation not in self._HANDLED_OPERATIONS: return capture_content = genai_capture_message_content() if self._call_context.operation == "ConverseStream": if "stream" in result and isinstance(result["stream"], EventStream): def stream_done_callback(response, span_ended): self._converse_on_success(span, response, instrumentor_context, capture_content) if not span_ended: span.end() def stream_error_callback(exception, span_ended): self._on_stream_error_callback(span, exception, instrumentor_context, span_ended) result["stream"] = ConverseStreamWrapper( result["stream"], stream_done_callback, stream_error_callback, ) return elif self._call_context.operation == "Converse": self._converse_on_success(span, result, instrumentor_context, capture_content) model_id = self._call_context.params.get(_MODEL_ID_KEY) if not model_id: return if self._call_context.operation == "InvokeModel": if "body" in result and isinstance(result["body"], StreamingBody): self._invoke_model_on_success( span, result, model_id, instrumentor_context, capture_content, ) return elif self._call_context.operation == "InvokeModelWithResponseStream": if "body" in result and isinstance(result["body"], EventStream): def invoke_model_stream_done_callback(response, span_ended): # the callback gets data formatted as the simpler converse API self._converse_on_success(span, response, instrumentor_context, capture_content) if not span_ended: span.end() def invoke_model_stream_error_callback(exception, span_ended): self._on_stream_error_callback(span, exception, instrumentor_context, span_ended) result["body"] = InvokeModelWithResponseStreamWrapper( result["body"], invoke_model_stream_done_callback, invoke_model_stream_error_callback, model_id, ) return # pylint: disable=no-self-use,too-many-locals def _handle_amazon_titan_response( self, span: Span, response_body: dict[str, Any], instrumentor_context: _BotocoreInstrumentorContext, capture_content: bool, ): if "inputTextTokenCount" in response_body: span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, response_body["inputTextTokenCount"]) if response_body.get("results"): result = response_body["results"][0] if "tokenCount" in result: span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, result["tokenCount"]) if "completionReason" in result: span.set_attribute( GEN_AI_RESPONSE_FINISH_REASONS, [result["completionReason"]], ) logger = instrumentor_context.logger choice = _Choice.from_invoke_amazon_titan(response_body, capture_content) logger.emit(choice.to_choice_event()) metrics = instrumentor_context.metrics metrics_attributes = self._extract_metrics_attributes() if operation_duration_histogram := metrics.get(GEN_AI_CLIENT_OPERATION_DURATION): duration = max((default_timer() - self._operation_start), 0) operation_duration_histogram.record( duration, attributes=metrics_attributes, ) if token_usage_histogram := metrics.get(GEN_AI_CLIENT_TOKEN_USAGE): if input_tokens := response_body.get("inputTextTokenCount"): input_attributes = { **metrics_attributes, GEN_AI_TOKEN_TYPE: GenAiTokenTypeValues.INPUT.value, } token_usage_histogram.record(input_tokens, input_attributes) if results := response_body.get("results"): if output_tokens := results[0].get("tokenCount"): output_attributes = { **metrics_attributes, GEN_AI_TOKEN_TYPE: GenAiTokenTypeValues.COMPLETION.value, } token_usage_histogram.record(output_tokens, output_attributes) # pylint: disable=no-self-use,too-many-locals def _handle_amazon_nova_response( self, span: Span, response_body: dict[str, Any], instrumentor_context: _BotocoreInstrumentorContext, capture_content: bool, ): if "usage" in response_body: usage = response_body["usage"] if "inputTokens" in usage: span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, usage["inputTokens"]) if "outputTokens" in usage: span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, usage["outputTokens"]) if "stopReason" in response_body: span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stopReason"]]) # In case of an early stream closure, the result may not contain outputs if self._stream_has_output_content(response_body): logger = instrumentor_context.logger choice = _Choice.from_converse(response_body, capture_content) logger.emit(choice.to_choice_event()) metrics = instrumentor_context.metrics metrics_attributes = self._extract_metrics_attributes() if operation_duration_histogram := metrics.get(GEN_AI_CLIENT_OPERATION_DURATION): duration = max((default_timer() - self._operation_start), 0) operation_duration_histogram.record( duration, attributes=metrics_attributes, ) if token_usage_histogram := metrics.get(GEN_AI_CLIENT_TOKEN_USAGE): if usage := response_body.get("usage"): if input_tokens := usage.get("inputTokens"): input_attributes = { **metrics_attributes, GEN_AI_TOKEN_TYPE: GenAiTokenTypeValues.INPUT.value, } token_usage_histogram.record(input_tokens, input_attributes) if output_tokens := usage.get("outputTokens"): output_attributes = { **metrics_attributes, GEN_AI_TOKEN_TYPE: GenAiTokenTypeValues.COMPLETION.value, } token_usage_histogram.record(output_tokens, output_attributes) # pylint: disable=no-self-use def _handle_anthropic_claude_response( self, span: Span, response_body: dict[str, Any], instrumentor_context: _BotocoreInstrumentorContext, capture_content: bool, ): if usage := response_body.get("usage"): if "input_tokens" in usage: span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, usage["input_tokens"]) if "output_tokens" in usage: span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, usage["output_tokens"]) if "stop_reason" in response_body: span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stop_reason"]]) logger = instrumentor_context.logger choice = _Choice.from_invoke_anthropic_claude(response_body, capture_content) logger.emit(choice.to_choice_event()) metrics = instrumentor_context.metrics metrics_attributes = self._extract_metrics_attributes() if operation_duration_histogram := metrics.get(GEN_AI_CLIENT_OPERATION_DURATION): duration = max((default_timer() - self._operation_start), 0) operation_duration_histogram.record( duration, attributes=metrics_attributes, ) if token_usage_histogram := metrics.get(GEN_AI_CLIENT_TOKEN_USAGE): if usage := response_body.get("usage"): if input_tokens := usage.get("input_tokens"): input_attributes = { **metrics_attributes, GEN_AI_TOKEN_TYPE: GenAiTokenTypeValues.INPUT.value, } token_usage_histogram.record(input_tokens, input_attributes) if output_tokens := usage.get("output_tokens"): output_attributes = { **metrics_attributes, GEN_AI_TOKEN_TYPE: GenAiTokenTypeValues.COMPLETION.value, } token_usage_histogram.record(output_tokens, output_attributes) def _handle_cohere_command_r_response( self, span: Span, response_body: dict[str, Any], instrumentor_context: _BotocoreInstrumentorContext, capture_content: bool, ): if "text" in response_body: span.set_attribute( GEN_AI_USAGE_OUTPUT_TOKENS, estimate_token_count(response_body["text"]), ) if "finish_reason" in response_body: span.set_attribute( GEN_AI_RESPONSE_FINISH_REASONS, [response_body["finish_reason"]], ) logger = instrumentor_context.logger choice = _Choice.from_invoke_cohere_command_r(response_body, capture_content) logger.emit(choice.to_choice_event()) def _handle_cohere_command_response( self, span: Span, response_body: dict[str, Any], instrumentor_context: _BotocoreInstrumentorContext, capture_content: bool, ): if response_body.get("generations"): generations = response_body["generations"][0] if "text" in generations: span.set_attribute( GEN_AI_USAGE_OUTPUT_TOKENS, estimate_token_count(generations["text"]), ) if "finish_reason" in generations: span.set_attribute( GEN_AI_RESPONSE_FINISH_REASONS, [generations["finish_reason"]], ) logger = instrumentor_context.logger choice = _Choice.from_invoke_cohere_command(response_body, capture_content) logger.emit(choice.to_choice_event()) def _handle_meta_llama_response( self, span: Span, response_body: dict[str, Any], instrumentor_context: _BotocoreInstrumentorContext, capture_content: bool, ): if "prompt_token_count" in response_body: span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, response_body["prompt_token_count"]) if "generation_token_count" in response_body: span.set_attribute( GEN_AI_USAGE_OUTPUT_TOKENS, response_body["generation_token_count"], ) if "stop_reason" in response_body: span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stop_reason"]]) logger = instrumentor_context.logger choice = _Choice.from_invoke_meta_llama(response_body, capture_content) logger.emit(choice.to_choice_event()) def _handle_mistral_ai_response( self, span: Span, response_body: dict[str, Any], instrumentor_context: _BotocoreInstrumentorContext, capture_content: bool, ): if "outputs" in response_body: outputs = response_body["outputs"][0] if "text" in outputs: span.set_attribute( GEN_AI_USAGE_OUTPUT_TOKENS, estimate_token_count(outputs["text"]), ) if "stop_reason" in outputs: span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [outputs["stop_reason"]]) logger = instrumentor_context.logger choice = _Choice.from_invoke_mistral_mistral(response_body, capture_content) logger.emit(choice.to_choice_event()) def on_error( self, span: Span, exception: _BotoClientErrorT, instrumentor_context: _BotocoreInstrumentorContext, ): if self._call_context.operation not in self._HANDLED_OPERATIONS: return span.set_status(Status(StatusCode.ERROR, str(exception))) if span.is_recording(): span.set_attribute(ERROR_TYPE, type(exception).__qualname__) if not self.should_end_span_on_exit(): span.end() metrics = instrumentor_context.metrics metrics_attributes = { **self._extract_metrics_attributes(), ERROR_TYPE: type(exception).__qualname__, } if operation_duration_histogram := metrics.get(GEN_AI_CLIENT_OPERATION_DURATION): duration = max((default_timer() - self._operation_start), 0) operation_duration_histogram.record( duration, attributes=metrics_attributes, ) def _stream_has_output_content(self, response_body: dict[str, Any]): return "output" in response_body and "message" in response_body["output"] bedrock_utils.py000066400000000000000000000555211527070072300510540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import json import math from collections.abc import Callable, Iterator, Sequence from os import environ from typing import Any from botocore.eventstream import EventStream, EventStreamError try: # wrapt 2.0.0+ from wrapt import BaseObjectProxy # pylint: disable=no-name-in-module except ImportError: from wrapt import ObjectProxy as BaseObjectProxy from opentelemetry._logs import LogRecord from opentelemetry.context import get_current from opentelemetry.instrumentation.botocore.environment_variables import ( OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, ) from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import ( GEN_AI_SYSTEM, GenAiSystemValues, ) _StreamDoneCallableT = Callable[[dict[str, int | str]], None] _StreamErrorCallableT = Callable[[Exception], None] # pylint: disable=abstract-method class ConverseStreamWrapper(BaseObjectProxy): """Wrapper for botocore.eventstream.EventStream""" def __init__( self, stream: EventStream, stream_done_callback: _StreamDoneCallableT, stream_error_callback: _StreamErrorCallableT, ): super().__init__(stream) self._stream_done_callback = stream_done_callback self._stream_error_callback = stream_error_callback # accumulating things in the same shape of non-streaming version # {"usage": {"inputTokens": 0, "outputTokens": 0}, "stopReason": "finish", "output": {"message": {"role": "", "content": [{"text": ""}]} self._response = {} self._message = None self._content_block = {} self._tool_json_input_buf = "" self._record_message = False self._ended = False def __iter__(self): try: for event in self.__wrapped__: # pylint: disable=no-member self._process_event(event) yield event except EventStreamError as exc: self._handle_stream_error(exc) raise def _process_event(self, event): # pylint: disable=too-many-branches if "messageStart" in event: # {'messageStart': {'role': 'assistant'}} if event["messageStart"].get("role") == "assistant": self._record_message = True self._message = {"role": "assistant", "content": []} return if "contentBlockStart" in event: # {'contentBlockStart': {'start': {'toolUse': {'toolUseId': 'id', 'name': 'func_name'}}, 'contentBlockIndex': 1}} start = event["contentBlockStart"].get("start", {}) if "toolUse" in start: self._content_block = {"toolUse": start["toolUse"]} return if "contentBlockDelta" in event: # {'contentBlockDelta': {'delta': {'text': "Hello"}, 'contentBlockIndex': 0}} # {'contentBlockDelta': {'delta': {'toolUse': {'input': '{"location":"Seattle"}'}}, 'contentBlockIndex': 1}} # {'contentBlockDelta': {'delta': {'toolUse': {'input': 'a", "Yok'}}, 'contentBlockIndex': 1}} if self._record_message: delta = event["contentBlockDelta"].get("delta", {}) if "text" in delta: self._content_block.setdefault("text", "") self._content_block["text"] += delta["text"] elif "toolUse" in delta: if (input_buf := delta["toolUse"].get("input")) is not None: self._tool_json_input_buf += input_buf return if "contentBlockStop" in event: # {'contentBlockStop': {'contentBlockIndex': 0}} if self._record_message: if self._tool_json_input_buf: try: self._content_block["toolUse"]["input"] = json.loads(self._tool_json_input_buf) except json.JSONDecodeError: self._content_block["toolUse"]["input"] = self._tool_json_input_buf self._message["content"].append(self._content_block) self._content_block = {} self._tool_json_input_buf = "" return if "messageStop" in event: # {'messageStop': {'stopReason': 'end_turn'}} if stop_reason := event["messageStop"].get("stopReason"): self._response["stopReason"] = stop_reason if self._record_message: self._response["output"] = {"message": self._message} self._record_message = False self._message = None return if "metadata" in event: # {'metadata': {'usage': {'inputTokens': 12, 'outputTokens': 15, 'totalTokens': 27}, 'metrics': {'latencyMs': 2980}}} if usage := event["metadata"].get("usage"): self._response["usage"] = {} if input_tokens := usage.get("inputTokens"): self._response["usage"]["inputTokens"] = input_tokens if output_tokens := usage.get("outputTokens"): self._response["usage"]["outputTokens"] = output_tokens if cache_read := usage.get("cacheReadInputTokens"): self._response["usage"]["cacheReadInputTokens"] = cache_read if cache_write := usage.get("cacheWriteInputTokens"): self._response["usage"]["cacheWriteInputTokens"] = cache_write self._complete_stream(self._response) return def close(self): self.__wrapped__.close() # pylint: disable=no-member # Treat the stream as done to ensure the span end. self._complete_stream(self._response) def _complete_stream(self, response): self._stream_done_callback(response, self._ended) self._ended = True def _handle_stream_error(self, exc): self._stream_error_callback(exc, self._ended) self._ended = True # pylint: disable=abstract-method class InvokeModelWithResponseStreamWrapper(BaseObjectProxy): """Wrapper for botocore.eventstream.EventStream""" def __init__( self, stream: EventStream, stream_done_callback: _StreamDoneCallableT, stream_error_callback: _StreamErrorCallableT, model_id: str, ): super().__init__(stream) self._stream_done_callback = stream_done_callback self._stream_error_callback = stream_error_callback self._model_id = model_id # accumulating things in the same shape of the Converse API # {"usage": {"inputTokens": 0, "outputTokens": 0}, "stopReason": "finish", "output": {"message": {"role": "", "content": [{"text": ""}]} self._response = {} self._message = None self._content_block = {} self._tool_json_input_buf = "" self._record_message = False self._ended = False def close(self): self.__wrapped__.close() # pylint: disable=no-member # Treat the stream as done to ensure the span end. self._stream_done_callback(self._response, self._ended) def _complete_stream(self, response): self._stream_done_callback(response, self._ended) self._ended = True def _handle_stream_error(self, exc): self._stream_error_callback(exc, self._ended) self._ended = True def __iter__(self): try: for event in self.__wrapped__: # pylint: disable=no-member self._process_event(event) yield event except EventStreamError as exc: self._handle_stream_error(exc) raise def _process_event(self, event): if "chunk" not in event: return json_bytes = event["chunk"].get("bytes", b"") decoded = json_bytes.decode("utf-8") try: chunk = json.loads(decoded) except json.JSONDecodeError: return if "amazon.titan" in self._model_id: self._process_amazon_titan_chunk(chunk) elif "amazon.nova" in self._model_id: self._process_amazon_nova_chunk(chunk) elif "anthropic.claude" in self._model_id: self._process_anthropic_claude_chunk(chunk) def _process_invocation_metrics(self, invocation_metrics): self._response["usage"] = {} if input_tokens := invocation_metrics.get("inputTokenCount"): self._response["usage"]["inputTokens"] = input_tokens if output_tokens := invocation_metrics.get("outputTokenCount"): self._response["usage"]["outputTokens"] = output_tokens def _process_amazon_titan_chunk(self, chunk): if (stop_reason := chunk.get("completionReason")) is not None: self._response["stopReason"] = stop_reason if invocation_metrics := chunk.get("amazon-bedrock-invocationMetrics"): # "amazon-bedrock-invocationMetrics":{ # "inputTokenCount":9,"outputTokenCount":128,"invocationLatency":3569,"firstByteLatency":2180 # } self._process_invocation_metrics(invocation_metrics) # transform the shape of the message to match other models self._response["output"] = {"message": {"content": [{"text": chunk["outputText"]}]}} self._complete_stream(self._response) def _process_amazon_nova_chunk(self, chunk): # pylint: disable=too-many-branches if "messageStart" in chunk: # {'messageStart': {'role': 'assistant'}} if chunk["messageStart"].get("role") == "assistant": self._record_message = True self._message = {"role": "assistant", "content": []} return if "contentBlockStart" in chunk: # {'contentBlockStart': {'start': {'toolUse': {'toolUseId': 'id', 'name': 'name'}}, 'contentBlockIndex': 31}} if self._record_message: self._message["content"].append(self._content_block) start = chunk["contentBlockStart"].get("start", {}) if "toolUse" in start: self._content_block = start else: self._content_block = {} return if "contentBlockDelta" in chunk: # {'contentBlockDelta': {'delta': {'text': "Hello"}, 'contentBlockIndex': 0}} # {'contentBlockDelta': {'delta': {'toolUse': {'input': '{"location":"San Francisco"}'}}, 'contentBlockIndex': 31}} if self._record_message: delta = chunk["contentBlockDelta"].get("delta", {}) if "text" in delta: self._content_block.setdefault("text", "") self._content_block["text"] += delta["text"] elif "toolUse" in delta: self._content_block.setdefault("toolUse", {}) self._content_block["toolUse"]["input"] = json.loads(delta["toolUse"]["input"]) return if "contentBlockStop" in chunk: # {'contentBlockStop': {'contentBlockIndex': 0}} if self._record_message: # create a new content block only for tools if "toolUse" in self._content_block: self._message["content"].append(self._content_block) self._content_block = {} return if "messageStop" in chunk: # {'messageStop': {'stopReason': 'end_turn'}} if stop_reason := chunk["messageStop"].get("stopReason"): self._response["stopReason"] = stop_reason if self._record_message: self._message["content"].append(self._content_block) self._content_block = {} self._response["output"] = {"message": self._message} self._record_message = False self._message = None return if "metadata" in chunk: # {'metadata': {'usage': {'inputTokens': 8, 'outputTokens': 117}, 'metrics': {}, 'trace': {}}} if usage := chunk["metadata"].get("usage"): self._response["usage"] = {} if input_tokens := usage.get("inputTokens"): self._response["usage"]["inputTokens"] = input_tokens if output_tokens := usage.get("outputTokens"): self._response["usage"]["outputTokens"] = output_tokens self._complete_stream(self._response) return def _process_anthropic_claude_chunk(self, chunk): # pylint: disable=too-many-return-statements,too-many-branches if not (message_type := chunk.get("type")): return if message_type == "message_start": # {'type': 'message_start', 'message': {'id': 'id', 'type': 'message', 'role': 'assistant', 'model': 'claude-2.0', 'content': [], 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': 18, 'output_tokens': 1}}} if chunk.get("message", {}).get("role") == "assistant": self._record_message = True message = chunk["message"] self._message = { "role": message["role"], "content": message.get("content", []), } return if message_type == "content_block_start": # {'type': 'content_block_start', 'index': 0, 'content_block': {'type': 'text', 'text': ''}} # {'type': 'content_block_start', 'index': 1, 'content_block': {'type': 'tool_use', 'id': 'id', 'name': 'func_name', 'input': {}}} if self._record_message: block = chunk.get("content_block", {}) if block.get("type") == "text" or block.get("type") == "tool_use": self._content_block = block return if message_type == "content_block_delta": # {'type': 'content_block_delta', 'index': 0, 'delta': {'type': 'text_delta', 'text': 'Here'}} # {'type': 'content_block_delta', 'index': 1, 'delta': {'type': 'input_json_delta', 'partial_json': ''}} if self._record_message: delta = chunk.get("delta", {}) if delta.get("type") == "text_delta": self._content_block["text"] += delta.get("text", "") elif delta.get("type") == "input_json_delta": self._tool_json_input_buf += delta.get("partial_json", "") return if message_type == "content_block_stop": # {'type': 'content_block_stop', 'index': 0} if self._tool_json_input_buf: try: self._content_block["input"] = json.loads(self._tool_json_input_buf) except json.JSONDecodeError: self._content_block["input"] = self._tool_json_input_buf self._message["content"].append(self._content_block) self._content_block = {} self._tool_json_input_buf = "" return if message_type == "message_delta": # {'type': 'message_delta', 'delta': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'usage': {'output_tokens': 123}} if (stop_reason := chunk.get("delta", {}).get("stop_reason")) is not None: self._response["stopReason"] = stop_reason return if message_type == "message_stop": # {'type': 'message_stop', 'amazon-bedrock-invocationMetrics': {'inputTokenCount': 18, 'outputTokenCount': 123, 'invocationLatency': 5250, 'firstByteLatency': 290}} if invocation_metrics := chunk.get("amazon-bedrock-invocationMetrics"): self._process_invocation_metrics(invocation_metrics) if self._record_message: self._response["output"] = {"message": self._message} self._record_message = False self._message = None self._complete_stream(self._response) return def estimate_token_count(message: str) -> int: # https://docs.aws.amazon.com/bedrock/latest/userguide/model-customization-prepare.html # use 6 chars per token to approximate token count when not provided in response body return math.ceil(len(message) / 6) def genai_capture_message_content() -> bool: capture_content = environ.get(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "false") return capture_content.lower() == "true" def extract_tool_calls(message: dict[str, Any], capture_content: bool) -> Sequence[dict[str, Any]] | None: content = message.get("content") if not content: return None tool_uses = [item["toolUse"] for item in content if "toolUse" in item] if not tool_uses: tool_uses = [item for item in content if isinstance(item, dict) and item.get("type") == "tool_use"] tool_id_key = "id" else: tool_id_key = "toolUseId" if not tool_uses: return None tool_calls = [] for tool_use in tool_uses: tool_call = {"type": "function"} if call_id := tool_use.get(tool_id_key): tool_call["id"] = call_id if function_name := tool_use.get("name"): tool_call["function"] = {"name": function_name} if (function_input := tool_use.get("input")) and capture_content: tool_call.setdefault("function", {}) tool_call["function"]["arguments"] = function_input tool_calls.append(tool_call) return tool_calls def extract_tool_results(message: dict[str, Any], capture_content: bool) -> Iterator[dict[str, Any]]: content = message.get("content") if not content: return # langchain sends content as string with InvokeModel and Anthropic Claude if isinstance(content, str): return # Converse format tool_results = [item["toolResult"] for item in content if "toolResult" in item] # InvokeModel anthropic.claude format if not tool_results: tool_results = [item for item in content if item.get("type") == "tool_result"] tool_id_key = "tool_use_id" else: tool_id_key = "toolUseId" if not tool_results: return # if we have a user message with toolResult keys we need to send # one tool event for each part of the content for tool_result in tool_results: body = {} if tool_id := tool_result.get(tool_id_key): body["id"] = tool_id tool_content = tool_result.get("content") if capture_content and tool_content: body["content"] = tool_content yield body def message_to_event(message: dict[str, Any], capture_content: bool) -> Iterator[LogRecord]: attributes = {GEN_AI_SYSTEM: GenAiSystemValues.AWS_BEDROCK.value} role = message.get("role") content = message.get("content") body = {} if capture_content and content: body["content"] = content if role == "assistant": # the assistant message contains both tool calls and model thinking content if tool_calls := extract_tool_calls(message, capture_content): body["tool_calls"] = tool_calls elif role == "user": # in case of tool calls we send one tool event for tool call and one for the user event for tool_body in extract_tool_results(message, capture_content): yield LogRecord( event_name="gen_ai.tool.message", attributes=attributes, body=tool_body, context=get_current(), ) yield LogRecord( event_name=f"gen_ai.{role}.message", attributes=attributes, body=body if body else None, context=get_current(), ) class _Choice: def __init__(self, message: dict[str, Any], finish_reason: str, index: int): self.message = message self.finish_reason = finish_reason self.index = index @classmethod def from_converse(cls, response: dict[str, Any], capture_content: bool) -> _Choice: # be defensive about malformed responses, refer to #3958 for more context output = response.get("output", {}) orig_message = output.get("message", {}) if role := orig_message.get("role"): message = {"role": role} else: # amazon.titan does not serialize the role message = {} if tool_calls := extract_tool_calls(orig_message, capture_content): message["tool_calls"] = tool_calls elif capture_content and (content := orig_message.get("content")): message["content"] = content return cls(message, response["stopReason"], index=0) @classmethod def from_invoke_amazon_titan(cls, response: dict[str, Any], capture_content: bool) -> _Choice: result = response["results"][0] if capture_content: message = {"content": result["outputText"]} else: message = {} return cls(message, result["completionReason"], index=0) @classmethod def from_invoke_anthropic_claude(cls, response: dict[str, Any], capture_content: bool) -> _Choice: message = {"role": response["role"]} if tool_calls := extract_tool_calls(response, capture_content): message["tool_calls"] = tool_calls elif capture_content: message["content"] = response["content"] return cls(message, response["stop_reason"], index=0) @classmethod def from_invoke_cohere_command_r(cls, response: dict[str, Any], capture_content: bool) -> _Choice: if capture_content: message = {"content": response["text"]} else: message = {} return cls(message, response["finish_reason"], index=0) @classmethod def from_invoke_cohere_command(cls, response: dict[str, Any], capture_content: bool) -> _Choice: result = response["generations"][0] if capture_content: message = {"content": result["text"]} else: message = {} return cls(message, result["finish_reason"], index=0) @classmethod def from_invoke_meta_llama(cls, response: dict[str, Any], capture_content: bool) -> _Choice: if capture_content: message = {"content": response["generation"]} else: message = {} return cls(message, response["stop_reason"], index=0) @classmethod def from_invoke_mistral_mistral(cls, response: dict[str, Any], capture_content: bool) -> _Choice: result = response["outputs"][0] if capture_content: message = {"content": result["text"]} else: message = {} return cls(message, result["stop_reason"], index=0) def _to_body_dict(self) -> dict[str, Any]: return { "finish_reason": self.finish_reason, "index": self.index, "message": self.message, } def to_choice_event(self, **event_kwargs) -> LogRecord: attributes = {GEN_AI_SYSTEM: GenAiSystemValues.AWS_BEDROCK.value} return LogRecord( event_name="gen_ai.choice", attributes=attributes, body=self._to_body_dict(), **event_kwargs, ) dynamodb.py000066400000000000000000000317621527070072300500210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import abc import inspect import json from collections.abc import Callable from typing import Any from urllib.parse import urlparse from opentelemetry.instrumentation.botocore.extensions.types import ( _AttributeMapT, _AwsSdkCallContext, _AwsSdkExtension, _BotocoreInstrumentorContext, _BotoResultT, ) from opentelemetry.semconv._incubating.attributes import ( aws_attributes, db_attributes, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, ) from opentelemetry.trace.span import Span from opentelemetry.util.types import AttributeValue # pylint: disable=invalid-name _AttributePathT = str | tuple[str] # converter functions def _conv_val_to_single_attr_tuple(value: str) -> tuple[str]: return None if value is None else (value,) def _conv_dict_to_key_tuple(value: dict[str, Any]) -> tuple[str] | None: return tuple(value.keys()) if isinstance(value, dict) else None def _conv_list_to_json_list(value: list) -> list[str] | None: return [json.dumps(item) for item in value] if isinstance(value, list) else None def _conv_val_to_single_json_tuple(value: str) -> tuple[str] | None: return (json.dumps(value),) if value is not None else None def _conv_dict_to_json_str(value: dict) -> str | None: return json.dumps(value) if isinstance(value, dict) else None def _conv_val_to_len(value) -> int | None: return len(value) if value is not None else None ################################################################################ # common request attributes ################################################################################ _REQ_TABLE_NAME = ("TableName", _conv_val_to_single_attr_tuple) _REQ_REQITEMS_TABLE_NAMES = ("RequestItems", _conv_dict_to_key_tuple) _REQ_GLOBAL_SEC_INDEXES = ("GlobalSecondaryIndexes", _conv_list_to_json_list) _REQ_LOCAL_SEC_INDEXES = ("LocalSecondaryIndexes", _conv_list_to_json_list) _REQ_PROV_READ_CAP = (("ProvisionedThroughput", "ReadCapacityUnits"), None) _REQ_PROV_WRITE_CAP = (("ProvisionedThroughput", "WriteCapacityUnits"), None) _REQ_CONSISTENT_READ = ("ConsistentRead", None) _REQ_PROJECTION = ("ProjectionExpression", None) _REQ_ATTRS_TO_GET = ("AttributesToGet", None) _REQ_LIMIT = ("Limit", None) _REQ_SELECT = ("Select", None) _REQ_INDEX_NAME = ("IndexName", None) ################################################################################ # common response attributes ################################################################################ _RES_CONSUMED_CAP = ("ConsumedCapacity", _conv_list_to_json_list) _RES_CONSUMED_CAP_SINGLE = ("ConsumedCapacity", _conv_val_to_single_json_tuple) _RES_ITEM_COL_METRICS = ("ItemCollectionMetrics", _conv_dict_to_json_str) ################################################################################ # DynamoDB operations with enhanced attributes ################################################################################ _AttrSpecT = tuple[_AttributePathT, Callable | None] class _DynamoDbOperation(abc.ABC): start_attributes = None # type: Optional[Dict[str, _AttrSpecT]] request_attributes = None # type: Optional[Dict[str, _AttrSpecT]] response_attributes = None # type: Optional[Dict[str, _AttrSpecT]] @classmethod @abc.abstractmethod def operation_name(cls): pass @classmethod def add_start_attributes(cls, call_context: _AwsSdkCallContext, attributes: _AttributeMapT): pass @classmethod def add_response_attributes(cls, call_context: _AwsSdkCallContext, span: Span, result: _BotoResultT): pass class _OpBatchGetItem(_DynamoDbOperation): start_attributes = { aws_attributes.AWS_DYNAMODB_TABLE_NAMES: _REQ_REQITEMS_TABLE_NAMES, } response_attributes = { aws_attributes.AWS_DYNAMODB_CONSUMED_CAPACITY: _RES_CONSUMED_CAP, } @classmethod def operation_name(cls): return "BatchGetItem" class _OpBatchWriteItem(_DynamoDbOperation): start_attributes = { aws_attributes.AWS_DYNAMODB_TABLE_NAMES: _REQ_REQITEMS_TABLE_NAMES, } response_attributes = { aws_attributes.AWS_DYNAMODB_CONSUMED_CAPACITY: _RES_CONSUMED_CAP, aws_attributes.AWS_DYNAMODB_ITEM_COLLECTION_METRICS: _RES_ITEM_COL_METRICS, } @classmethod def operation_name(cls): return "BatchWriteItem" class _OpCreateTable(_DynamoDbOperation): start_attributes = { aws_attributes.AWS_DYNAMODB_TABLE_NAMES: _REQ_TABLE_NAME, } request_attributes = { aws_attributes.AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES: _REQ_GLOBAL_SEC_INDEXES, aws_attributes.AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES: _REQ_LOCAL_SEC_INDEXES, aws_attributes.AWS_DYNAMODB_PROVISIONED_READ_CAPACITY: _REQ_PROV_READ_CAP, aws_attributes.AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY: _REQ_PROV_WRITE_CAP, } @classmethod def operation_name(cls): return "CreateTable" class _OpDeleteItem(_DynamoDbOperation): start_attributes = { aws_attributes.AWS_DYNAMODB_TABLE_NAMES: _REQ_TABLE_NAME, } response_attributes = { aws_attributes.AWS_DYNAMODB_CONSUMED_CAPACITY: _RES_CONSUMED_CAP_SINGLE, aws_attributes.AWS_DYNAMODB_ITEM_COLLECTION_METRICS: _RES_ITEM_COL_METRICS, } @classmethod def operation_name(cls): return "DeleteItem" class _OpDeleteTable(_DynamoDbOperation): start_attributes = { aws_attributes.AWS_DYNAMODB_TABLE_NAMES: _REQ_TABLE_NAME, } @classmethod def operation_name(cls): return "DeleteTable" class _OpDescribeTable(_DynamoDbOperation): start_attributes = { aws_attributes.AWS_DYNAMODB_TABLE_NAMES: _REQ_TABLE_NAME, } @classmethod def operation_name(cls): return "DescribeTable" class _OpGetItem(_DynamoDbOperation): start_attributes = { aws_attributes.AWS_DYNAMODB_TABLE_NAMES: _REQ_TABLE_NAME, } request_attributes = { aws_attributes.AWS_DYNAMODB_CONSISTENT_READ: _REQ_CONSISTENT_READ, aws_attributes.AWS_DYNAMODB_PROJECTION: _REQ_PROJECTION, } response_attributes = { aws_attributes.AWS_DYNAMODB_CONSUMED_CAPACITY: _RES_CONSUMED_CAP_SINGLE, } @classmethod def operation_name(cls): return "GetItem" class _OpListTables(_DynamoDbOperation): request_attributes = { aws_attributes.AWS_DYNAMODB_EXCLUSIVE_START_TABLE: ( "ExclusiveStartTableName", None, ), aws_attributes.AWS_DYNAMODB_LIMIT: _REQ_LIMIT, } response_attributes = { aws_attributes.AWS_DYNAMODB_TABLE_COUNT: ( "TableNames", _conv_val_to_len, ), } @classmethod def operation_name(cls): return "ListTables" class _OpPutItem(_DynamoDbOperation): start_attributes = {aws_attributes.AWS_DYNAMODB_TABLE_NAMES: _REQ_TABLE_NAME} response_attributes = { aws_attributes.AWS_DYNAMODB_CONSUMED_CAPACITY: _RES_CONSUMED_CAP_SINGLE, aws_attributes.AWS_DYNAMODB_ITEM_COLLECTION_METRICS: _RES_ITEM_COL_METRICS, } @classmethod def operation_name(cls): return "PutItem" class _OpQuery(_DynamoDbOperation): start_attributes = { aws_attributes.AWS_DYNAMODB_TABLE_NAMES: _REQ_TABLE_NAME, } request_attributes = { aws_attributes.AWS_DYNAMODB_SCAN_FORWARD: ("ScanIndexForward", None), aws_attributes.AWS_DYNAMODB_ATTRIBUTES_TO_GET: _REQ_ATTRS_TO_GET, aws_attributes.AWS_DYNAMODB_CONSISTENT_READ: _REQ_CONSISTENT_READ, aws_attributes.AWS_DYNAMODB_INDEX_NAME: _REQ_INDEX_NAME, aws_attributes.AWS_DYNAMODB_LIMIT: _REQ_LIMIT, aws_attributes.AWS_DYNAMODB_PROJECTION: _REQ_PROJECTION, aws_attributes.AWS_DYNAMODB_SELECT: _REQ_SELECT, } response_attributes = { aws_attributes.AWS_DYNAMODB_CONSUMED_CAPACITY: _RES_CONSUMED_CAP_SINGLE, } @classmethod def operation_name(cls): return "Query" class _OpScan(_DynamoDbOperation): start_attributes = { aws_attributes.AWS_DYNAMODB_TABLE_NAMES: _REQ_TABLE_NAME, } request_attributes = { aws_attributes.AWS_DYNAMODB_SEGMENT: ("Segment", None), aws_attributes.AWS_DYNAMODB_TOTAL_SEGMENTS: ("TotalSegments", None), aws_attributes.AWS_DYNAMODB_ATTRIBUTES_TO_GET: _REQ_ATTRS_TO_GET, aws_attributes.AWS_DYNAMODB_CONSISTENT_READ: _REQ_CONSISTENT_READ, aws_attributes.AWS_DYNAMODB_INDEX_NAME: _REQ_INDEX_NAME, aws_attributes.AWS_DYNAMODB_LIMIT: _REQ_LIMIT, aws_attributes.AWS_DYNAMODB_PROJECTION: _REQ_PROJECTION, aws_attributes.AWS_DYNAMODB_SELECT: _REQ_SELECT, } response_attributes = { aws_attributes.AWS_DYNAMODB_COUNT: ("Count", None), aws_attributes.AWS_DYNAMODB_SCANNED_COUNT: ("ScannedCount", None), aws_attributes.AWS_DYNAMODB_CONSUMED_CAPACITY: _RES_CONSUMED_CAP_SINGLE, } @classmethod def operation_name(cls): return "Scan" class _OpUpdateItem(_DynamoDbOperation): start_attributes = { aws_attributes.AWS_DYNAMODB_TABLE_NAMES: _REQ_TABLE_NAME, } response_attributes = { aws_attributes.AWS_DYNAMODB_CONSUMED_CAPACITY: _RES_CONSUMED_CAP_SINGLE, aws_attributes.AWS_DYNAMODB_ITEM_COLLECTION_METRICS: _RES_ITEM_COL_METRICS, } @classmethod def operation_name(cls): return "UpdateItem" class _OpUpdateTable(_DynamoDbOperation): start_attributes = { aws_attributes.AWS_DYNAMODB_TABLE_NAMES: _REQ_TABLE_NAME, } request_attributes = { aws_attributes.AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS: ( "AttributeDefinitions", _conv_list_to_json_list, ), aws_attributes.AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES: ( "GlobalSecondaryIndexUpdates", _conv_list_to_json_list, ), aws_attributes.AWS_DYNAMODB_PROVISIONED_READ_CAPACITY: _REQ_PROV_READ_CAP, aws_attributes.AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY: _REQ_PROV_WRITE_CAP, } @classmethod def operation_name(cls): return "UpdateTable" ################################################################################ # DynamoDB extension ################################################################################ def _is_operation(op: object) -> bool: try: return inspect.isclass(op) and issubclass(op, _DynamoDbOperation) and not inspect.isabstract(op) except TypeError: return False _OPERATION_MAPPING = {op.operation_name(): op for op in globals().values() if _is_operation(op)} # type: Dict[str, _DynamoDbOperation] class _DynamoDbExtension(_AwsSdkExtension): def __init__(self, call_context: _AwsSdkCallContext): super().__init__(call_context) self._op = _OPERATION_MAPPING.get(call_context.operation) def extract_attributes(self, attributes: _AttributeMapT): attributes[db_attributes.DB_SYSTEM] = db_attributes.DbSystemValues.DYNAMODB.value attributes[db_attributes.DB_OPERATION] = self._call_context.operation attributes[NET_PEER_NAME] = self._get_peer_name() if self._op is None: return def attr_setter(key: str, value: AttributeValue): attributes[key] = value self._add_attributes(self._call_context.params, self._op.start_attributes, attr_setter) def _get_peer_name(self) -> str: return urlparse(self._call_context.endpoint_url).netloc def before_service_call(self, span: Span, instrumentor_context: _BotocoreInstrumentorContext): if not span.is_recording() or self._op is None: return self._add_attributes( self._call_context.params, self._op.request_attributes, span.set_attribute, ) def on_success( self, span: Span, result: _BotoResultT, instrumentor_context: _BotocoreInstrumentorContext, ): if not span.is_recording(): return if self._op is None: return self._add_attributes(result, self._op.response_attributes, span.set_attribute) def _add_attributes( self, provider: dict[str, Any], attributes: dict[str, _AttrSpecT], setter: Callable[[str, AttributeValue], None], ): if attributes is None: return for attr_key, attr_spec in attributes.items(): attr_path, converter = attr_spec value = self._get_attr_value(provider, attr_path) if value is None: continue if converter is not None: value = converter(value) if value is None: continue setter(attr_key, value) @staticmethod def _get_attr_value(provider: dict[str, Any], attr_path: _AttributePathT): if isinstance(attr_path, str): return provider.get(attr_path) value = provider for path_part in attr_path: value = value.get(path_part) if value is None: return None return None if value is provider else value lmbd.py000066400000000000000000000073521527070072300471400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import abc import inspect import json import re from opentelemetry.instrumentation.botocore.extensions.types import ( _AttributeMapT, _AwsSdkCallContext, _AwsSdkExtension, _BotocoreInstrumentorContext, ) from opentelemetry.propagate import inject from opentelemetry.semconv._incubating.attributes.faas_attributes import ( FAAS_INVOKED_NAME, FAAS_INVOKED_PROVIDER, FAAS_INVOKED_REGION, ) from opentelemetry.trace.span import Span class _LambdaOperation(abc.ABC): @classmethod @abc.abstractmethod def operation_name(cls): pass @classmethod def prepare_attributes(cls, call_context: _AwsSdkCallContext, attributes: _AttributeMapT): pass @classmethod def before_service_call(cls, call_context: _AwsSdkCallContext, span: Span): pass class _OpInvoke(_LambdaOperation): # https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#API_Invoke_RequestParameters ARN_LAMBDA_PATTERN = re.compile( "(?:arn:(?:aws[a-zA-Z-]*)?:lambda:)?" "(?:[a-z]{2}(?:-gov)?-[a-z]+-\\d{1}:)?(?:\\d{12}:)?" "(?:function:)?([a-zA-Z0-9-_\\.]+)(?::(?:\\$LATEST|[a-zA-Z0-9-_]+))?" ) @classmethod def operation_name(cls): return "Invoke" @classmethod def extract_attributes(cls, call_context: _AwsSdkCallContext, attributes: _AttributeMapT): attributes[FAAS_INVOKED_PROVIDER] = "aws" attributes[FAAS_INVOKED_NAME] = cls._parse_function_name(call_context) attributes[FAAS_INVOKED_REGION] = call_context.region @classmethod def _parse_function_name(cls, call_context: _AwsSdkCallContext): function_name_or_arn = call_context.params.get("FunctionName") matches = cls.ARN_LAMBDA_PATTERN.match(function_name_or_arn) function_name = matches.group(1) return function_name_or_arn if function_name is None else function_name @classmethod def before_service_call(cls, call_context: _AwsSdkCallContext, span: Span): cls._inject_current_span(call_context) @classmethod def _inject_current_span(cls, call_context: _AwsSdkCallContext): payload_str = call_context.params.get("Payload") if payload_str is None: return # TODO: reconsider propagation via payload as it manipulates input of the called lambda function try: payload = json.loads(payload_str) headers = payload.get("headers", {}) inject(headers) payload["headers"] = headers call_context.params["Payload"] = json.dumps(payload) except ValueError: pass ################################################################################ # Lambda extension ################################################################################ def _is_operation(op: object) -> bool: try: return inspect.isclass(op) and issubclass(op, _LambdaOperation) and not inspect.isabstract(op) except TypeError: return False _OPERATION_MAPPING: dict[str, _LambdaOperation] = { op.operation_name(): op for op in globals().values() if _is_operation(op) } class _LambdaExtension(_AwsSdkExtension): def __init__(self, call_context: _AwsSdkCallContext): super().__init__(call_context) self._op = _OPERATION_MAPPING.get(call_context.operation) def extract_attributes(self, attributes: _AttributeMapT): if self._op is None: return self._op.extract_attributes(self._call_context, attributes) def before_service_call(self, span: Span, instrumentor_context: _BotocoreInstrumentorContext): if self._op is None: return self._op.before_service_call(self._call_context, span) registry.py000066400000000000000000000155471527070072300500770ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging from collections.abc import Callable, Mapping from typing import TYPE_CHECKING from opentelemetry._logs import get_logger from opentelemetry.instrumentation.botocore.extensions.types import ( _AwsSdkCallContext, _AwsSdkExtension, ) from opentelemetry.instrumentation.botocore.utils import _safe_invoke from opentelemetry.instrumentation.botocore.version import __version__ from opentelemetry.metrics import get_meter from opentelemetry.trace import get_tracer if TYPE_CHECKING: from opentelemetry._logs import Logger, LoggerProvider from opentelemetry.metrics import Instrument, Meter, MeterProvider from opentelemetry.trace import Tracer, TracerProvider _logger = logging.getLogger(__name__) class ExtensionRegistry: """ Registry for AWS SDK extensions that manages extension lookup and associated OpenTelemetry instrumentation components (tracers, loggers, meters, metrics). """ def __init__( self, package_name: str, extensions: Mapping[str, Callable[[], type[_AwsSdkExtension]]], tracer_provider: TracerProvider | None = None, logger_provider: LoggerProvider | None = None, meter_provider: MeterProvider | None = None, ): self._package_name = package_name self._extensions: Mapping[str, Callable[[], type[_AwsSdkExtension]]] = extensions self._tracer_provider: TracerProvider = tracer_provider self._logger_provider: LoggerProvider = logger_provider self._meter_provider: MeterProvider = meter_provider self._tracers: dict[str, Tracer] = {} self._loggers: dict[str, Logger] = {} self._meters: dict[str, Meter] = {} self._metrics: dict[str, dict[str, Instrument]] = {} def get_extension(self, call_context: _AwsSdkCallContext) -> _AwsSdkExtension: """ Get the appropriate extension for a given call context. Args: call_context: The AWS SDK call context Returns: The matching extension for the service/operation """ try: loader: Callable[[], type[_AwsSdkExtension]] = self._extensions.get(call_context.service) if loader is None: return _AwsSdkExtension(call_context) extension_cls = loader() return extension_cls(call_context) except Exception as exc: # pylint: disable=broad-except _logger.error("Error when loading extension: %s", exc) return _AwsSdkExtension(call_context) def has_extension(self, call_context: _AwsSdkCallContext) -> bool: """ Check if a dedicated extension exists for the given call context. Args: call_context: The AWS SDK call context Returns: True if a service-specific extension exists, False otherwise """ return call_context.service in self._extensions def get_instrumentation_name(self, extension: _AwsSdkExtension) -> str: """ Get the instrumentation name for an extension. Service-specific extensions get a namespaced name (e.g., 'module.s3'), while the default extension uses just the module name. Args: extension: The AWS SDK extension Returns: The instrumentation name string """ if self.has_extension(extension._call_context): return f"{self._package_name}.{extension._call_context.service}" return self._package_name def get_tracer(self, extension: _AwsSdkExtension) -> Tracer: """ Get or create a tracer for the given extension. Tracers are cached per instrumentation name for reuse. Args: extension: The AWS SDK extension Returns: A configured Tracer instance """ instrumentation_name: str = self.get_instrumentation_name(extension) if instrumentation_name in self._tracers: return self._tracers[instrumentation_name] schema_version: str = extension.tracer_schema_version() tracer: Tracer = get_tracer( instrumentation_name, __version__, self._tracer_provider, schema_url=f"https://opentelemetry.io/schemas/{schema_version}", ) self._tracers[instrumentation_name] = tracer return tracer def get_logger(self, extension: _AwsSdkExtension): """ Get or create a logger for the given extension. Loggers are cached per instrumentation name for reuse. Args: extension: The AWS SDK extension Returns: A configured Logger instance """ instrumentation_name: str = self.get_instrumentation_name(extension) if instrumentation_name in self._loggers: return self._loggers[instrumentation_name] schema_version: str = extension.event_logger_schema_version() logger: Logger = get_logger( instrumentation_name, schema_url=f"https://opentelemetry.io/schemas/{schema_version}", logger_provider=self._logger_provider, ) self._loggers[instrumentation_name] = logger return logger def get_meter(self, extension: _AwsSdkExtension) -> Meter: """ Get or create a meter for the given extension. Meters are cached per instrumentation name for reuse. Args: extension: The AWS SDK extension Returns: A configured Meter instance """ instrumentation_name: str = self.get_instrumentation_name(extension) if instrumentation_name in self._meters: return self._meters[instrumentation_name] schema_version: str = extension.meter_schema_version() meter = get_meter( instrumentation_name, "", schema_url=f"https://opentelemetry.io/schemas/{schema_version}", meter_provider=self._meter_provider, ) self._meters[instrumentation_name] = meter return meter def get_metrics(self, extension: _AwsSdkExtension) -> dict[str, Instrument]: """ Get or create metrics for the given extension. Metrics are lazily initialized by calling the extension's setup_metrics method. Args: extension: The AWS SDK extension Returns: A dictionary mapping metric names to Instrument instances """ instrumentation_name: str = self.get_instrumentation_name(extension) if instrumentation_name in self._metrics: return self._metrics[instrumentation_name] meter: Meter = self.get_meter(extension) metrics: dict[str, Instrument] = {} _safe_invoke(extension.setup_metrics, meter, metrics) self._metrics[instrumentation_name] = metrics return metrics secretsmanager.py000066400000000000000000000023031527070072300512140ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.instrumentation.botocore.extensions.types import ( _AttributeMapT, _AwsSdkExtension, _BotocoreInstrumentorContext, _BotoResultT, ) from opentelemetry.semconv._incubating.attributes.aws_attributes import ( AWS_SECRETSMANAGER_SECRET_ARN, ) from opentelemetry.trace.span import Span class _SecretsManagerExtension(_AwsSdkExtension): def extract_attributes(self, attributes: _AttributeMapT): """ SecretId is extracted if a secret ARN, the function extracts the attribute only if the SecretId parameter is provided as an arn which starts with `arn:aws:secretsmanager:` """ secret_id = self._call_context.params.get("SecretId") if secret_id and secret_id.startswith("arn:aws:secretsmanager:"): attributes[AWS_SECRETSMANAGER_SECRET_ARN] = secret_id def on_success( self, span: Span, result: _BotoResultT, instrumentor_context: _BotocoreInstrumentorContext, ): secret_arn = result.get("ARN") if secret_arn: span.set_attribute(AWS_SECRETSMANAGER_SECRET_ARN, secret_arn) sfns.py000066400000000000000000000027511527070072300471710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.instrumentation.botocore.extensions.types import ( _AttributeMapT, _AwsSdkExtension, _BotocoreInstrumentorContext, _BotoResultT, ) from opentelemetry.semconv._incubating.attributes.aws_attributes import ( AWS_STEP_FUNCTIONS_ACTIVITY_ARN, AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN, ) from opentelemetry.trace.span import Span class _StepFunctionsExtension(_AwsSdkExtension): @staticmethod def _set_arn_attributes(source, target, setter_func): """Helper to set ARN attributes if they exist in source.""" activity_arn = source.get("activityArn") if activity_arn: setter_func(target, AWS_STEP_FUNCTIONS_ACTIVITY_ARN, activity_arn) state_machine_arn = source.get("stateMachineArn") if state_machine_arn: setter_func(target, AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN, state_machine_arn) def extract_attributes(self, attributes: _AttributeMapT): self._set_arn_attributes( self._call_context.params, attributes, lambda target, key, value: target.__setitem__(key, value), ) def on_success( self, span: Span, result: _BotoResultT, instrumentor_context: _BotocoreInstrumentorContext, ): self._set_arn_attributes( result, span, lambda target, key, value: target.set_attribute(key, value), ) sns.py000066400000000000000000000125111527070072300470160ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import abc import inspect from collections.abc import MutableMapping from typing import Any from opentelemetry.instrumentation.botocore.extensions._messaging import ( inject_propagation_context, ) from opentelemetry.instrumentation.botocore.extensions.types import ( _AttributeMapT, _AwsSdkCallContext, _AwsSdkExtension, _BotocoreInstrumentorContext, _BotoResultT, ) from opentelemetry.semconv._incubating.attributes import messaging_attributes from opentelemetry.semconv._incubating.attributes.aws_attributes import ( AWS_SNS_TOPIC_ARN, ) from opentelemetry.semconv.trace import ( MessagingDestinationKindValues, SpanAttributes, ) from opentelemetry.trace import SpanKind from opentelemetry.trace.span import Span ################################################################################ # SNS operations ################################################################################ class _SnsOperation(abc.ABC): @classmethod @abc.abstractmethod def operation_name(cls) -> str: pass @classmethod def span_kind(cls) -> SpanKind: return SpanKind.CLIENT @classmethod def extract_attributes(cls, call_context: _AwsSdkCallContext, attributes: _AttributeMapT): pass @classmethod def before_service_call(cls, call_context: _AwsSdkCallContext, span: Span): pass class _OpPublish(_SnsOperation): _arn_arg_names = ("TopicArn", "TargetArn") _phone_arg_name = "PhoneNumber" @classmethod def operation_name(cls) -> str: return "Publish" @classmethod def span_kind(cls) -> SpanKind: return SpanKind.PRODUCER @classmethod def extract_attributes(cls, call_context: _AwsSdkCallContext, attributes: _AttributeMapT): span_name, destination_name = cls._extract_destination_name(call_context) call_context.span_name = f"{span_name} send" attributes[SpanAttributes.MESSAGING_DESTINATION_KIND] = MessagingDestinationKindValues.TOPIC.value attributes[SpanAttributes.MESSAGING_DESTINATION] = destination_name attributes[SpanAttributes.MESSAGING_DESTINATION_NAME] = destination_name @classmethod def _extract_destination_name(cls, call_context: _AwsSdkCallContext) -> tuple[str, str]: arn = cls._extract_input_arn(call_context) if arn: return arn.rsplit(":", 1)[-1], arn if cls._phone_arg_name: phone_number = call_context.params.get(cls._phone_arg_name) if phone_number: # phone number redacted because it's a PII return "phone_number", "phone_number:**" return "unknown", "unknown" @classmethod def _extract_input_arn(cls, call_context: _AwsSdkCallContext) -> str | None: for input_arn in cls._arn_arg_names: arn = call_context.params.get(input_arn) if arn: return arn return None @classmethod def before_service_call(cls, call_context: _AwsSdkCallContext, span: Span): cls._inject_span_into_entry(call_context.params) @classmethod def _inject_span_into_entry(cls, entry: MutableMapping[str, Any]): entry["MessageAttributes"] = inject_propagation_context(entry.get("MessageAttributes")) class _OpPublishBatch(_OpPublish): _arn_arg_names = ("TopicArn",) _phone_arg_name = None @classmethod def operation_name(cls) -> str: return "PublishBatch" @classmethod def before_service_call(cls, call_context: _AwsSdkCallContext, span: Span): for entry in call_context.params.get("PublishBatchRequestEntries", ()): cls._inject_span_into_entry(entry) ################################################################################ # SNS extension ################################################################################ def _is_operation(op: object) -> bool: try: return inspect.isclass(op) and issubclass(op, _SnsOperation) and not inspect.isabstract(op) except TypeError: return False _OPERATION_MAPPING: dict[str, _SnsOperation] = { op.operation_name(): op for op in globals().values() if _is_operation(op) } class _SnsExtension(_AwsSdkExtension): def __init__(self, call_context: _AwsSdkCallContext): super().__init__(call_context) self._op = _OPERATION_MAPPING.get(call_context.operation) if self._op: call_context.span_kind = self._op.span_kind() def extract_attributes(self, attributes: _AttributeMapT): attributes[messaging_attributes.MESSAGING_SYSTEM] = "aws.sns" topic_arn = self._call_context.params.get("TopicArn") if topic_arn: attributes[AWS_SNS_TOPIC_ARN] = topic_arn if self._op: self._op.extract_attributes(self._call_context, attributes) def before_service_call(self, span: Span, instrumentor_context: _BotocoreInstrumentorContext): if self._op: self._op.before_service_call(self._call_context, span) def on_success( self, span: Span, result: _BotoResultT, instrumentor_context: _BotocoreInstrumentorContext, ): if not span.is_recording(): return topic_arn = result.get("TopicArn") if topic_arn: span.set_attribute(AWS_SNS_TOPIC_ARN, topic_arn) sqs.py000066400000000000000000000046001527070072300470210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging from opentelemetry.instrumentation.botocore.extensions.types import ( _AttributeMapT, _AwsSdkExtension, _BotocoreInstrumentorContext, _BotoResultT, ) from opentelemetry.semconv._incubating.attributes import messaging_attributes from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace.span import Span _SUPPORTED_OPERATIONS = ["SendMessage", "SendMessageBatch", "ReceiveMessage"] _logger = logging.getLogger(__name__) class _SqsExtension(_AwsSdkExtension): def extract_attributes(self, attributes: _AttributeMapT): queue_url = self._call_context.params.get("QueueUrl") if queue_url: # TODO: update when semantic conventions exist attributes["aws.queue_url"] = queue_url attributes[messaging_attributes.MESSAGING_SYSTEM] = "aws.sqs" attributes[SpanAttributes.MESSAGING_URL] = queue_url try: attributes[SpanAttributes.MESSAGING_DESTINATION] = queue_url.split("/")[-1] except IndexError: _logger.error( "Could not extract messaging destination from '%s'", queue_url, ) def on_success( self, span: Span, result: _BotoResultT, instrumentor_context: _BotocoreInstrumentorContext, ): operation = self._call_context.operation if operation in _SUPPORTED_OPERATIONS: try: if operation == "SendMessage": span.set_attribute( messaging_attributes.MESSAGING_MESSAGE_ID, result.get("MessageId"), ) elif operation == "SendMessageBatch" and result.get("Successful"): span.set_attribute( messaging_attributes.MESSAGING_MESSAGE_ID, result["Successful"][0]["MessageId"], ) elif operation == "ReceiveMessage" and result.get("Messages"): span.set_attribute( messaging_attributes.MESSAGING_MESSAGE_ID, result["Messages"][0]["MessageId"], ) except (IndexError, KeyError): _logger.error("Could not extract the messaging message ID") types.py000066400000000000000000000136011527070072300473600ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging from typing import Any from opentelemetry._logs import Logger from opentelemetry.metrics import Instrument, Meter from opentelemetry.trace import SpanKind from opentelemetry.trace.span import Span from opentelemetry.util.types import AttributeValue _logger = logging.getLogger(__name__) _BotoClientT = "botocore.client.BaseClient" _BotoResultT = dict[str, Any] _BotoClientErrorT = "botocore.exceptions.ClientError" _OperationParamsT = dict[str, Any] _AttributeMapT = dict[str, AttributeValue] class _AwsSdkCallContext: """An context object providing information about the invoked AWS service call. Args: service: the AWS service (e.g. s3, lambda, ...) which is called service_id: the name of the service in proper casing operation: the called operation (e.g. ListBuckets, Invoke, ...) of the AWS service. params: a dict of input parameters passed to the service operation. region: the AWS region in which the service call is made endpoint_url: the endpoint which the service operation is calling api_version: the API version of the called AWS service. span_name: the name used to create the span. span_kind: the kind used to create the span. """ def __init__(self, client: _BotoClientT, args: tuple[str, dict[str, Any]]): operation = args[0] try: params = args[1] except (IndexError, TypeError): _logger.warning("Could not get request params.") params = {} boto_meta = client.meta service_model = boto_meta.service_model self.service = service_model.service_name.lower() self.operation = operation self.params = params # 'operation' and 'service' are essential for instrumentation. # for all other attributes we extract them defensively. All of them should # usually exist unless some future botocore version moved things. self.region: str | None = self._get_attr(boto_meta, "region_name") self.endpoint_url: str | None = self._get_attr(boto_meta, "endpoint_url") self.api_version: str | None = self._get_attr(service_model, "api_version") # name of the service in proper casing self.service_id = str(self._get_attr(service_model, "service_id", self.service)) self.span_name = f"{self.service_id}.{self.operation}" self.span_kind = SpanKind.CLIENT @staticmethod def _get_attr(obj, name: str, default=None): try: return getattr(obj, name) except AttributeError: _logger.warning("Could not get attribute '%s'", name) return default class _BotocoreInstrumentorContext: def __init__( self, logger: Logger, metrics: dict[str, Instrument] | None = None, ): self.logger = logger self.metrics = metrics or {} class _AwsSdkExtension: def __init__(self, call_context: _AwsSdkCallContext): self._call_context = call_context @staticmethod def tracer_schema_version() -> str: """Returns the tracer OTel schema version the extension is following""" return "1.11.0" @staticmethod def event_logger_schema_version() -> str: """Returns the event logger OTel schema version the extension is following""" return "1.30.0" @staticmethod def meter_schema_version() -> str: """Returns the meter OTel schema version the extension is following""" return "1.30.0" def should_trace_service_call(self) -> bool: # pylint:disable=no-self-use """Returns if the AWS SDK service call should be traced or not Extensions might override this function to disable tracing for certain operations. """ return True def should_end_span_on_exit(self) -> bool: # pylint:disable=no-self-use """Returns if the span should be closed automatically on exit Extensions might override this function to disable automatic closing of the span if they need to close it at a later time themselves. """ return True def setup_metrics(self, meter: Meter, metrics: dict[str, Instrument]): """Callback which gets invoked to setup metrics. Extensions might override this function to add to the metrics dictionary all the metrics they want to receive later in _BotocoreInstrumentorContext.""" def extract_attributes(self, attributes: _AttributeMapT): """Callback which gets invoked before the span is created. Extensions might override this function to extract additional attributes. """ def before_service_call(self, span: Span, instrumentor_context: _BotocoreInstrumentorContext): """Callback which gets invoked after the span is created but before the AWS SDK service is called. Extensions might override this function e.g. for injecting the span into a carrier. """ def on_success( self, span: Span, result: _BotoResultT, instrumentor_context: _BotocoreInstrumentorContext, ): """Callback that gets invoked when the AWS SDK call returns successfully. Extensions might override this function e.g. to extract and set response attributes on the span. """ def on_error( self, span: Span, exception: _BotoClientErrorT, instrumentor_context: _BotocoreInstrumentorContext, ): """Callback that gets invoked when the AWS SDK service call raises a ClientError. """ def after_service_call(self, instrumentor_context: _BotocoreInstrumentorContext): """Callback that gets invoked after the AWS SDK service was called. Extensions might override this function to do some cleanup tasks. """ package.py000066400000000000000000000004101527070072300454020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments_botocore = ("botocore~=1.0",) _instruments_aiobotocore = ("aiobotocore>=2.0,<4.0",) _instruments = () _instruments_any = (*_instruments_botocore, *_instruments_aiobotocore) utils.py000066400000000000000000000020621527070072300451540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging from collections.abc import Callable from urllib.parse import urlparse from opentelemetry.semconv._incubating.attributes import ( server_attributes as ServerAttributes, ) from opentelemetry.util.types import AttributeValue _logger = logging.getLogger(__name__) def get_server_attributes(endpoint_url: str) -> dict[str, AttributeValue]: """Extract server.* attributes from AWS endpoint URL.""" parsed = urlparse(endpoint_url) attributes = {} if parsed.hostname: attributes[ServerAttributes.SERVER_ADDRESS] = parsed.hostname attributes[ServerAttributes.SERVER_PORT] = parsed.port or 443 return attributes def _safe_invoke(function: Callable, *args): function_name = "" try: function_name = function.__name__ function(*args) except Exception as ex: # pylint:disable=broad-except _logger.error("Error when invoking function '%s'", function_name, exc_info=ex) version.py000066400000000000000000000001441527070072300455000ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements-0-wrapt1.txt000066400000000000000000000000511527070072300405060ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore-r test-requirements-0.txt wrapt==1.17.3 test-requirements-0-wrapt2.txt000066400000000000000000000000501527070072300405060ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore-r test-requirements-0.txt wrapt==2.1.2 test-requirements-0.txt000066400000000000000000000013131527070072300372740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocoreasgiref==3.8.1 aws-xray-sdk==2.12.1 boto3==1.28.80 botocore==1.31.80 certifi==2024.7.4 cffi==1.17.0 charset-normalizer==3.3.2 cryptography==43.0.1 docker==7.0.0 idna==3.7 iniconfig==2.0.0 Jinja2==3.1.6 jmespath==1.0.1 MarkupSafe==2.1.5 moto==5.1.11 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pycparser==2.21 pytest==7.4.4 pytest-vcr==1.0.2 python-dateutil==2.8.2 pytz==2024.1 PyYAML==6.0.1 requests==2.33.1 responses==0.25.0 s3transfer==0.7.0 six==1.16.0 tomli==2.0.1 typing_extensions==4.12.2 urllib3==1.26.19 vcrpy==8.3.0 Werkzeug==3.0.6 xmltodict==0.13.0 zipp==3.19.2 -e opentelemetry-instrumentation -e propagator/opentelemetry-propagator-aws-xray -e instrumentation/opentelemetry-instrumentation-botocore test-requirements-1-wrapt1.txt000066400000000000000000000000511527070072300405070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore-r test-requirements-1.txt wrapt==1.17.3 test-requirements-1-wrapt2.txt000066400000000000000000000000501527070072300405070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore-r test-requirements-1.txt wrapt==2.1.2 test-requirements-1.txt000066400000000000000000000013121527070072300372740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocoreasgiref==3.8.1 aws-xray-sdk==2.12.1 boto3==1.43.0 botocore==1.43.0 certifi==2024.7.4 cffi==1.17.0 charset-normalizer==3.3.2 cryptography==43.0.1 docker==7.0.0 idna==3.7 iniconfig==2.0.0 Jinja2==3.1.6 jmespath==1.0.1 MarkupSafe==2.1.5 moto==5.1.11 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pycparser==2.21 pytest==7.4.4 pytest-vcr==1.0.2 python-dateutil==2.8.2 pytz==2024.1 PyYAML==6.0.1 requests==2.33.1 responses==0.25.0 s3transfer==0.17.0 six==1.16.0 tomli==2.0.1 typing_extensions==4.12.2 urllib3==1.26.19 vcrpy==8.3.0 Werkzeug==3.0.6 xmltodict==0.13.0 zipp==3.19.2 -e opentelemetry-instrumentation -e propagator/opentelemetry-propagator-aws-xray -e instrumentation/opentelemetry-instrumentation-botocore test-requirements-2.txt000066400000000000000000000013751527070072300373060ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocoreasgiref==3.8.1 aws-xray-sdk==2.12.1 boto3==1.29.4 botocore==1.32.4 aiobotocore==2.8.0 certifi==2024.7.4 cffi==1.17.0 charset-normalizer==3.3.2 cryptography==43.0.1 Deprecated==1.2.14 docker==7.0.0 idna==3.7 iniconfig==2.0.0 Jinja2==3.1.6 jmespath==1.0.1 MarkupSafe==2.1.5 moto==5.1.11 packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pycparser==2.21 pytest==7.4.4 pytest-vcr==1.0.2 python-dateutil==2.8.2 pytz==2024.1 PyYAML==6.0.1 requests==2.33.1 responses==0.25.0 s3transfer==0.7.0 six==1.16.0 tomli==2.0.1 typing_extensions==4.12.2 urllib3==1.26.19 vcrpy==8.3.0 Werkzeug==3.0.6 wrapt==1.16.0 xmltodict==0.13.0 zipp==3.19.2 -e opentelemetry-instrumentation -e propagator/opentelemetry-propagator-aws-xray -e instrumentation/opentelemetry-instrumentation-botocore test-requirements-3.txt000066400000000000000000000013761527070072300373100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocoreasgiref==3.8.1 aws-xray-sdk==2.12.1 boto3==1.43.0 botocore==1.43.0 aiobotocore==3.7.0 certifi==2024.7.4 cffi==1.17.0 charset-normalizer==3.3.2 cryptography==43.0.1 Deprecated==1.2.14 docker==7.0.0 idna==3.7 iniconfig==2.0.0 Jinja2==3.1.6 jmespath==1.0.1 MarkupSafe==2.1.5 moto==5.1.11 packaging==24.0 pluggy==1.5.0 py-cpuinfo==9.0.0 pycparser==2.21 pytest==7.4.4 pytest-vcr==1.0.2 python-dateutil==2.8.2 pytz==2024.1 PyYAML==6.0.1 requests==2.33.1 responses==0.25.0 s3transfer==0.17.0 six==1.16.0 tomli==2.0.1 typing_extensions==4.15.0 urllib3==1.26.19 vcrpy==8.3.0 Werkzeug==3.0.6 wrapt==1.16.0 xmltodict==0.13.0 zipp==3.19.2 -e opentelemetry-instrumentation -e propagator/opentelemetry-propagator-aws-xray -e instrumentation/opentelemetry-instrumentation-botocore opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/000077500000000000000000000000001527070072300341215ustar00rootroot00000000000000README.md000066400000000000000000000011351527070072300353210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests## Recording calls If you need to record calls you may need to export authentication variables and the default region as environment variables in order to have the code work properly. The recorded tests assume the region us-east-1, so ensure that AWS_DEFAULT_REGION is set accordingly when recording new calls. Since tox blocks environment variables by default you need to override its configuration to let them pass: ``` export TOX_OVERRIDE=testenv.pass_env=AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN,AWS_DEFAULT_REGION ``` We are not adding it to tox.ini because of security concerns. __init__.py000066400000000000000000000000001527070072300361410ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/testsbedrock_utils.py000066400000000000000000000323331527070072300372510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import json import math from typing import Any from botocore.response import StreamingBody from opentelemetry.instrumentation.botocore.extensions.bedrock import ( _GEN_AI_CLIENT_OPERATION_DURATION_BUCKETS, _GEN_AI_CLIENT_TOKEN_USAGE_BUCKETS, ) from opentelemetry.sdk.metrics._internal.point import ResourceMetrics from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, ) from opentelemetry.semconv._incubating.attributes import ( server_attributes as ServerAttributes, ) from opentelemetry.semconv._incubating.attributes.error_attributes import ( ERROR_TYPE, ) from opentelemetry.semconv._incubating.metrics.gen_ai_metrics import ( GEN_AI_CLIENT_OPERATION_DURATION, GEN_AI_CLIENT_TOKEN_USAGE, ) # pylint: disable=too-many-branches, too-many-locals, too-many-statements def assert_completion_attributes_from_streaming_body( span: ReadableSpan, request_model: str, response: StreamingBody | None, operation_name: str = "chat", request_top_p: int | None = None, request_temperature: int | None = None, request_max_tokens: int | None = None, request_stop_sequences: list[str] | None = None, ): input_tokens = None output_tokens = None finish_reason = None request_prompt = "Say this is a test" if response is not None: original_body = response["body"] body_content = original_body.read() response = json.loads(body_content.decode("utf-8")) assert response if "amazon.titan" in request_model: input_tokens = response.get("inputTextTokenCount") results = response.get("results") if results: first_result = results[0] output_tokens = first_result.get("tokenCount") finish_reason = (first_result["completionReason"],) elif "amazon.nova" in request_model: if usage := response.get("usage"): input_tokens = usage["inputTokens"] output_tokens = usage["outputTokens"] else: input_tokens, output_tokens = None, None if "stopReason" in response: finish_reason = (response["stopReason"],) else: finish_reason = None elif "anthropic.claude" in request_model: if usage := response.get("usage"): input_tokens = usage["input_tokens"] output_tokens = usage["output_tokens"] else: input_tokens, output_tokens = None, None if "stop_reason" in response: finish_reason = (response["stop_reason"],) else: finish_reason = None elif "cohere.command-r" in request_model: input_tokens = math.ceil(len(request_prompt) / 6) text = response.get("text") if text: output_tokens = math.ceil(len(text) / 6) finish_reason = (response["finish_reason"],) elif "cohere.command" in request_model: input_tokens = math.ceil(len(request_prompt) / 6) generations = response.get("generations") if generations: first_generation = generations[0] output_tokens = math.ceil(len(first_generation["text"]) / 6) finish_reason = (first_generation["finish_reason"],) elif "meta.llama" in request_model: if "prompt_token_count" in response: input_tokens = response.get("prompt_token_count") if "generation_token_count" in response: output_tokens = response.get("generation_token_count") if "stop_reason" in response: finish_reason = (response["stop_reason"],) elif "mistral.mistral" in request_model: input_tokens = math.ceil(len(request_prompt) / 6) outputs = response.get("outputs") if outputs: first_output = outputs[0] output_tokens = math.ceil(len(first_output["text"]) / 6) finish_reason = (first_output["stop_reason"],) return assert_all_attributes( span, request_model, input_tokens, output_tokens, finish_reason, operation_name, request_top_p, request_temperature, request_max_tokens, tuple(request_stop_sequences) if request_stop_sequences is not None else request_stop_sequences, ) def assert_converse_completion_attributes( span: ReadableSpan, request_model: str, response: dict[str, Any] | None, operation_name: str = "chat", request_top_p: int | None = None, request_temperature: int | None = None, request_max_tokens: int | None = None, request_stop_sequences: list[str] | None = None, ): if usage := (response and response.get("usage")): input_tokens = usage["inputTokens"] output_tokens = usage["outputTokens"] else: input_tokens, output_tokens = None, None if response and "stopReason" in response: finish_reason = (response["stopReason"],) else: finish_reason = None return assert_all_attributes( span, request_model, input_tokens, output_tokens, finish_reason, operation_name, request_top_p, request_temperature, request_max_tokens, tuple(request_stop_sequences) if request_stop_sequences is not None else request_stop_sequences, ) def assert_stream_completion_attributes( span: ReadableSpan, request_model: str, input_tokens: int | None = None, output_tokens: int | None = None, finish_reason: tuple[str] | None = None, operation_name: str = "chat", request_top_p: int | None = None, request_temperature: int | None = None, request_max_tokens: int | None = None, request_stop_sequences: list[str] | None = None, ): return assert_all_attributes( span, request_model, input_tokens, output_tokens, finish_reason, operation_name, request_top_p, request_temperature, request_max_tokens, tuple(request_stop_sequences) if request_stop_sequences is not None else request_stop_sequences, ) def assert_equal_or_not_present(value, attribute_name, span): if value is not None: assert attribute_name in span.attributes assert value == span.attributes[attribute_name], span.attributes[attribute_name] else: assert attribute_name not in span.attributes, attribute_name def assert_all_attributes( span: ReadableSpan, request_model: str, input_tokens: int | None = None, output_tokens: int | None = None, finish_reason: tuple[str] | None = None, operation_name: str = "chat", request_top_p: int | None = None, request_temperature: int | None = None, request_max_tokens: int | None = None, request_stop_sequences: tuple[str] | None = None, server_address: str = "bedrock-runtime.us-east-1.amazonaws.com", server_port: int = 443, ): assert span.name == f"{operation_name} {request_model}" assert operation_name == span.attributes[GenAIAttributes.GEN_AI_OPERATION_NAME] assert GenAIAttributes.GenAiSystemValues.AWS_BEDROCK.value == span.attributes[GenAIAttributes.GEN_AI_SYSTEM] assert request_model == span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] assert server_address == span.attributes[ServerAttributes.SERVER_ADDRESS] assert server_port == span.attributes[ServerAttributes.SERVER_PORT] assert_equal_or_not_present(input_tokens, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, span) assert_equal_or_not_present(output_tokens, GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, span) assert_equal_or_not_present(finish_reason, GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS, span) assert_equal_or_not_present(request_top_p, GenAIAttributes.GEN_AI_REQUEST_TOP_P, span) assert_equal_or_not_present(request_temperature, GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE, span) assert_equal_or_not_present(request_max_tokens, GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS, span) assert_equal_or_not_present( request_stop_sequences, GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES, span, ) def remove_none_values(body): result = {} for key, value in body.items(): if value is None: continue if isinstance(value, dict): result[key] = remove_none_values(value) else: result[key] = value return result def assert_log_parent(log, span): if span: assert log.log_record.trace_id == span.get_span_context().trace_id, ( f"{span.get_span_context().trace_id} does not equal {log.log_record.trace_id}" ) assert log.log_record.span_id == span.get_span_context().span_id assert log.log_record.trace_flags == span.get_span_context().trace_flags def assert_message_in_logs(log, event_name, expected_content, parent_span): assert log.log_record.event_name == event_name, log.log_record.event_name assert ( log.log_record.attributes[GenAIAttributes.GEN_AI_SYSTEM] == GenAIAttributes.GenAiSystemValues.AWS_BEDROCK.value ) if not expected_content: assert not log.log_record.body else: assert log.log_record.body assert dict(log.log_record.body) == remove_none_values(expected_content), dict(log.log_record.body) assert_log_parent(log, parent_span) def assert_all_metric_attributes( data_point, operation_name: str, model: str, error_type: str | None = None, server_address: str = "bedrock-runtime.us-east-1.amazonaws.com", server_port: int = 443, ): assert GenAIAttributes.GEN_AI_OPERATION_NAME in data_point.attributes assert data_point.attributes[GenAIAttributes.GEN_AI_OPERATION_NAME] == operation_name assert GenAIAttributes.GEN_AI_SYSTEM in data_point.attributes assert data_point.attributes[GenAIAttributes.GEN_AI_SYSTEM] == GenAIAttributes.GenAiSystemValues.AWS_BEDROCK.value assert GenAIAttributes.GEN_AI_REQUEST_MODEL in data_point.attributes assert data_point.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == model assert ServerAttributes.SERVER_ADDRESS in data_point.attributes assert data_point.attributes[ServerAttributes.SERVER_ADDRESS] == server_address assert ServerAttributes.SERVER_PORT in data_point.attributes assert data_point.attributes[ServerAttributes.SERVER_PORT] == server_port if error_type is not None: assert ERROR_TYPE in data_point.attributes assert data_point.attributes[ERROR_TYPE] == error_type else: assert ERROR_TYPE not in data_point.attributes def assert_metrics( resource_metrics: ResourceMetrics, operation_name: str, model: str, input_tokens: float | None = None, output_tokens: float | None = None, error_type: str | None = None, ): assert len(resource_metrics) == 1 metric_data = resource_metrics[0].scope_metrics[0].metrics if input_tokens is not None or output_tokens is not None: expected_metrics_data_len = 2 else: expected_metrics_data_len = 1 assert len(metric_data) == expected_metrics_data_len duration_metric = next( (m for m in metric_data if m.name == GEN_AI_CLIENT_OPERATION_DURATION), None, ) assert duration_metric is not None duration_point = duration_metric.data.data_points[0] assert duration_point.sum > 0 assert_all_metric_attributes(duration_point, operation_name, model, error_type) assert duration_point.explicit_bounds == tuple(_GEN_AI_CLIENT_OPERATION_DURATION_BUCKETS) if input_tokens is not None: token_usage_metric = next( (m for m in metric_data if m.name == GEN_AI_CLIENT_TOKEN_USAGE), None, ) assert token_usage_metric is not None input_token_usage = next( ( d for d in token_usage_metric.data.data_points if d.attributes[GenAIAttributes.GEN_AI_TOKEN_TYPE] == GenAIAttributes.GenAiTokenTypeValues.INPUT.value ), None, ) assert input_token_usage is not None assert input_token_usage.sum == input_tokens assert input_token_usage.explicit_bounds == tuple(_GEN_AI_CLIENT_TOKEN_USAGE_BUCKETS) assert_all_metric_attributes(input_token_usage, operation_name, model) if output_tokens is not None: token_usage_metric = next( (m for m in metric_data if m.name == GEN_AI_CLIENT_TOKEN_USAGE), None, ) assert token_usage_metric is not None output_token_usage = next( ( d for d in token_usage_metric.data.data_points if d.attributes[GenAIAttributes.GEN_AI_TOKEN_TYPE] == GenAIAttributes.GenAiTokenTypeValues.COMPLETION.value ), None, ) assert output_token_usage is not None assert output_token_usage.sum == output_tokens assert output_token_usage.explicit_bounds == tuple(_GEN_AI_CLIENT_TOKEN_USAGE_BUCKETS) assert_all_metric_attributes(output_token_usage, operation_name, model) cassettes/000077500000000000000000000000001527070072300360405ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/teststest_converse_no_content.yaml000066400000000000000000000037751527070072300440510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] } ], "inferenceConfig": { "maxTokens": 10, "temperature": 0.8, "topP": 1, "stopSequences": [ "|" ] } } headers: Content-Length: - '170' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250211T141035Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-459d61b3-c785d242640df661d315555d;Parent=384fac3cd601ebb0;Sampled=1 amz-sdk-invocation-id: - 1de3c974-3f70-4126-85a8-d4b9bcc2e1b1 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.titan-text-lite-v1/converse response: body: string: |- { "metrics": { "latencyMs": 683 }, "output": { "message": { "content": [ { "text": "Hello, how are you doing" } ], "role": "assistant" } }, "stopReason": "max_tokens", "usage": { "inputTokens": 8, "outputTokens": 10, "totalTokens": 18 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Tue, 11 Feb 2025 14:10:36 GMT Set-Cookie: test_set_cookie x-amzn-RequestId: - ff8f385b-009e-49da-b124-1359bc244f10 status: code: 200 message: OK version: 1 test_converse_no_content_different_events.yaml000066400000000000000000000043711527070072300474540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "system": [ { "text": "You are a friendly app" } ], "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] }, { "role": "assistant", "content": [ { "text": "This is a test" } ] }, { "role": "user", "content": [ { "text": "Say again this is a test" } ] } ] } headers: Content-Length: - '258' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250213T113027Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-d9112d64-56325c43023ba04895b18fc1;Parent=d56bf41aee9dd9b2;Sampled=1 amz-sdk-invocation-id: - 4e1fd77b-1b21-4b3a-9b96-d74fb74ede7c amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-v2/converse response: body: string: |- { "metrics": { "latencyMs": 489 }, "output": { "message": { "content": [ { "text": "This is a test" } ], "role": "assistant" } }, "stopReason": "end_turn", "usage": { "inputTokens": 37, "outputTokens": 8, "totalTokens": 45 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 13 Feb 2025 11:30:28 GMT Set-Cookie: test_set_cookie x-amzn-RequestId: - 67f0b4ef-e481-4c12-853b-d8e992395129 status: code: 200 message: OK version: 1 test_converse_stream_close_before_consumption.yaml000066400000000000000000000056611527070072300503370ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] } ], "inferenceConfig": { "maxTokens": 10, "temperature": 0.8, "topP": 1, "stopSequences": [ "|" ] } } headers: Content-Length: - '170' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.4.0 md/arch#arm64 lang/python#3.12.0 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250509T104610Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-98fd1391-6f6d1be3cc209d9ba2fb3034;Parent=6dc095915efed44e;Sampled=1 amz-sdk-invocation-id: - 3a37ec88-d989-4397-bfbe-d463b66a3a08 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.titan-text-lite-v1/converse-stream response: body: string: !!binary | AAAApAAAAFJl4NbnCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTSIsInJvbGUiOiJhc3Npc3RhbnQifSKBqDQAAADZAAAA VxTIBhYLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBw bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4Ijow LCJkZWx0YSI6eyJ0ZXh0IjoiSGksIGRpZCB5b3UgaGF2ZSBhIHF1ZXN0aW9uIn0sInAiOiJhYmNk ZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLIn22h6U3AAAAqgAAAFbdvayfCzpldmVu dC10eXBlBwAQY29udGVudEJsb2NrU3RvcA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJwIjoiYWJjZGVm Z2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0wifQSjvQkAAAChAAAAUTQJCC0LOmV2ZW50 LXR5cGUHAAttZXNzYWdlU3RvcA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7InAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0QiLCJz dG9wUmVhc29uIjoibWF4X3Rva2VucyJ9jxPapgAAAMsAAABOaoNqNAs6ZXZlbnQtdHlwZQcACG1l dGFkYXRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVl dmVudHsibWV0cmljcyI6eyJsYXRlbmN5TXMiOjYyNX0sInAiOiJhYmNkZWZnaGlqa2wiLCJ1c2Fn ZSI6eyJpbnB1dFRva2VucyI6OCwib3V0cHV0VG9rZW5zIjoxMCwidG90YWxUb2tlbnMiOjE4fX0B /Sts headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Fri, 09 May 2025 10:46:10 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked x-amzn-RequestId: - 5aaaa521-d2c6-4980-ba0a-7bba19633a40 status: code: 200 message: OK version: 1 test_converse_stream_handles_event_stream_error.yaml000066400000000000000000000055241527070072300506530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] } ], "inferenceConfig": { "maxTokens": 10, "temperature": 0.8, "topP": 1, "stopSequences": [ "|" ] } } headers: Content-Length: - '170' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.0.0 md/arch#arm64 lang/python#3.13.1 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250206T051234Z X-Amzn-Trace-Id: - Root=1-3cedc279-3dd3346b0b2bb6a9f72fec1f;Parent=3b0e6652d91a2352;Sampled=1 amz-sdk-invocation-id: - 5425c97e-39aa-4c59-9c78-20d972cab920 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.titan-text-lite-v1/converse-stream response: body: string: !!binary | AAAAmgAAAFJ78dAACzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6QUJDIiwicm9sZSI6ImFzc2lzdGFudCJ92EeA9QAAAMwAAABXvMge5As6ZXZl bnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9q c29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7 InRleHQiOiJTdXJlLCBob3cgY2FuIEkgaGVscCB5b3UifSwicCI6ImFiY2RlZmdoaWprbG1ub3Bx cnN0dXZ3eHl6QSJ9FFegJQAAAKsAAABW4N2FLws6ZXZlbnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0 b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 eyJjb250ZW50QmxvY2tJbmRleCI6MCwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJD REVGR0hJSktMTSJ9FYIDaQAAAJcAAABRGmhFCws6ZXZlbnQtdHlwZQcAC21lc3NhZ2VTdG9wDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6 ImFiY2RlZmdoaWprbG1ub3BxcnN0Iiwic3RvcFJlYXNvbiI6Im1heF90b2tlbnMifdtb84cAAADZ AAAATnCjrtYLOmV2ZW50LXR5cGUHAAhtZXRhZGF0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRp b24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7Im1ldHJpY3MiOnsibGF0ZW5jeU1zIjo2MTN9 LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoiLCJ1c2FnZSI6eyJpbnB1dFRva2VucyI6 OCwib3V0cHV0VG9rZW5zIjoxMCwidG90YWxUb2tlbnMiOjE4fX24InyS headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Thu, 06 Feb 2025 05:12:35 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked x-amzn-RequestId: - 63519949-dc02-437f-9ee4-d2eba90685e4 status: code: 200 message: OK version: 1 test_converse_stream_no_content.yaml000066400000000000000000000056461527070072300454230ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] } ], "inferenceConfig": { "maxTokens": 10, "temperature": 0.8, "topP": 1, "stopSequences": [ "|" ] } } headers: Content-Length: - '170' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250211T141036Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-d573e167-b547bec74e267f033a202851;Parent=0a96ae00e1f4d524;Sampled=1 amz-sdk-invocation-id: - abc27203-3d67-43ec-b426-9f01928fd698 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.titan-text-lite-v1/converse-stream response: body: string: !!binary | AAAAuQAAAFL9kIXUCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU2NyIsInJvbGUiOiJh c3Npc3RhbnQifWf51EkAAAC6AAAAV8paC4sLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0 YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7 ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiU3VyZSwgZGlkIHlvdSBoYXZl IGEgcXVlc3Rpb24ifSwicCI6ImFiY2QifQDDASsAAAChAAAAVqptnY4LOmV2ZW50LXR5cGUHABBj b250ZW50QmxvY2tTdG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdl LXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsInAiOiJhYmNkZWZnaGlqa2xtbm9w cXJzdHV2d3h5ekFCQyJ9AHyeLwAAAIoAAABRghgWOAs6ZXZlbnQtdHlwZQcAC21lc3NhZ2VTdG9w DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi cCI6ImFiY2RlZmciLCJzdG9wUmVhc29uIjoibWF4X3Rva2VucyJ9gceI6AAAAOoAAABOliJsgAs6 ZXZlbnQtdHlwZQcACG1ldGFkYXRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTpt ZXNzYWdlLXR5cGUHAAVldmVudHsibWV0cmljcyI6eyJsYXRlbmN5TXMiOjY5Nn0sInAiOiJhYmNk ZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRIiwidXNhZ2UiOnsiaW5wdXRU b2tlbnMiOjgsIm91dHB1dFRva2VucyI6MTAsInRvdGFsVG9rZW5zIjoxOH19By4BYA== headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Tue, 11 Feb 2025 14:10:37 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked x-amzn-RequestId: - a2ff4e96-777d-4bf0-9db7-8a9aa7aecf78 status: code: 200 message: OK version: 1 test_converse_stream_no_content_different_events.yaml000066400000000000000000000076031527070072300510300ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "system": [ { "text": "You are a friendly app" } ], "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] }, { "role": "assistant", "content": [ { "text": "This is a test" } ] }, { "role": "user", "content": [ { "text": "Say again this is a test" } ] } ] } headers: Content-Length: - '258' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250213T134717Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-014325aa-ed2dea786b6b5b895661a1fd;Parent=540790d110580421;Sampled=1 amz-sdk-invocation-id: - 22820432-2412-4351-98af-8c70c71c84a9 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-v2/converse-stream response: body: string: !!binary | AAAAkwAAAFJ24bJxCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXYiLCJyb2xlIjoiYXNzaXN0YW50In2AaQZuAAAAzQAAAFeBqDdUCzpldmVudC10eXBl BwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1l c3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6 IlRoaXMifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFS U1RVViJ9qHD4cgAAALQAAABXdWq16gs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29u dGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgaXMifSwicCI6ImFiY2RlZmdoaWpr bG1ub3BxcnN0dXZ3eCJ9dhKCRAAAALAAAABXgOoTKgs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9j a0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVl dmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgYSJ9LCJwIjoiYWJj ZGVmZ2hpamtsbW5vcHFyc3R1In0IN1EQAAAAtQAAAFdICpxaCzpldmVudC10eXBlBwARY29udGVu dEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlw ZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiB0ZXN0In0s InAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2dyJ9V81m8wAAAIkAAABWW9z5Sws6ZXZlbnQtdHlw ZQcAEGNvbnRlbnRCbG9ja1N0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1l c3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwicCI6ImFiY2RlIn3nHPCv AAAAoAAAAFEJaSGdCzpldmVudC10eXBlBwALbWVzc2FnZVN0b3ANOmNvbnRlbnQtdHlwZQcAEGFw cGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJwIjoiYWJjZGVmZ2hpamtsbW5v cHFyc3R1dnd4eXpBQkNERSIsInN0b3BSZWFzb24iOiJlbmRfdHVybiJ9p5BdEgAAAMsAAABOaoNq NAs6ZXZlbnQtdHlwZQcACG1ldGFkYXRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsibWV0cmljcyI6eyJsYXRlbmN5TXMiOjM2MX0sInAiOiJh YmNkZWZnaGlqa2wiLCJ1c2FnZSI6eyJpbnB1dFRva2VucyI6MzcsIm91dHB1dFRva2VucyI6OCwi dG90YWxUb2tlbnMiOjQ1fX1Nq2WY headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Thu, 13 Feb 2025 13:47:17 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked x-amzn-RequestId: - e443d332-cba6-4076-a4f6-86d6321eada3 status: code: 200 message: OK version: 1 test_converse_stream_no_content_tool_call.yaml000066400000000000000000001164261527070072300474520ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today?" } ] } ], "toolConfig": { "tools": [ { "toolSpec": { "name": "get_current_weather", "description": "Get the current weather in a given location.", "inputSchema": { "json": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } } } ] } } headers: Content-Length: - '409' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250226T110349Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-14730244-640b58229ef56a614d4a069d;Parent=13190044e1ff46a8;Sampled=1 amz-sdk-invocation-id: - 5073899c-6d97-4455-9a8b-fb76fa8d3246 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse-stream response: body: string: !!binary | AAAAkAAAAFIxQcihCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u b3BxcnMiLCJyb2xlIjoiYXNzaXN0YW50In0RduqwAAAAvAAAAFdFGv4rCzpldmVudC10eXBlBwAR Y29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh Z2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6Ijx0 aGlua2luZyJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoifRvDqgwAAACiAAAAV5rK 18gLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJk ZWx0YSI6eyJ0ZXh0IjoiPiJ9LCJwIjoiYWJjZGVmZ2gifbCwznIAAADOAAAAV8YITYQLOmV2ZW50 LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0 ZXh0IjoiIFRoZSJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xN Tk9QUVJTVFVWVyJ97ek1oQAAANYAAABXlpiRxws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0Rl bHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu dHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgVXNlciJ9LCJwIjoiYWJj ZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjMifZfV QZIAAAClAAAAVyjqC9gLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10 eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9j a0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGhhcyJ9LCJwIjoiYWJjZGVmZ2gifcsAIMsAAACx AAAAV72KOpoLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQ YXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4 IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGFza2VkIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXIifXZk tmAAAADZAAAAVxTIBhYLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10 eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9j a0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGZvciJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFy c3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0NTY3In0rjiWOAAAAwQAAAFdE WNpVCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxp Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwi ZGVsdGEiOnsidGV4dCI6IiB0aGUifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJD REVGR0hJSiJ96OGhEwAAAMQAAABXjLhVJQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRh DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Y29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgY3VycmVudCJ9LCJwIjoiYWJj ZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSEkifQvCFpoAAACzAAAAV8dKafoLOmV2ZW50 LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0 ZXh0IjoiIHdlYXRoZXIifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxciJ9CjS8ogAAAK4AAABXXzo6 yQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNh dGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRl bHRhIjp7InRleHQiOiIgaW4ifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxciJ91NAoJgAAAM4AAABX xghNhAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBs aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAs ImRlbHRhIjp7InRleHQiOiIgdHdvIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFC Q0RFRkdISUpLTE1OT1BRUlNUVVZXIn2vLkRuAAAArwAAAFdiWhN5CzpldmVudC10eXBlBwARY29u dGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2Ut dHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBkaWZm ZXJlbnQifSwicCI6ImFiY2RlZmdoaWprbCJ9Wq0gMgAAANAAAABXGdhkZws6ZXZlbnQtdHlwZQcA EWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIg Y2l0aWVzIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BR UlNUVVYifenbblUAAADWAAAAV5aYkccLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06 Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNv bnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiOiJ9LCJwIjoiYWJjZGVmZ2hpamts bW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0NTY3In0PNgK9AAAA 0QAAAFckuE3XCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRl eCI6MCwiZGVsdGEiOnsidGV4dCI6IiBTZWF0dGxlIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJz dHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVYifenGQLcAAADBAAAAV0RY2lULOmV2ZW50LXR5 cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06 bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0 IjoiIGFuZCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKIn1LKIVf AAAAoQAAAFfdaq0YCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlw ZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJ bmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBTYW4ifSwicCI6ImFiY2QifbFjc9YAAAC4AAAAV7Ca WOsLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJk ZWx0YSI6eyJ0ZXh0IjoiIEZyYW5jaXNjbyJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1In26 qf9BAAAAzgAAAFfGCE2ECzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50Qmxv Y2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6Ii4ifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0 dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVoifaBNeiIAAACiAAAAV5rK18gLOmV2ZW50 LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0 ZXh0IjoiIFRvIn0sInAiOiJhYmNkZWYifaxSD78AAACuAAAAV186OskLOmV2ZW50LXR5cGUHABFj b250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHBy b3ZpZGUifSwicCI6ImFiY2RlZmdoaWprbG0ifXap5fYAAACmAAAAV29KcQgLOmV2ZW50LXR5cGUH ABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0Ijoi IHRoaXMifSwicCI6ImFiY2RlZmdoIn1JStpaAAAAugAAAFfKWguLCzpldmVudC10eXBlBwARY29u dGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2Ut dHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBpbmZv cm1hdGlvbiJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1In0he3brAAAA1QAAAFfROOsXCzpl dmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEi OnsidGV4dCI6IiwifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktM TU5PUFFSU1RVVldYWVowMTIzNDU2In1PifLWAAAArwAAAFdiWhN5CzpldmVudC10eXBlBwARY29u dGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2Ut dHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBJIn0s InAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdCJ9exLK/QAAAKwAAABXJfppqQs6ZXZlbnQtdHlwZQcA EWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIg d2lsbCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW4ifYkFaD0AAACvAAAAV2JaE3kLOmV2ZW50LXR5cGUH ABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0Ijoi IG5lZWQifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxIn2klneuAAAAywAAAFcO6ML0CzpldmVudC10 eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24N Om1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4 dCI6IiB0byJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9Q UVJTVFUifSd95ysAAADKAAAAVzOI60QLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06 Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNv bnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHVzZSJ9LCJwIjoiYWJjZGVmZ2hp amtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTIn1elYrQAAAA2QAAAFcUyAYWCzpl dmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEi OnsidGV4dCI6IiB0aGUifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJ SktMTU5PUFFSU1RVVldYWVowMTIzNDU2NyJ9OkAbRQAAALoAAABXyloLiws6ZXZlbnQtdHlwZQcA EWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIg YCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERSJ9GdLZ0AAAALUAAABXSAqc Wgs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNh dGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRl bHRhIjp7InRleHQiOiJnZXQifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHkifSCItygA AADBAAAAV0RY2lULOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBl BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0lu ZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiXyJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4 eXpBQkNERUZHSElKS0xNIn3OZcpMAAAAtwAAAFcyys86CzpldmVudC10eXBlBwARY29udGVudEJs b2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA BWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6ImN1cnJlbnQifSwi cCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3In2mNm+mAAAAnQAAAFe5u/ifCzpldmVudC10eXBl BwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1l c3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6 Il8ifSwicCI6ImFiYyJ9xyLVsgAAAK8AAABXYloTeQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9j a0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVl dmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiJ3ZWF0aGVyIn0sInAi OiJhYmNkZWZnaGlqa2xtbm8iffGg+YAAAACeAAAAV/4bgk8LOmV2ZW50LXR5cGUHABFjb250ZW50 QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBl BwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiYCJ9LCJwIjoi YWJjZCJ9aGmldAAAAK8AAABXYloTeQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29u dGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgdG9vbCJ9LCJwIjoiYWJjZGVmZ2hp amtsbW5vcHEifQXTKocAAADTAAAAV154HrcLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0 YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7 ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHR3aWNlIn0sInAiOiJhYmNk ZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaIn32CtnIAAAA vQAAAFd4etebCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRl eCI6MCwiZGVsdGEiOnsidGV4dCI6IiwifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6 QUJDREVGR0hJIn1wnLBgAAAA0gAAAFdjGDcHCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVs dGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBvbmNlIn0sInAiOiJhYmNk ZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaIn1aRN7cAAAA yQAAAFd0KJGUCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRl eCI6MCwiZGVsdGEiOnsidGV4dCI6IiBmb3IifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3 eHl6QUJDREVGR0hJSktMTU5PUFFSIn0LzBVWAAAA2AAAAFcpqC+mCzpldmVudC10eXBlBwARY29u dGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2Ut dHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBlYWNo In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZX WFlaMDEyMzQ1In014Iy8AAAA1wAAAFer+Lh3CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVs dGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBjaXR5In0sInAiOiJhYmNk ZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQifbV2 F6kAAACfAAAAV8N7q/8LOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10 eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9j a0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiLiJ9LCJwIjoiYWJjZGUifbe2AI0AAADOAAAAV8YI TYQLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJk ZWx0YSI6eyJ0ZXh0IjoiIDwvIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RF RkdISUpLTE1OT1BRUlNUVVZXWCJ9r6ClzwAAALsAAABX9zoiOws6ZXZlbnQtdHlwZQcAEWNvbnRl bnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5 cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiJ0aGlua2lu ZyJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoifULc640AAADXAAAAV6v4uHcLOmV2 ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24v anNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6 eyJ0ZXh0IjoiPiJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xN Tk9QUVJTVFVWV1hZWjAxMjM0NTY3OCJ9od8CUAAAAMcAAABXyxgv9Qs6ZXZlbnQtdHlwZQcAEWNv bnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdl LXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiJcbiJ9 LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVIiff/7dMUA AACsAAAAVlL9WT8LOmV2ZW50LXR5cGUHABBjb250ZW50QmxvY2tTdG9wDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5k ZXgiOjAsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OIn0xkM9d AAABBgAAAFfUMT07CzpldmVudC10eXBlBwARY29udGVudEJsb2NrU3RhcnQNOmNvbnRlbnQtdHlw ZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJ bmRleCI6MSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGRyIsInN0YXJ0Ijp7 InRvb2xVc2UiOnsibmFtZSI6ImdldF9jdXJyZW50X3dlYXRoZXIiLCJ0b29sVXNlSWQiOiJ0b29s dXNlX0lJcE1GVGdhUlZHeUk3eDRzSmZLelEifX19D/LwmQAAAMsAAABXDujC9As6ZXZlbnQtdHlw ZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTpt ZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjEsImRlbHRhIjp7InRvb2xV c2UiOnsiaW5wdXQiOiJ7XCJsb2NhdGlvblwiOlwiU2VhdHRsZVwifSJ9fSwicCI6ImFiY2RlZmdo aWprIn3vc+uaAAAAnwAAAFa0fJtpCzpldmVudC10eXBlBwAQY29udGVudEJsb2NrU3RvcA06Y29u dGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRl bnRCbG9ja0luZGV4IjoxLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBIn0XG2WUAAAB BgAAAFfUMT07CzpldmVudC10eXBlBwARY29udGVudEJsb2NrU3RhcnQNOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRl eCI6MiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGRyIsInN0YXJ0Ijp7InRv b2xVc2UiOnsibmFtZSI6ImdldF9jdXJyZW50X3dlYXRoZXIiLCJ0b29sVXNlSWQiOiJ0b29sdXNl X0taUEJZWVV0UzcyVW1fSkRJOGhUb3cifX19Cd58CwAAAN0AAABX4Uig1gs6ZXZlbnQtdHlwZQcA EWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjIsImRlbHRhIjp7InRvb2xVc2Ui OnsiaW5wdXQiOiJ7XCJsb2NhdGlvblwiOlwiU2FuIEZyYW5jaXNjb1wifSJ9fSwicCI6ImFiY2Rl ZmdoaWprbG1ub3BxcnN0dXZ3In2Ijuw2AAAAmwAAAFZB/D2pCzpldmVudC10eXBlBwAQY29udGVu dEJsb2NrU3RvcA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBl BwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjoyLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1 dncifX26ZYUAAACZAAAAUaVY+2oLOmV2ZW50LXR5cGUHAAttZXNzYWdlU3RvcA06Y29udGVudC10 eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7InAiOiJhYmNkZWZn aGlqa2xtbm9wcXJzdHV2d3giLCJzdG9wUmVhc29uIjoidG9vbF91c2UifdivBEkAAADHAAAATq9z hzULOmV2ZW50LXR5cGUHAAhtZXRhZGF0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7Im1ldHJpY3MiOnsibGF0ZW5jeU1zIjo1OTN9LCJwIjoi YWJjZCIsInVzYWdlIjp7ImlucHV0VG9rZW5zIjo0MTUsIm91dHB1dFRva2VucyI6MTk1LCJ0b3Rh bFRva2VucyI6NjEwfX3rWRkN headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Wed, 26 Feb 2025 11:03:49 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked x-amzn-RequestId: - b386c247-d072-4cc9-a14d-4d63ef2952ca status: code: 200 message: OK - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today?" } ] }, { "content": [ { "text": " The User has asked for the current weather in two different cities: Seattle and San Francisco. To provide this information, I will need to use the `get_current_weather` tool twice, once for each city. \n" }, { "toolUse": { "toolUseId": "tooluse_IIpMFTgaRVGyI7x4sJfKzQ", "name": "get_current_weather", "input": { "location": "Seattle" } } }, { "toolUse": { "toolUseId": "tooluse_KZPBYYUtS72Um_JDI8hTow", "name": "get_current_weather", "input": { "location": "San Francisco" } } } ], "role": "assistant" }, { "role": "user", "content": [ { "toolResult": { "content": [ { "json": { "weather": "50 degrees and raining" } } ], "toolUseId": "tooluse_IIpMFTgaRVGyI7x4sJfKzQ" } }, { "toolResult": { "content": [ { "json": { "weather": "70 degrees and sunny" } } ], "toolUseId": "tooluse_KZPBYYUtS72Um_JDI8hTow" } } ] } ], "toolConfig": { "tools": [ { "toolSpec": { "name": "get_current_weather", "description": "Get the current weather in a given location.", "inputSchema": { "json": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } } } ] } } headers: Content-Length: - '1227' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250226T110350Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-a7f289c8-1900bcc14d30a430fe81c6c0;Parent=19191d335b8dee7e;Sampled=1 amz-sdk-invocation-id: - 4a1d4e1e-7212-4ad8-bc30-46a0dd038dfb amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse-stream response: body: string: !!binary | AAAAugAAAFK6MP8ECzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU2NzgiLCJyb2xlIjoi YXNzaXN0YW50In00xKBOAAAA0AAAAFcZ2GRnCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVs dGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6Ijx0aGlua2luZyJ9LCJwIjoi YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVCJ9272QNgAAAJ0A AABXubv4nws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgi OjAsImRlbHRhIjp7InRleHQiOiI+In0sInAiOiJhYmMifdbwu+0AAAClAAAAVyjqC9gLOmV2ZW50 LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0 ZXh0IjoiIEkifSwicCI6ImFiY2RlZmdoaWoifQhufBcAAADDAAAAVz6YiTULOmV2ZW50LXR5cGUH ABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0Ijoi IGhhdmUifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSksifVFnP/8A AACvAAAAV2JaE3kLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBl BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0lu ZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHJlY2VpdmVkIn0sInAiOiJhYmNkZWZnaGlqa2xtIn00 OqR0AAAA1wAAAFer+Lh3CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50Qmxv Y2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiB0aGUifSwicCI6ImFiY2RlZmdoaWprbG1ub3Bx cnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDUifXS+A7sAAAC9AAAAV3h6 15sLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJk ZWx0YSI6eyJ0ZXh0IjoiIHdlYXRoZXIifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6 QUIiff8p8c4AAADaAAAAV1NofMYLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29u dGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRl bnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGluZm9ybWF0aW9uIn0sInAiOiJhYmNk ZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMCJ9f5XL9AAA ANcAAABXq/i4dws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5k ZXgiOjAsImRlbHRhIjp7InRleHQiOiIgZm9yIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2 d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQ1In37vEGNAAAAqQAAAFftGubZCzpl dmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEi OnsidGV4dCI6IiBib3RoIn0sInAiOiJhYmNkZWZnaGlqayJ986LlSAAAAMIAAABXA/ighQs6ZXZl bnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9q c29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7 InRleHQiOiIgY2l0aWVzIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdI In3wlGolAAAAngAAAFf+G4JPCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRl bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50 QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6Ii4ifSwicCI6ImFiY2QifU/hxnwAAAC8AAAA V0Ua/isLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBw bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4Ijow LCJkZWx0YSI6eyJ0ZXh0IjoiIE5vdyJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpB QkNERSJ9xo6ozgAAALsAAABX9zoiOws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29u dGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIsIn0sInAiOiJhYmNkZWZnaGlqa2xt bm9wcXJzdHV2d3h5ekFCQ0RFRkcifdPRJDYAAAC2AAAAVw+q5ooLOmV2ZW50LXR5cGUHABFjb250 ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10 eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIEkifSwi cCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QSJ9bKLxKQAAAK4AAABXXzo6yQs6ZXZlbnQt dHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRl eHQiOiIgd2lsbCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcCJ9OK6HnQAAALwAAABXRRr+Kws6ZXZl bnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9q c29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7 InRleHQiOiIgc3VtbWFyaXplIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5In3UagVv AAAAuwAAAFf3OiI7CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlw ZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJ bmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiB0aGlzIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJz dHV2d3h5ekFCQyJ9w6slRAAAAN4AAABXpujaBgs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0Rl bHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu dHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgaW5mb3JtYXRpb24ifSwi cCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVow MTIzNCJ9snHIPQAAAKYAAABXb0pxCAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29u dGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgZm9yIn0sInAiOiJhYmNkZWZnaGki fYofts8AAACwAAAAV4DqEyoLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVu dC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRC bG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHRoZSJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5v cHFycyJ9HSwR7gAAAMwAAABXvMge5As6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29u dGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgVXNlciJ9LCJwIjoiYWJjZGVmZ2hp amtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVCJ9rP6RngAAALIAAABX+ipASgs6 ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlv bi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRh Ijp7InRleHQiOiIuIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3gifYhr65EAAADZAAAA VxTIBhYLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBw bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4Ijow LCJkZWx0YSI6eyJ0ZXh0IjoiIDwvIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFC Q0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQ1Njc4In3glJIDAAAApgAAAFdvSnEICzpldmVu dC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pz b24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsi dGV4dCI6InRoaW5raW5nIn0sInAiOiJhYmNkZSJ9sai2pgAAAJ4AAABX/huCTws6ZXZlbnQtdHlw ZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTpt ZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQi OiI+In0sInAiOiJhYmNkIn0Lg1kzAAAAvQAAAFd4etebCzpldmVudC10eXBlBwARY29udGVudEJs b2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA BWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IlxuXG5UaGUifSwi cCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDIn2AykmuAAAAwwAAAFc+mIk1CzpldmVu dC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pz b24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsi dGV4dCI6IiBjdXJyZW50In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdI In1oIwMqAAAA2wAAAFduCFV2CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRl bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50 QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiB3ZWF0aGVyIn0sInAiOiJhYmNkZWZnaGlq a2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQ1In3jsCV7AAAA yQAAAFd0KJGUCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRl eCI6MCwiZGVsdGEiOnsidGV4dCI6IiBpbiJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4 eXpBQkNERUZHSElKS0xNTk9QUVJTIn0YWTJ3AAAAygAAAFcziOtECzpldmVudC10eXBlBwARY29u dGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2Ut dHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBTZWF0 dGxlIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OTyJ98CRA VAAAAKsAAABXl9q1uQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5 cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2Nr SW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgaXMifSwicCI6ImFiY2RlZmdoaWprbG1ubyJ97Fol SwAAAMwAAABXvMge5As6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5 cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2Nr SW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3 eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWSJ9QR39oAAAAKwAAABXJfppqQs6ZXZlbnQtdHlw ZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTpt ZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQi OiIgNSJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHEifWLzlf8AAADKAAAAVzOI60QLOmV2ZW50LXR5 cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06 bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0 IjoiMCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJT VFVWIn1h3IxOAAAAsAAAAFeA6hMqCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNv bnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250 ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBkZWdyZWVzIn0sInAiOiJhYmNkZWZn aGlqa2xtbm8ifcPVvUUAAADJAAAAV3QokZQLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0 YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7 ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGFuZCJ9LCJwIjoiYWJjZGVm Z2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVIifdysqB4AAAChAAAAV91qrRgL OmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRp b24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0 YSI6eyJ0ZXh0IjoiIGl0In0sInAiOiJhYmNkZSJ9kgjqiwAAAMwAAABXvMge5As6ZXZlbnQtdHlw ZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTpt ZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQi OiIgaXMifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFS U1RVViJ9mQSbJgAAALwAAABXRRr+Kws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29u dGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgcmFpbmluZyJ9LCJwIjoiYWJjZGVm Z2hpamtsbW5vcHFyc3R1dnd4eXpBIn3l6zxVAAAAsAAAAFeA6hMqCzpldmVudC10eXBlBwARY29u dGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2Ut dHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6Ii4ifSwi cCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXYifQIiYMcAAADcAAAAV9woiWYLOmV2ZW50LXR5cGUH ABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0Ijoi IE1lYW53aGlsZSJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xN Tk9QUVJTVFVWV1hZWjAxMjM0In1ysnhYAAAAwwAAAFc+mIk1CzpldmVudC10eXBlBwARY29udGVu dEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlw ZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiwifSwicCI6 ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PIn2IG/R6AAAAqQAAAFft GubZCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxp Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwi ZGVsdGEiOnsidGV4dCI6IiB0aGUifSwicCI6ImFiY2RlZmdoaWprbCJ9Hz4ORQAAAK0AAABXGJpA GQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNh dGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRl bHRhIjp7InRleHQiOiIgd2VhdGhlciJ9LCJwIjoiYWJjZGVmZ2hpamtsIn2ekeUoAAAApgAAAFdv SnEICzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxp Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwi ZGVsdGEiOnsidGV4dCI6IiBpbiJ9LCJwIjoiYWJjZGVmZ2hpaiJ9su1oNQAAAKkAAABX7Rrm2Qs6 ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlv bi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRh Ijp7InRleHQiOiIgU2FuIn0sInAiOiJhYmNkZWZnaGlqa2wifUQ0n7MAAADeAAAAV6bo2gYLOmV2 ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24v anNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6 eyJ0ZXh0IjoiIEZyYW5jaXNjbyJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNE RUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0NTYifekcbnsAAAC+AAAAVz/arUsLOmV2ZW50LXR5 cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06 bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0 IjoiIGlzIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdIIn1ZNtdTAAAA tgAAAFcPquaKCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRl eCI6MCwiZGVsdGEiOnsidGV4dCI6IiJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpB QkMifYaLcckAAAClAAAAVyjqC9gLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29u dGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRl bnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIDcifSwicCI6ImFiY2RlZmdoaWoiffNs CmIAAAC7AAAAV/c6IjsLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10 eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9j a0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiMCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1 dnd4eXpBQkNERUZHIn0+UYwMAAAAswAAAFfHSmn6CzpldmVudC10eXBlBwARY29udGVudEJsb2Nr RGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2 ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBkZWdyZWVzIn0sInAi OiJhYmNkZWZnaGlqa2xtbm9wcXIifRA9Z9wAAAC9AAAAV3h615sLOmV2ZW50LXR5cGUHABFjb250 ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10 eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGFuZCJ9 LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUYifX9ITaoAAADNAAAAV4GoN1QL OmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRp b24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0 YSI6eyJ0ZXh0IjoiIHN1bm55In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RF RkdISUpLTE1OT1BRUlNUIn0hiqnWAAAApwAAAFdSKli4CzpldmVudC10eXBlBwARY29udGVudEJs b2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA BWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6Ii4ifSwicCI6ImFi Y2RlZmdoaWprbG0ifa9PEvkAAADGAAAAV/Z4BkULOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tE ZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZl bnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIn0sInAiOiJhYmNkZWZn aGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlMifddocy8AAACuAAAAVig9Cl8L OmV2ZW50LXR5cGUHABBjb250ZW50QmxvY2tTdG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlv bi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsInAiOiJh YmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1AifY2eizoAAACeAAAAURd4 J3oLOmV2ZW50LXR5cGUHAAttZXNzYWdlU3RvcA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24v anNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7InAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5 ekFCQyIsInN0b3BSZWFzb24iOiJlbmRfdHVybiJ9C0lLuQAAANEAAABOQNPlFws6ZXZlbnQtdHlw ZQcACG1ldGFkYXRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5 cGUHAAVldmVudHsibWV0cmljcyI6eyJsYXRlbmN5TXMiOjQ5MH0sInAiOiJhYmNkZWZnaGlqa2xt bm8iLCJ1c2FnZSI6eyJpbnB1dFRva2VucyI6NTU4LCJvdXRwdXRUb2tlbnMiOjU3LCJ0b3RhbFRv a2VucyI6NjE1fX0yjOMb headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Wed, 26 Feb 2025 11:03:50 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked x-amzn-RequestId: - 3b6454ed-7ed2-4289-b2ef-d41a4b135f4e status: code: 200 message: OK version: 1 test_converse_stream_tool_call_parsing_errors.yaml000066400000000000000000000731021527070072300503340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Use the get_cities_list tool to provide exactly 10 popular tourist cities in Japan. Call the tool with a cities array containing: Tokyo, Osaka, Kyoto, Hiroshima, Nara, Yokohama, Sapporo, Fukuoka, Sendai, and Nagoya" } ] } ], "toolConfig": { "tools": [ { "toolSpec": { "name": "get_cities_list", "description": "Get a list of cities", "inputSchema": { "json": { "type": "object", "properties": { "cities": { "type": "array", "items": { "type": "string" } } } } } } } ] } } headers: Content-Length: - '501' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.5.0-1027-oem md/arch#x86_64 lang/python#3.12.10 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250527T075740Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-40fd504e-0a7f9803ae9c68171acef129;Parent=d9ac04fb6d83d92e;Sampled=1 amz-sdk-invocation-id: - 17b1bf3e-ee7d-40e8-864d-44f347daef4c amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-3-sonnet-20240229-v1%3A0/converse-stream response: body: string: !!binary | AAAAuQAAAFL9kIXUCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU2NyIsInJvbGUiOiJh c3Npc3RhbnQifWf51EkAAAEdAAAAV8MBm6gLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tTdGFy dA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7 ImNvbnRlbnRCbG9ja0luZGV4IjowLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNE RUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0NTY3Iiwic3RhcnQiOnsidG9vbFVzZSI6eyJuYW1l IjoiZ2V0X2NpdGllc19saXN0IiwidG9vbFVzZUlkIjoidG9vbHVzZV9GUVEyQXVvbVNXU3J5X1My N1lwUmJBIn19fdn+oFgAAADIAAAAV0lIuCQLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0 YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7 ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0b29sVXNlIjp7ImlucHV0IjoiIn19LCJw IjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSCJ9WdVhbwAAAOQAAABXTXl6IQs6 ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlv bi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRh Ijp7InRvb2xVc2UiOnsiaW5wdXQiOiJ7XCJjaXRpZSJ9fSwicCI6ImFiY2RlZmdoaWprbG1ub3Bx cnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMSJ9zQPkxwAAAOAAAABXuPnc4Qs6 ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlv bi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRh Ijp7InRvb2xVc2UiOnsiaW5wdXQiOiJzXCI6In19LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1 dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxIn0d5KkkAAAAygAAAFcziOtECzpldmVu dC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pz b24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsi dG9vbFVzZSI6eyJpbnB1dCI6IiBbXCJUb2t5b1wiLCJ9fSwicCI6ImFiY2RlZmdoaWprbG1ub3Bx cnN0dXZ3eCJ9g9C2sAAAAMgAAABXSUi4JAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRh DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Y29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRvb2xVc2UiOnsiaW5wdXQiOiIgXCJPcyJ9 fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDIn34ywPnAAAAxwAAAFfLGC/1Czpl dmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEi OnsidG9vbFVzZSI6eyJpbnB1dCI6ImFrIn19LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4 eXpBQkNERSJ9kk21DQAAAMYAAABX9ngGRQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRh DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Y29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRvb2xVc2UiOnsiaW5wdXQiOiJhXCIsIFwi S3kifX0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2dyJ9RlEuBwAAAMIAAABXA/ighQs6ZXZl bnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9q c29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7 InRvb2xVc2UiOnsiaW5wdXQiOiJvdG8ifX0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5 In1LzEvqAAAA6gAAAFfyScRACzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRl bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50 QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidG9vbFVzZSI6eyJpbnB1dCI6IlwiLCBcIkhpcm9zIn19 LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZ WjAxMjM0In2BFnqVAAAAzgAAAFfGCE2ECzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGEN OmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJj b250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidG9vbFVzZSI6eyJpbnB1dCI6ImhpbWFcIiwg XCJOYSJ9fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUIifbtH9OQAAADgAAAAV7j5 3OELOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJk ZWx0YSI6eyJ0b29sVXNlIjp7ImlucHV0IjoicmFcIiwgIn19LCJwIjoiYWJjZGVmZ2hpamtsbW5v cHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWiJ95fx/FAAAAMQAAABXjLhVJQs6 ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlv bi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRh Ijp7InRvb2xVc2UiOnsiaW5wdXQiOiJcIllvIn19LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1 dnd4eXoifTED4EIAAADCAAAAVwP4oIULOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06 Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNv bnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0b29sVXNlIjp7ImlucHV0Ijoia29oYW1hXCIi fX0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdCJ9fvF73AAAAMAAAABXeTjz5Qs6ZXZlbnQtdHlw ZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTpt ZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRvb2xV c2UiOnsiaW5wdXQiOiIsIFwiU2FwcCJ9fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxciJ9e4UQpQAA APAAAABX2BlLYws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5k ZXgiOjAsImRlbHRhIjp7InRvb2xVc2UiOnsiaW5wdXQiOiJvcm9cIiwgXCJGdWt1In19LCJwIjoi YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0 NTY3OCJ9hvUgpgAAANcAAABXq/i4dws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29u dGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRvb2xVc2UiOnsiaW5wdXQiOiJva2FcIiwgXCJT ZW4ifX0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLIn0IwD68AAAA tQAAAFdICpxaCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRl eCI6MCwiZGVsdGEiOnsidG9vbFVzZSI6eyJpbnB1dCI6ImRhaVwiLCAifX0sInAiOiJhYmNkZWZn aCJ9TpCuSgAAANIAAABXYxg3Bws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250 ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVu dEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRvb2xVc2UiOnsiaW5wdXQiOiJcIk5hZyJ9fSwicCI6 ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTSJ9e9kAbwAAALQAAABXdWq1 6gs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNh dGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRl bHRhIjp7InRvb2xVc2UiOnsiaW5wdXQiOiJveWFcIl19In19LCJwIjoiYWJjZGVmZyJ9DSK/iQAA AL0AAABWD33nDQs6ZXZlbnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0b3ANOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRl eCI6MCwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RV VldYWVowMTIzNCJ9XgX4bgAAAJIAAABR0ojKews6ZXZlbnQtdHlwZQcAC21lc3NhZ2VTdG9wDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6 ImFiY2RlZmdoaWprbG1ub3BxIiwic3RvcFJlYXNvbiI6InRvb2xfdXNlIn1Xajn2AAAA4AAAAE7c knQhCzpldmVudC10eXBlBwAIbWV0YWRhdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pz b24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJtZXRyaWNzIjp7ImxhdGVuY3lNcyI6MjQ5OX0sInAi OiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQyIsInVzYWdlIjp7ImlucHV0VG9rZW5zIjoy OTUsIm91dHB1dFRva2VucyI6NDMsInRvdGFsVG9rZW5zIjozMzh9fRnrM38= headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Tue, 27 May 2025 07:57:40 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked x-amzn-RequestId: - ec25d949-bde0-4ed6-9fd5-4fe595818b33 status: code: 200 message: OK - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Use the get_cities_list tool to provide exactly 10 popular tourist cities in Japan. Call the tool with a cities array containing: Tokyo, Osaka, Kyoto, Hiroshima, Nara, Yokohama, Sapporo, Fukuoka, Sendai, and Nagoya" } ] }, { "content": [ { "toolUse": { "toolUseId": "tooluse_FQQ2AuomSWSry_S27YpRbA", "name": "get_cities_list", "input": { "cities": [ "Tokyo", "Osaka", "Kyoto", "Hiroshima", "Nara", "Yokohama", "Sapporo", "Fukuoka", "Sendai", "Nagoya" ] } } } ], "role": "assistant" }, { "role": "user", "content": [ { "toolResult": { "content": [ { "json": { "weather": "50 degrees and raining" } } ], "toolUseId": "tooluse_FQQ2AuomSWSry_S27YpRbA" } } ] } ], "toolConfig": { "tools": [ { "toolSpec": { "name": "get_cities_list", "description": "Get a list of cities", "inputSchema": { "json": { "type": "object", "properties": { "cities": { "type": "array", "items": { "type": "string" } } } } } } } ] } } headers: Content-Length: - '909' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.5.0-1027-oem md/arch#x86_64 lang/python#3.12.10 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250527T075743Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-7ab9a0ae-91ddf412dcd8f99f70e7ddb4;Parent=b30d0a82216082c0;Sampled=1 amz-sdk-invocation-id: - 89524564-fea4-4b45-bc04-4ad904a7daa8 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-3-sonnet-20240229-v1%3A0/converse-stream response: body: string: !!binary | AAAAjAAAAFKUUbIiCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u byIsInJvbGUiOiJhc3Npc3RhbnQifaICnXQAAACoAAAAV9B6z2kLOmV2ZW50LXR5cGUHABFjb250 ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10 eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiXG5cblxu VGhlIn0sInAiOiJhYmNkZWYifcmnOXYAAADPAAAAV/toZDQLOmV2ZW50LXR5cGUHABFjb250ZW50 QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBl BwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHRvb2wifSwi cCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVlcifSnR MFsAAAC6AAAAV8paC4sLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10 eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9j a0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHByb3ZpZGUifSwicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHkifS4e8OYAAADSAAAAV2MYNwcLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tE ZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZl bnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiZCBhIn0sInAiOiJhYmNk ZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEifWrItfsA AADNAAAAV4GoN1QLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBl BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0lu ZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHJlc3VsdCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFy c3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTIn0+mQXdAAAAvAAAAFdFGv4rCzpldmVudC10eXBl BwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1l c3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6 IiBhYm91dCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkMifVWb+5MAAADNAAAA V4GoN1QLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBw bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4Ijow LCJkZWx0YSI6eyJ0ZXh0IjoiIHRoZSJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpB QkNERUZHSElKS0xNTk9QUVJTVFVWIn0N6sYQAAAAywAAAFcO6ML0CzpldmVudC10eXBlBwARY29u dGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2Ut dHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiB3ZWF0 aGVyIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1AifdjX xU0AAADSAAAAV2MYNwcLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10 eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9j a0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiLCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1 dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjMifW3ESEgAAADFAAAAV7HYfJULOmV2 ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24v anNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6 eyJ0ZXh0IjoiIHdoaWNoIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdI SUpLTCJ9xXbFpwAAAKAAAABX4AqEqAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29u dGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgd2FzIn0sInAiOiJhYmMifSIb+YgA AAC6AAAAV8paC4sLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBl BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0lu ZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIG5vdCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1 dnd4eXpBQkMifVnIk6cAAADMAAAAV7zIHuQLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0 YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7 ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHdoYXQifSwicCI6ImFiY2Rl ZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1QifRLfkq4AAACzAAAAV8dK afoLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJk ZWx0YSI6eyJ0ZXh0IjoiIEkifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eCJ9NIiywQAA AMIAAABXA/ighQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5k ZXgiOjAsImRlbHRhIjp7InRleHQiOiIgYXNrZSJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1 dnd4eXpBQkNERUZHSElKIn1VafXeAAAAugAAAFfKWguLCzpldmVudC10eXBlBwARY29udGVudEJs b2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA BWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6ImQgZm9yIn0sInAi OiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCIn2NlGQwAAAAxAAAAFeMuFUlCzpldmVudC10 eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24N Om1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4 dCI6Ii4ifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUCJ9 jg+H5AAAAMIAAABXA/ighQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50 LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJs b2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgSSJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFy c3R1dnd4eXpBQkNERUZHSElKS0xNIn2TvlBiAAAAzwAAAFf7aGQ0CzpldmVudC10eXBlBwARY29u dGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2Ut dHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiByZXF1 ZXN0ZSJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJT In1rb9iIAAAAygAAAFcziOtECzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRl bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50 QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6ImQgYSJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5v cHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVCJ9FYUuFgAAAKkAAABX7Rrm2Qs6ZXZlbnQt dHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRl eHQiOiIgbGlzdCJ9LCJwIjoiYWJjZGVmZ2hpamsifT7jL+YAAADMAAAAV7zIHuQLOmV2ZW50LXR5 cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06 bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0 IjoiIG9mIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BR UlNUVVYifX8G4uYAAADMAAAAV7zIHuQLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06 Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNv bnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiICJ9LCJwIjoiYWJjZGVmZ2hpamts bW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1giffE1KNUAAACeAAAAV/4bgk8L OmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRp b24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0 YSI6eyJ0ZXh0IjoiMTAifSwicCI6ImFiYyJ9ajAJDgAAAMAAAABXeTjz5Qs6ZXZlbnQtdHlwZQcA EWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIg In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTCJ94H5CdwAAAM4A AABXxghNhAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgi OjAsImRlbHRhIjp7InRleHQiOiJwb3B1bGFyIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2 d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUIn3ctEStAAAAyQAAAFd0KJGUCzpldmVudC10eXBlBwAR Y29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh Z2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiB0 b3VyaXN0In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OIn1k 0gvYAAAApgAAAFdvSnEICzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50Qmxv Y2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBjaXRpZXMifSwicCI6ImFiY2RlZiJ9p13vhwAA AMcAAABXyxgv9Qs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5k ZXgiOjAsImRlbHRhIjp7InRleHQiOiIgaW4ifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3 eHl6QUJDREVGR0hJSktMTU5PUFEiferofRoAAADCAAAAVwP4oIULOmV2ZW50LXR5cGUHABFjb250 ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10 eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIEphcGFu In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISSJ96UzwGQAAALEAAABX vYo6mgs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBs aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAs ImRlbHRhIjp7InRleHQiOiIuIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2dyJ9vuJWlgAA AL4AAABXP9qtSws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5k ZXgiOjAsImRlbHRhIjp7InRleHQiOiIgTGV0In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2 d3h5ekFCQ0RFRkcifSEOoZMAAAC5AAAAV436cVsLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tE ZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZl bnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIG1lIn0sInAiOiJhYmNk ZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQyJ9c/GzWwAAAMcAAABXyxgv9Qs6ZXZlbnQtdHlwZQcA EWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIg dHJ5In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1AifWrP s8cAAAC+AAAAVz/arUsLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10 eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9j a0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGFnYWluIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9w cXJzdHV2d3h5ekFCQ0RFIn35veqsAAAAnAAAAFeE29EvCzpldmVudC10eXBlBwARY29udGVudEJs b2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA BWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IjoifSwicCI6ImFi In35l6xsAAAAswAAAFawTVlsCzpldmVudC10eXBlBwAQY29udGVudEJsb2NrU3RvcA06Y29udGVu dC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRC bG9ja0luZGV4IjowLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xN Tk9QUVJTVFUifZxSy0MAAAD9AAAAVyCJj9ILOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tTdGFy dA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7 ImNvbnRlbnRCbG9ja0luZGV4IjoxLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQiIs InN0YXJ0Ijp7InRvb2xVc2UiOnsibmFtZSI6ImdldF9jaXRpZXNfbGlzdCIsInRvb2xVc2VJZCI6 InRvb2x1c2Vfd2wtSHR5OFVSNFcySW9uTGp0b05idyJ9fX20oMySAAAAzwAAAFf7aGQ0CzpldmVu dC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pz b24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MSwiZGVsdGEiOnsi dG9vbFVzZSI6eyJpbnB1dCI6IiJ9fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJD REVGR0hJSktMTU5PIn0hW8dYAAAA6gAAAFfyScRACzpldmVudC10eXBlBwARY29udGVudEJsb2Nr RGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2 ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MSwiZGVsdGEiOnsidG9vbFVzZSI6eyJpbnB1dCI6Intc ImNpdGllc1wiOiAifX0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpL TE1OT1BRUlNUVVZXWFlaMDEyIn1Kko1kAAAAuQAAAFeN+nFbCzpldmVudC10eXBlBwARY29udGVu dEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlw ZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MSwiZGVsdGEiOnsidG9vbFVzZSI6eyJpbnB1 dCI6IltcIlRva3lvXCIsIFwiTyJ9fSwicCI6ImFiY2QifX34RLYAAADdAAAAV+FIoNYLOmV2ZW50 LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjoxLCJkZWx0YSI6eyJ0 b29sVXNlIjp7ImlucHV0Ijoic2FrYVwiLCBcIiJ9fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0 dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSUyJ9pM0nZQAAAMsAAABXDujC9As6ZXZlbnQtdHlwZQcA EWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjEsImRlbHRhIjp7InRvb2xVc2Ui OnsiaW5wdXQiOiJLeSJ9fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJ In1XArYAAAAAwQAAAFdEWNpVCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRl bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50 QmxvY2tJbmRleCI6MSwiZGVsdGEiOnsidG9vbFVzZSI6eyJpbnB1dCI6Im90b1wiLCBcIkhpciJ9 fSwicCI6ImFiY2RlZmdoaWprbG1ubyJ9jXaZzAAAAN4AAABXpujaBgs6ZXZlbnQtdHlwZQcAEWNv bnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdl LXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjEsImRlbHRhIjp7InRvb2xVc2UiOnsi aW5wdXQiOiJvc2hpbSJ9fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJ SktMTU5PUFFSU1RVVldYWSJ9dPprXAAAAMcAAABXyxgv9Qs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRC bG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUH AAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjEsImRlbHRhIjp7InRvb2xVc2UiOnsiaW5wdXQi OiJhXCIsIFwiTmFyYVwiLCAifX0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXIifQvLhiUAAADfAAAA V5uI87YLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBw bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4Ijox LCJkZWx0YSI6eyJ0b29sVXNlIjp7ImlucHV0IjoiXCJZb2sifX0sInAiOiJhYmNkZWZnaGlqa2xt bm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaIn3jrYUsAAAAsgAAAFf6KkBK CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MSwiZGVs dGEiOnsidG9vbFVzZSI6eyJpbnB1dCI6Im9oYW1hXCIsICJ9fSwicCI6ImFiYyJ9/Kcm3wAAALYA AABXD6rmigs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgi OjEsImRlbHRhIjp7InRvb2xVc2UiOnsiaW5wdXQiOiJcIlNhcHBvciJ9fSwicCI6ImFiY2RlZmdo In2qxVY2AAAAwgAAAFcD+KCFCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRl bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50 QmxvY2tJbmRleCI6MSwiZGVsdGEiOnsidG9vbFVzZSI6eyJpbnB1dCI6Im9cIiwifX0sInAiOiJh YmNkZWZnaGlqa2xtbm9wcXJzdHV2d3gifY16k2AAAADfAAAAV5uI87YLOmV2ZW50LXR5cGUHABFj b250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjoxLCJkZWx0YSI6eyJ0b29sVXNlIjp7 ImlucHV0IjoiIFwiRnVrdW8ifX0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RF RkdISUpLTE1OT1BRUlNUVVZXIn37eomFAAAA3gAAAFem6NoGCzpldmVudC10eXBlBwARY29udGVu dEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlw ZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MSwiZGVsdGEiOnsidG9vbFVzZSI6eyJpbnB1 dCI6ImthXCIsICJ9fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktM TU5PUFFSU1RVVldYIn0pqNFzAAAA3wAAAFebiPO2CzpldmVudC10eXBlBwARY29udGVudEJsb2Nr RGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2 ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MSwiZGVsdGEiOnsidG9vbFVzZSI6eyJpbnB1dCI6Ilwi U2VuZGFpIn19LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9Q UVJTVFVWVyJ9tB85SgAAAM8AAABX+2hkNAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRh DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Y29udGVudEJsb2NrSW5kZXgiOjEsImRlbHRhIjp7InRvb2xVc2UiOnsiaW5wdXQiOiJcIiwgXCJO YWdveWFcIiJ9fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QSJ9pptI1AAAANsAAABX bghVdgs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBs aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjEs ImRlbHRhIjp7InRvb2xVc2UiOnsiaW5wdXQiOiJdfSJ9fSwicCI6ImFiY2RlZmdoaWprbG1ub3Bx cnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWSJ9vZnRIwAAAIkAAABWW9z5Sws6ZXZl bnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pz b24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MSwicCI6ImFiY2Rl In16ExHZAAAAjQAAAFEwOMooCzpldmVudC10eXBlBwALbWVzc2FnZVN0b3ANOmNvbnRlbnQtdHlw ZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJwIjoiYWJjZGVmZ2hp amtsIiwic3RvcFJlYXNvbiI6InRvb2xfdXNlIn3sr3SgAAAA7wAAAE5ewuPwCzpldmVudC10eXBl BwAIbWV0YWRhdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlw ZQcABWV2ZW50eyJtZXRyaWNzIjp7ImxhdGVuY3lNcyI6MzA4OX0sInAiOiJhYmNkZWZnaGlqa2xt bm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUiIsInVzYWdlIjp7ImlucHV0VG9rZW5zIjo0 MjIsIm91dHB1dFRva2VucyI6ODIsInRvdGFsVG9rZW5zIjo1MDR9fcCbTkU= headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Tue, 27 May 2025 07:57:43 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked x-amzn-RequestId: - 5d12cdb0-59b9-493a-b41d-4d236934f15d status: code: 200 message: OK version: 1 test_converse_stream_with_content.yaml000066400000000000000000000054231527070072300457530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] } ], "inferenceConfig": { "maxTokens": 10, "temperature": 0.8, "topP": 1, "stopSequences": [ "|" ] } } headers: Content-Length: - '170' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.0.0 md/arch#arm64 lang/python#3.13.1 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250206T051233Z X-Amzn-Trace-Id: - Root=1-6c333b46-841c20f5fad291f66e0ff261;Parent=da80a23d1ac23800;Sampled=1 amz-sdk-invocation-id: - cb361c76-ead1-4ad2-a866-eb674ceb560a amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.titan-text-lite-v1/converse-stream response: body: string: !!binary | AAAAkAAAAFIxQcihCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u b3BxcnMiLCJyb2xlIjoiYXNzaXN0YW50In0RduqwAAAA2wAAAFduCFV2CzpldmVudC10eXBlBwAR Y29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh Z2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6Ikkg YW0gaGVyZSBhbmQgcmVhZHkgdG8gYXNzaXN0In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2 d3h5ekFCQ0RFRkdISUpLIn0uPEHpAAAAkAAAAFY2LAy4CzpldmVudC10eXBlBwAQY29udGVudEJs b2NrU3RvcA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJwIjoiYWJjZGVmZ2hpamtsIn3C2aZeAAAAjgAA AFF3mLD4CzpldmVudC10eXBlBwALbWVzc2FnZVN0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJwIjoiYWJjZGVmZ2hpamsiLCJzdG9wUmVh c29uIjoibWF4X3Rva2VucyJ9cjPl1AAAAMwAAABO2KO2JAs6ZXZlbnQtdHlwZQcACG1ldGFkYXRh DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi bWV0cmljcyI6eyJsYXRlbmN5TXMiOjYwNX0sInAiOiJhYmNkZWZnaGlqa2xtIiwidXNhZ2UiOnsi aW5wdXRUb2tlbnMiOjgsIm91dHB1dFRva2VucyI6MTAsInRvdGFsVG9rZW5zIjoxOH19/X5xsw== headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Thu, 06 Feb 2025 05:12:34 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked x-amzn-RequestId: - 77f2fab1-08a9-4235-a402-e0e5f3a5e117 status: code: 200 message: OK version: 1 test_converse_stream_with_content_different_events.yaml000066400000000000000000000101521527070072300513600ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "system": [ { "text": "You are a friendly app" } ], "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] }, { "role": "assistant", "content": [ { "text": "This is a test" } ] }, { "role": "user", "content": [ { "text": "Say again this is a test" } ] } ] } headers: Content-Length: - '258' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250213T134611Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-deb34c78-8ab87e14665141b9e132396b;Parent=8e997adc8051f5a6;Sampled=1 amz-sdk-invocation-id: - 81e436d8-7c5d-4025-a9e5-de505ccb9b51 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-v2/converse-stream response: body: string: !!binary | AAAArAAAAFJVkJ0mCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVIiwicm9sZSI6ImFzc2lzdGFudCJ9mBqg YwAAANQAAABX7FjCpws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5 cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2Nr SW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiJUaGlzIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJz dHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyIn2n2T4pAAAArQAAAFcYmkAZCzpl dmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEi OnsidGV4dCI6IiBpcyJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHEifYkheRMAAADQAAAAVxnYZGcL OmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRp b24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0 YSI6eyJ0ZXh0IjoiIGEifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJ SktMTU5PUFFSU1RVVldYWVowIn3K4xpaAAAA0wAAAFdeeB63CzpldmVudC10eXBlBwARY29udGVu dEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlw ZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiB0ZXN0In0s InAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFla MCJ916eYywAAALkAAABW+v1BzQs6ZXZlbnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0b3ANOmNvbnRl bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50 QmxvY2tJbmRleCI6MCwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktM TU5PUFFSU1RVVldYWVowIn2IAETUAAAAhgAAAFFH6Ps5CzpldmVudC10eXBlBwALbWVzc2FnZVN0 b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 eyJwIjoiYWJjZGUiLCJzdG9wUmVhc29uIjoiZW5kX3R1cm4ifSknYaEAAAD3AAAATg5SP7MLOmV2 ZW50LXR5cGUHAAhtZXRhZGF0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7Im1ldHJpY3MiOnsibGF0ZW5jeU1zIjo1MDh9LCJwIjoiYWJjZGVm Z2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjMiLCJ1c2Fn ZSI6eyJpbnB1dFRva2VucyI6MzcsIm91dHB1dFRva2VucyI6OCwidG90YWxUb2tlbnMiOjQ1fX0o MQhc headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Thu, 13 Feb 2025 13:46:12 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked x-amzn-RequestId: - 829dbcea-d3df-4781-be84-c1c2d5c420bd status: code: 200 message: OK version: 1 test_converse_stream_with_content_tool_call.yaml000066400000000000000000001203671527070072300500100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today?" } ] } ], "toolConfig": { "tools": [ { "toolSpec": { "name": "get_current_weather", "description": "Get the current weather in a given location.", "inputSchema": { "json": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } } } ] } } headers: Content-Length: - '409' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250226T102738Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-04f71bd4-d970fcc14c74e92835e6d859;Parent=d9e4bf8f7e1cd23c;Sampled=1 amz-sdk-invocation-id: - 637b9c2c-3732-4754-9cc8-fd5eabb69254 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse-stream response: body: string: !!binary | AAAAiwAAAFImcW4yCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u Iiwicm9sZSI6ImFzc2lzdGFudCJ9uwonDAAAANoAAABXU2h8xgs6ZXZlbnQtdHlwZQcAEWNvbnRl bnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5 cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiI8dGhpbmtp bmcifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RV VldYWVowMTIzIn0WU72dAAAAnwAAAFfDe6v/CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVs dGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6Ij4ifSwicCI6ImFiY2RlIn1R kT4TAAAAtAAAAFd1arXqCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50Qmxv Y2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBUaGUifSwicCI6ImFiY2RlZmdoaWprbG1ub3Bx cnN0dXZ3In3lkCKbAAAAuAAAAFewmljrCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGEN OmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJj b250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBVc2VyIn0sInAiOiJhYmNkZWZn aGlqa2xtbm9wcXJzdHV2d3h5eiJ92v8R0gAAAM4AAABXxghNhAs6ZXZlbnQtdHlwZQcAEWNvbnRl bnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5 cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgaGFzIn0s InAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXIn0Q UxZqAAAApAAAAFcViiJoCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50Qmxv Y2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBhc2tlZCJ9LCJwIjoiYWJjZGUifeApkbkAAADP AAAAV/toZDQLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQ YXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4 IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGZvciJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4 eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1gifcub8LwAAAC1AAAAV0gKnFoLOmV2ZW50LXR5cGUH ABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0Ijoi IHRoZSJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4In2wiMtzAAAArQAAAFcYmkAZCzpl dmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEi OnsidGV4dCI6IiBjdXJyZW50In0sInAiOiJhYmNkZWZnaGlqa2wifZOyLqUAAADFAAAAV7HYfJUL OmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRp b24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0 YSI6eyJ0ZXh0IjoiIHdlYXRoZXIifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJD REVGR0hJSiJ9+qOWLgAAAMoAAABXM4jrRAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRh DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Y29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgaW4ifSwicCI6ImFiY2RlZmdo aWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1QifRtVX4oAAADSAAAAV2MYNwcL OmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRp b24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0 YSI6eyJ0ZXh0IjoiIHR3byJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZH SElKS0xNTk9QUVJTVFVWV1hZWjAifTxfXF0AAACqAAAAV6q6nAkLOmV2ZW50LXR5cGUHABFjb250 ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10 eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGRpZmZl cmVudCJ9LCJwIjoiYWJjZGVmZyJ9WEVw5gAAALsAAABX9zoiOws6ZXZlbnQtdHlwZQcAEWNvbnRl bnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5 cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgY2l0aWVz In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekEifb1HzFAAAADNAAAAV4GoN1QLOmV2 ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24v anNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6 eyJ0ZXh0IjoiOiJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xN Tk9QUVJTVFVWV1hZIn0Yp11YAAAA0AAAAFcZ2GRnCzpldmVudC10eXBlBwARY29udGVudEJsb2Nr RGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2 ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBTZWF0dGxlIn0sInAi OiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVSJ9ieDPBgAA AMgAAABXSUi4JAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5k ZXgiOjAsImRlbHRhIjp7InRleHQiOiIgYW5kIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2 d3h5ekFCQ0RFRkdISUpLTE1OT1BRIn2bOp/QAAAAzgAAAFfGCE2ECzpldmVudC10eXBlBwARY29u dGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2Ut dHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBTYW4i fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVlci fRghTtsAAADAAAAAV3k48+ULOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVu dC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRC bG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIEZyYW5jaXNjbyJ9LCJwIjoiYWJjZGVmZ2hp amtsbW5vcHFyc3R1dnd4eXpBQkMifX3m7KMAAADAAAAAV3k48+ULOmV2ZW50LXR5cGUHABFjb250 ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10 eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiLiJ9LCJw IjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0wifdSrwYEAAAC+AAAAVz/a rUsLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJk ZWx0YSI6eyJ0ZXh0IjoiIFRvIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RF RkdIIn3kcEvQAAAAxAAAAFeMuFUlCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNv bnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250 ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBwcm92aWRlIn0sInAiOiJhYmNkZWZn aGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISSJ9/KQeHgAAALgAAABXsJpY6ws6ZXZlbnQtdHlw ZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTpt ZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQi OiIgdGhpcyJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoifdLTdowAAADcAAAAV9wo iWYLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJk ZWx0YSI6eyJ0ZXh0IjoiIGluZm9ybWF0aW9uIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2 d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyIn15+KG9AAAAzQAAAFeBqDdUCzpldmVu dC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pz b24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsi dGV4dCI6IiwifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5P UFFSU1RVVldYWSJ9oHw2NQAAAKUAAABXKOoL2As6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0Rl bHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu dHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgSSJ9LCJwIjoiYWJjZGVm Z2hpaiJ9CG58FwAAAMIAAABXA/ighQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29u dGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgd2lsbCJ9LCJwIjoiYWJjZGVmZ2hp amtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKIn0N3iz4AAAAsAAAAFeA6hMqCzpldmVudC10eXBl BwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1l c3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6 IiB1c2UifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnMifWaf5q8AAACfAAAAV8N7q/8LOmV2ZW50 LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0 ZXh0IjoiIHRoZSJ9LCJwIjoiYWIiffQkFPIAAACuAAAAV186OskLOmV2ZW50LXR5cGUHABFjb250 ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10 eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGAifSwi cCI6ImFiY2RlZmdoaWprbG1ub3BxcnMifQeVw34AAAClAAAAVyjqC9gLOmV2ZW50LXR5cGUHABFj b250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiZ2V0 In0sInAiOiJhYmNkZWZnaGkiffirkXQAAACeAAAAV/4bgk8LOmV2ZW50LXR5cGUHABFjb250ZW50 QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBl BwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiXyJ9LCJwIjoi YWJjZCJ98EbHOAAAANkAAABXFMgGFgs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29u dGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiJjdXJyZW50In0sInAiOiJhYmNkZWZn aGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQifS00KhYA AACvAAAAV2JaE3kLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBl BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0lu ZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiXyJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1In2W Rz5sAAAA0AAAAFcZ2GRnCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50Qmxv Y2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IndlYXRoZXIifSwicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVViJ9g/IirAAAAMkAAABXdCiRlAs6ZXZl bnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9q c29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7 InRleHQiOiJgIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1O T1BRUlNUVSJ9uJpkUQAAAMYAAABX9ngGRQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRh DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Y29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgdG9vbCJ9LCJwIjoiYWJjZGVm Z2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTiJ91v3JAwAAANIAAABXYxg3Bws6ZXZl bnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9q c29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7 InRleHQiOiIgZm9yIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpL TE1OT1BRUlNUVVZXWFlaMCJ9t05fvwAAALUAAABXSAqcWgs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRC bG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUH AAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgZWFjaCJ9LCJw IjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dncifYp34wcAAADQAAAAVxnYZGcLOmV2ZW50LXR5cGUH ABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0Ijoi IGNpdHkifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFS U1RVVldYIn1Nf0rRAAAAwQAAAFdEWNpVCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGEN OmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJj b250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6Ii4ifSwicCI6ImFiY2RlZmdoaWpr bG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTSJ9gJPMLgAAAMAAAABXeTjz5Qs6ZXZlbnQtdHlw ZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTpt ZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQi OiIgSSJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKSyJ9XT574QAA AKMAAABXp6r+eAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5k ZXgiOjAsImRlbHRhIjp7InRleHQiOiIgbmVlZCJ9LCJwIjoiYWJjZGUifaf+7WgAAACqAAAAV6q6 nAkLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJk ZWx0YSI6eyJ0ZXh0IjoiIHRvIn0sInAiOiJhYmNkZWZnaGlqa2xtbiJ9f7sOGgAAALQAAABXdWq1 6gs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNh dGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRl bHRhIjp7InRleHQiOiIgY2FsbCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1diJ9I2TKjwAA AJ8AAABXw3ur/ws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5k ZXgiOjAsImRlbHRhIjp7InRleHQiOiIgdGhlIn0sInAiOiJhYiJ99CQU8gAAAKYAAABXb0pxCAs6 ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlv bi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRh Ijp7InRleHQiOiIgdG9vbCJ9LCJwIjoiYWJjZGVmZ2giffRTbBMAAADGAAAAV/Z4BkULOmV2ZW50 LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0 ZXh0IjoiIHR3aWNlIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpL TE0ifVxjFU4AAADBAAAAV0RY2lULOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29u dGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRl bnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiLCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5v cHFyc3R1dnd4eXpBQkNERUZHSElKS0xNIn1TgPmNAAAAzAAAAFe8yB7kCzpldmVudC10eXBlBwAR Y29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh Z2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBv bmNlIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNU In2Z9N6MAAAAygAAAFcziOtECzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRl bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50 QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBmb3IifSwicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSUyJ9UmDMdgAAAM0AAABXgag3VAs6ZXZlbnQt dHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRl eHQiOiIgZWFjaCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xN Tk9QUVJTVFUifWVeCiEAAACsAAAAVyX6aakLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0 YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7 ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGNpdHkifSwicCI6ImFiY2Rl ZmdoaWprbG1uIn1t18bGAAAA1wAAAFer+Lh3CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVs dGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6Ii48LyJ9LCJwIjoiYWJjZGVm Z2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0NTYifeXm GQoAAADcAAAAV9woiWYLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10 eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9j a0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoidGhpbmtpbmcifSwicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU2In2leF3tAAAAuQAA AFeN+nFbCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFw cGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6 MCwiZGVsdGEiOnsidGV4dCI6Ij4ifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJD REUifXAgJvgAAADBAAAAV0RY2lULOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29u dGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRl bnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiXG4ifSwicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMIn1xMIUhAAAAswAAAFawTVlsCzpldmVudC10eXBlBwAQ Y29udGVudEJsb2NrU3RvcA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJwIjoiYWJjZGVmZ2hpamtsbW5v cHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFUifZxSy0MAAADtAAAAV0BpGFALOmV2ZW50 LXR5cGUHABFjb250ZW50QmxvY2tTdGFydA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjoxLCJwIjoiYWJjZGVm Z2giLCJzdGFydCI6eyJ0b29sVXNlIjp7Im5hbWUiOiJnZXRfY3VycmVudF93ZWF0aGVyIiwidG9v bFVzZUlkIjoidG9vbHVzZV9KWjExUWN4U1EzbTN4YWNNUUtWSUt3In19fWV83eAAAADTAAAAV154 HrcLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjoxLCJk ZWx0YSI6eyJ0b29sVXNlIjp7ImlucHV0Ijoie1wibG9jYXRpb25cIjpcIlNlYXR0bGVcIn0ifX0s InAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzIn2/MyvDAAAAqgAAAFbdvayfCzpldmVudC10eXBlBwAQ Y29udGVudEJsb2NrU3RvcA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjoxLCJwIjoiYWJjZGVmZ2hpamtsbW5v cHFyc3R1dnd4eXpBQkNERUZHSElKS0wifW3atmwAAAEYAAAAVwvhFNgLOmV2ZW50LXR5cGUHABFj b250ZW50QmxvY2tTdGFydA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjoyLCJwIjoiYWJjZGVmZ2hpamtsbW5v cHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZIiwic3RhcnQiOnsidG9vbFVzZSI6 eyJuYW1lIjoiZ2V0X2N1cnJlbnRfd2VhdGhlciIsInRvb2xVc2VJZCI6InRvb2x1c2VfLWh4QkVF d0dSYy1WUXFDMmk3U0ZxZyJ9fX0Pxz0FAAAA3QAAAFfhSKDWCzpldmVudC10eXBlBwARY29udGVu dEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlw ZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MiwiZGVsdGEiOnsidG9vbFVzZSI6eyJpbnB1 dCI6IntcImxvY2F0aW9uXCI6XCJTYW4gRnJhbmNpc2NvXCJ9In19LCJwIjoiYWJjZGVmZ2hpamts bW5vcHFyc3R1dncifYiO7DYAAADAAAAAVg4/w3MLOmV2ZW50LXR5cGUHABBjb250ZW50QmxvY2tT dG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu dHsiY29udGVudEJsb2NrSW5kZXgiOjIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFC Q0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQ1NjcifWGPLHgAAACtAAAAUfH55SwLOmV2ZW50 LXR5cGUHAAttZXNzYWdlU3RvcA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7InAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdI SUpLTE1OT1BRUiIsInN0b3BSZWFzb24iOiJ0b29sX3VzZSJ9GccuEgAAANcAAABOz5MQtws6ZXZl bnQtdHlwZQcACG1ldGFkYXRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsibWV0cmljcyI6eyJsYXRlbmN5TXMiOjYyNX0sInAiOiJhYmNkZWZn aGlqa2xtbm9wcXJzdCIsInVzYWdlIjp7ImlucHV0VG9rZW5zIjo0MTUsIm91dHB1dFRva2VucyI6 MjAyLCJ0b3RhbFRva2VucyI6NjE3fX0EzYWg headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Wed, 26 Feb 2025 10:27:39 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked x-amzn-RequestId: - 74f31869-e1ef-4224-b9d8-3f143d117f77 status: code: 200 message: OK - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today?" } ] }, { "content": [ { "text": " The User has asked for the current weather in two different cities: Seattle and San Francisco. To provide this information, I will use the `get_current_weather` tool for each city. I need to call the tool twice, once for each city.\n" }, { "toolUse": { "toolUseId": "tooluse_JZ11QcxSQ3m3xacMQKVIKw", "name": "get_current_weather", "input": { "location": "Seattle" } } }, { "toolUse": { "toolUseId": "tooluse_-hxBEEwGRc-VQqC2i7SFqg", "name": "get_current_weather", "input": { "location": "San Francisco" } } } ], "role": "assistant" }, { "role": "user", "content": [ { "toolResult": { "content": [ { "json": { "weather": "50 degrees and raining" } } ], "toolUseId": "tooluse_JZ11QcxSQ3m3xacMQKVIKw" } }, { "toolResult": { "content": [ { "json": { "weather": "70 degrees and sunny" } } ], "toolUseId": "tooluse_-hxBEEwGRc-VQqC2i7SFqg" } } ] } ], "toolConfig": { "tools": [ { "toolSpec": { "name": "get_current_weather", "description": "Get the current weather in a given location.", "inputSchema": { "json": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } } } ] } } headers: Content-Length: - '1257' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250226T102739Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-c39757fc-71ea72a4be94c71dd8ad5f02;Parent=5311711eae0d4922;Sampled=1 amz-sdk-invocation-id: - 9dc7d733-7c64-49ff-97eb-268cd9631654 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse-stream response: body: string: !!binary | AAAAtgAAAFJ/wBIFCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNCIsInJvbGUiOiJhc3Np c3RhbnQifURlAvAAAADXAAAAV6v4uHcLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06 Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNv bnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiPHRoaW5raW5nIn0sInAiOiJhYmNk ZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMCJ9tJ4pEQAA AKMAAABXp6r+eAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5k ZXgiOjAsImRlbHRhIjp7InRleHQiOiI+In0sInAiOiJhYmNkZWZnaGkifdHdiHwAAAC8AAAAV0Ua /isLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJk ZWx0YSI6eyJ0ZXh0IjoiIEkifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVG RyJ9AVfFzgAAANUAAABX0TjrFws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250 ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVu dEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgaGF2ZSJ9LCJwIjoiYWJjZGVmZ2hpamts bW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMiJ9V2u+hgAAANAAAABX GdhkZws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBs aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAs ImRlbHRhIjp7InRleHQiOiIgcmVjZWl2ZWQifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3 eHl6QUJDREVGR0hJSktMTU5PUFFSU1Qiff6IOc0AAADXAAAAV6v4uHcLOmV2ZW50LXR5cGUHABFj b250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHRo ZSJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVW V1hZWjAxMjM0NSJ9dL4DuwAAAKYAAABXb0pxCAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0Rl bHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu dHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgd2VhdGhlciJ9LCJwIjoi YWJjZGUifR3OSxMAAACrAAAAV5fatbkLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06 Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNv bnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGluZm9ybWF0aW9uIn0sInAiOiJh YmNkZWYifUzlQlUAAACsAAAAVyX6aakLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06 Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNv bnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGZvciJ9LCJwIjoiYWJjZGVmZ2hp amtsbW5vIn33KxbQAAAAzwAAAFf7aGQ0CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGEN OmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJj b250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBib3RoIn0sInAiOiJhYmNkZWZn aGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXIn3oCbikAAAAqQAAAFft GubZCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxp Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwi ZGVsdGEiOnsidGV4dCI6IiBjaXRpZXMifSwicCI6ImFiY2RlZmdoaSJ98FCVowAAAJwAAABXhNvR Lws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNh dGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRl bHRhIjp7InRleHQiOiIuIn0sInAiOiJhYiJ9jSlgIAAAAM4AAABXxghNhAs6ZXZlbnQtdHlwZQcA EWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIg Tm93In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNU VVZXIn2voOsQAAAAxQAAAFex2HyVCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNv bnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250 ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBJIn0sInAiOiJhYmNkZWZnaGlqa2xt bm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1AifaUUZNwAAADNAAAAV4GoN1QLOmV2ZW50LXR5 cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06 bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0 IjoiIHdpbGwifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5P UFFSU1RVIn0GhzUHAAAApgAAAFdvSnEICzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGEN OmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJj b250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBwcm92aWRlIn0sInAiOiJhYmNk ZSJ9RHIXOAAAAMYAAABX9ngGRQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250 ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVu dEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgdGhlIn0sInAiOiJhYmNkZWZnaGlqa2xt bm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OTyJ9joUcYgAAAKQAAABXFYoiaAs6ZXZlbnQtdHlw ZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTpt ZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQi OiIgZGV0YWlscyJ9LCJwIjoiYWJjIn3zDRJJAAAAxAAAAFeMuFUlCzpldmVudC10eXBlBwARY29u dGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2Ut dHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiB0byJ9 LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTiJ9ACAmaQAAAM8A AABX+2hkNAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgi OjAsImRlbHRhIjp7InRleHQiOiIgdGhlIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5 ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWCJ9zj4Z2QAAAK4AAABXXzo6yQs6ZXZlbnQtdHlwZQcA EWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIg VXNlciJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcCJ9Yxn2iwAAALkAAABXjfpxWws6ZXZlbnQtdHlw ZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTpt ZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQi OiIuPC8ifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDIn2Su3/YAAAAxwAAAFfL GC/1CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxp Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwi ZGVsdGEiOnsidGV4dCI6InRoaW5raW5nIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5 ekFCQ0RFRkdISUpLTCJ9igrL9QAAANQAAABX7FjCpws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9j a0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVl dmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiI+In0sInAiOiJhYmNk ZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQ1In2a G4eMAAAAugAAAFfKWguLCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50Qmxv Y2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IlxuXG5UaGUifSwicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6In0B6RJ+AAAAyQAAAFd0KJGUCzpldmVudC10eXBlBwARY29udGVudEJsb2Nr RGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2 ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBjdXJyZW50In0sInAi OiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OIn3it/96AAAA1AAAAFfs WMKnCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxp Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwi ZGVsdGEiOnsidGV4dCI6IiB3ZWF0aGVyIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5 ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFkifSaQh8MAAACvAAAAV2JaE3kLOmV2ZW50LXR5cGUH ABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0Ijoi IGluIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzIn1lavJtAAAAwwAAAFc+mIk1CzpldmVudC10 eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24N Om1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4 dCI6IiBTZWF0dGxlIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdIIn0K BGzvAAAAtwAAAFcyys86CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50Qmxv Y2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBpcyJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFy c3R1dnd4eXpBIn0cqUi6AAAA0wAAAFdeeB63CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVs dGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiJ9LCJwIjoiYWJjZGVmZ2hp amtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0NSJ90L2sxwAA AKkAAABX7Rrm2Qs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5k ZXgiOjAsImRlbHRhIjp7InRleHQiOiIgNSJ9LCJwIjoiYWJjZGVmZ2hpamtsbW4ifb/9rQAAAACv AAAAV2JaE3kLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQ YXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4 IjowLCJkZWx0YSI6eyJ0ZXh0IjoiMCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1In2CEX7b AAAApQAAAFco6gvYCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlw ZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJ bmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBkZWdyZWVzIn0sInAiOiJhYmNkIn1xW6MgAAAAtwAA AFcyys86CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFw cGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6 MCwiZGVsdGEiOnsidGV4dCI6IiBhbmQifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6 In3QAS1JAAAAuQAAAFeN+nFbCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRl bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50 QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiByYWluaW5nIn0sInAiOiJhYmNkZWZnaGlq a2xtbm9wcXJzdHV2d3gifQuEdUcAAAC3AAAAVzLKzzoLOmV2ZW50LXR5cGUHABFjb250ZW50Qmxv Y2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiLiJ9LCJwIjoiYWJj ZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkMifTKl3CoAAAC1AAAAV0gKnFoLOmV2ZW50LXR5cGUH ABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0Ijoi IEluIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5In2bdLPRAAAAoAAAAFfgCoSoCzpl dmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEi OnsidGV4dCI6IiBTYW4ifSwicCI6ImFiYyJ9MF2CRQAAAKUAAABXKOoL2As6ZXZlbnQtdHlwZQcA EWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIg RnJhbmNpc2NvIn0sInAiOiJhYiJ9LZSTvwAAANQAAABX7FjCpws6ZXZlbnQtdHlwZQcAEWNvbnRl bnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5 cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIsIn0sInAi OiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEy MzQ1In3WXYB/AAAAwQAAAFdEWNpVCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNv bnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250 ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiB0aGUifSwicCI6ImFiY2RlZmdoaWpr bG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSiJ96OGhEwAAAKsAAABXl9q1uQs6ZXZlbnQtdHlwZQcA EWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIg d2VhdGhlciJ9LCJwIjoiYWJjZGVmZ2hpaiJ9adYFDAAAAMwAAABXvMge5As6ZXZlbnQtdHlwZQcA EWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIg aXMifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RV ViJ9mQSbJgAAAM8AAABX+2hkNAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250 ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVu dEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIifSwicCI6ImFiY2RlZmdoaWprbG1ub3Bx cnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMSJ9VS8lYwAAAMkAAABXdCiRlAs6 ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlv bi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRh Ijp7InRleHQiOiIgNyJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElK S0xNTk9QUVJTVCJ9nuAf5QAAANAAAABXGdhkZws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0Rl bHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu dHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIwIn0sInAiOiJhYmNkZWZn aGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEifbtb2n4AAADA AAAAV3k48+ULOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQ YXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4 IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGRlZ3JlZXMifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0 dXZ3eHl6QUJDREUifey6V54AAADUAAAAV+xYwqcLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tE ZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZl bnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGFuZCJ9LCJwIjoiYWJj ZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMiJ9e9pI fQAAAMoAAABXM4jrRAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5 cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2Nr SW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgc3VubnkifSwicCI6ImFiY2RlZmdoaWprbG1ub3Bx cnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFEifV1wvHQAAADOAAAAV8YITYQLOmV2ZW50LXR5cGUH ABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0Ijoi LiJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVW V1hZWiJ9oE16IgAAAMcAAABXyxgv9Qs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29u dGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIifSwicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1QifZG1Gj4AAACeAAAAVokcstkLOmV2ZW50 LXR5cGUHABBjb250ZW50QmxvY2tTdG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsInAiOiJhYmNkZWZn aGlqa2xtbm9wcXJzdHV2d3h5eiJ91hBR8gAAALsAAABRHlmHDgs6ZXZlbnQtdHlwZQcAC21lc3Nh Z2VTdG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVl dmVudHsicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RV VldYWVowMTIzNDUiLCJzdG9wUmVhc29uIjoiZW5kX3R1cm4ifQnrjxcAAADUAAAATogzamcLOmV2 ZW50LXR5cGUHAAhtZXRhZGF0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7Im1ldHJpY3MiOnsibGF0ZW5jeU1zIjo0Nzl9LCJwIjoiYWJjZGVm Z2hpamtsbW5vcHFyIiwidXNhZ2UiOnsiaW5wdXRUb2tlbnMiOjU2NSwib3V0cHV0VG9rZW5zIjo1 MiwidG90YWxUb2tlbnMiOjYxN3199cxefA== headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Wed, 26 Feb 2025 10:27:39 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked x-amzn-RequestId: - a23749ab-7ba7-45b1-81c4-f7bc1de0f139 status: code: 200 message: OK version: 1 test_converse_stream_with_invalid_model.yaml000066400000000000000000000027411527070072300471070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] } ] } headers: Content-Length: - '77' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.0.0 md/arch#arm64 lang/python#3.13.1 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250206T051235Z X-Amzn-Trace-Id: - Root=1-3b3ab5b7-f89b7315b15dce8b8cda5b98;Parent=2674a1ffc8cf10b9;Sampled=1 amz-sdk-invocation-id: - f69bd018-8fa4-41f3-bd81-8545ea8e3570 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/does-not-exist/converse-stream response: body: string: |- { "message": "The provided model identifier is invalid." } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 06 Feb 2025 05:12:36 GMT Set-Cookie: test_set_cookie x-amzn-ErrorType: - ValidationException:http://internal.amazon.com/coral/com.amazon.bedrock/ x-amzn-RequestId: - 32ef4ce3-5d0d-4ac2-9928-b7648a98c5b2 status: code: 400 message: Bad Request version: 1 test_converse_stream_with_prompt_caching.yaml000066400000000000000000001116571527070072300473050ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "system": [ { "text": "You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. " }, { "cachePoint": { "type": "default" } } ], "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] } ], "inferenceConfig": { "maxTokens": 10 } } headers: Content-Length: - '14878' Content-Type: - application/json User-Agent: - Boto3/1.43.0 md/Botocore#1.43.0 ua/2.1 os/macos#25.5.0 md/arch#arm64 lang/python#3.12.13 md/pyimpl#CPython m/Z,b,g,D cfg/retry-mode#legacy Botocore/1.43.0 X-Amz-Date: - 20260901T130459Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-dbb8d44b-cc7cbf6c084807b6ce94eccb;Parent=a78c7c8afc033eb6;Sampled=1 amz-sdk-invocation-id: - 5b699213-ade8-4b13-9100-de3785a6da64 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.amazon.nova-micro-v1%3A0/converse-stream response: body: string: !!binary | AAAAgQAAAFJswXaTCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2QiLCJyb2xlIjoi YXNzaXN0YW50In31EqAFAAAA2AAAAFcpqC+mCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVs dGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IkdvdCJ9LCJwIjoiYWJjZGVm Z2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0NTY3In3+ 5Jr4AAAAywAAAFcO6ML0CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50Qmxv Y2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBpdCwifSwicCI6ImFiY2RlZmdoaWprbG1ub3Bx cnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1QifeAWEPgAAADYAAAAVymoL6YLOmV2ZW50LXR5 cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06 bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0 IjoiIHRoaXMgaXMgYSB0ZXN0In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RF RkdISUpLTE1OT1BRUlNUVVYifYO5PsUAAAC6AAAAV8paC4sLOmV2ZW50LXR5cGUHABFjb250ZW50 QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBl BwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiLiBJZiJ9LCJw IjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkMifan3oQoAAADLAAAAVw7owvQLOmV2ZW50 LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0 ZXh0IjoiIHlvdSJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xN Tk9QUVJTVCJ9QfY99QAAALQAAABWAm2FfAs6ZXZlbnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0b3AN OmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJj b250ZW50QmxvY2tJbmRleCI6MCwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVG R0hJSktMTU5PUFFSU1RVViJ9qr7BagAAAJ0AAABRUNhdqgs6ZXZlbnQtdHlwZQcAC21lc3NhZ2VT dG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu dHsicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6Iiwic3RvcFJlYXNvbiI6Im1heF90b2tl bnMiffFcPUYAAAF8AAAATgM4bZULOmV2ZW50LXR5cGUHAAhtZXRhZGF0YQ06Y29udGVudC10eXBl BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7Im1ldHJpY3MiOnsibGF0 ZW5jeU1zIjo0NTR9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xN Tk9QUVJTVFVWV1giLCJ1c2FnZSI6eyJjYWNoZVJlYWRJbnB1dFRva2VuQ291bnQiOjAsImNhY2hl UmVhZElucHV0VG9rZW5zIjowLCJjYWNoZVdyaXRlSW5wdXRUb2tlbkNvdW50IjoyNzAxLCJjYWNo ZVdyaXRlSW5wdXRUb2tlbnMiOjI3MDEsImlucHV0VG9rZW5zIjo0LCJvdXRwdXRUb2tlbnMiOjEw LCJzZXJ2ZXJUb29sVXNhZ2UiOnt9LCJ0b3RhbFRva2VucyI6MjcxNX19foZQwg== headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Tue, 01 Sep 2026 13:05:00 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked x-amzn-RequestId: - b31e3ac7-96fc-45a4-ab4b-9d4d66a0db0d status: code: 200 message: OK - request: body: |- { "system": [ { "text": "You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. You are a helpful assistant for streaming tests. " }, { "cachePoint": { "type": "default" } } ], "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] } ], "inferenceConfig": { "maxTokens": 10 } } headers: Content-Length: - '14878' Content-Type: - application/json User-Agent: - Boto3/1.43.0 md/Botocore#1.43.0 ua/2.1 os/macos#25.5.0 md/arch#arm64 lang/python#3.12.13 md/pyimpl#CPython m/Z,b,g,D cfg/retry-mode#legacy Botocore/1.43.0 X-Amz-Date: - 20260901T130500Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-12cc5206-a78faad2f1f1f3ee384099d4;Parent=0c6a1c4c4154212a;Sampled=1 amz-sdk-invocation-id: - f9eea2be-28dc-4460-a46b-a32ca7bb6a3b amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.amazon.nova-micro-v1%3A0/converse-stream response: body: string: !!binary | AAAAkQAAAFIMIeERCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0Iiwicm9sZSI6ImFzc2lzdGFudCJ9vw+WSAAAALkAAABXjfpxWws6ZXZlbnQtdHlwZQcA EWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiJJ dCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNEIn2M0+7dAAAA9AAAAFctme2j CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVs dGEiOnsidGV4dCI6IiBzb3VuZHMgbGlrZSB5b3UncmUgaW5pdGlhdGluZyJ9LCJwIjoiYWJjZGVm Z2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0NTY3OCJ9 iUMw/QAAAM8AAABX+2hkNAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50 LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJs b2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgYSB0ZXN0IHNjZW5hcmlvIn0sInAiOiJhYmNk ZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTCJ9fyns8QAAALIAAABWjS1w3As6ZXZl bnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pz b24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwicCI6ImFiY2Rl ZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1QifY2vo8sAAACUAAAAUV3I P9sLOmV2ZW50LXR5cGUHAAttZXNzYWdlU3RvcA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24v anNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7InAiOiJhYmNkZWZnaGlqa2xtbm9wcSIsInN0b3BS ZWFzb24iOiJtYXhfdG9rZW5zIn2FlyN9AAABhgAAAE4BvCVpCzpldmVudC10eXBlBwAIbWV0YWRh dGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 eyJtZXRyaWNzIjp7ImxhdGVuY3lNcyI6Mzg3fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3 eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU2NyIsInVzYWdlIjp7ImNhY2hlUmVh ZElucHV0VG9rZW5Db3VudCI6MjcwMSwiY2FjaGVSZWFkSW5wdXRUb2tlbnMiOjI3MDEsImNhY2hl V3JpdGVJbnB1dFRva2VuQ291bnQiOjAsImNhY2hlV3JpdGVJbnB1dFRva2VucyI6MCwiaW5wdXRU b2tlbnMiOjQsIm91dHB1dFRva2VucyI6MTAsInNlcnZlclRvb2xVc2FnZSI6e30sInRvdGFsVG9r ZW5zIjoyNzE1fX0sQ9ok headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Tue, 01 Sep 2026 13:05:01 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked x-amzn-RequestId: - 90e0bb1b-9352-4da4-8760-66080eb905eb status: code: 200 message: OK version: 1 test_converse_tool_call_no_content.yaml000066400000000000000000000177271527070072300461030ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today?" } ] } ], "toolConfig": { "tools": [ { "toolSpec": { "name": "get_current_weather", "description": "Get the current weather in a given location.", "inputSchema": { "json": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } } } ] } } headers: Content-Length: - '409' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250225T135923Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-80a19e08-5565b002b1f618668e779b90;Parent=a970189e766f47c0;Sampled=1 amz-sdk-invocation-id: - 532f5b6d-0307-4859-b0b2-d1fc492f43e0 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse response: body: string: |- { "metrics": { "latencyMs": 563 }, "output": { "message": { "content": [ { "text": "The User wants to know the current weather in two different cities: Seattle and San Francisco. To provide this information, I will use the available tool to get the current weather in each of these locations.\n" }, { "toolUse": { "input": { "location": "Seattle" }, "name": "get_current_weather", "toolUseId": "tooluse_AOOBCtPEQ7SHozPsXCqgNg" } }, { "toolUse": { "input": { "location": "San Francisco" }, "name": "get_current_weather", "toolUseId": "tooluse_biMWVYVLSR-qN88TpGnkUA" } } ], "role": "assistant" } }, "stopReason": "tool_use", "usage": { "inputTokens": 415, "outputTokens": 180, "totalTokens": 595 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Tue, 25 Feb 2025 13:59:24 GMT Set-Cookie: test_set_cookie x-amzn-RequestId: - c2e62244-dcdd-4453-9125-7f9c5c31e51d status: code: 200 message: OK - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today?" } ] }, { "role": "assistant", "content": [ { "text": "The User wants to know the current weather in two different cities: Seattle and San Francisco. To provide this information, I will use the available tool to get the current weather in each of these locations.\n" }, { "toolUse": { "toolUseId": "tooluse_AOOBCtPEQ7SHozPsXCqgNg", "name": "get_current_weather", "input": { "location": "Seattle" } } }, { "toolUse": { "toolUseId": "tooluse_biMWVYVLSR-qN88TpGnkUA", "name": "get_current_weather", "input": { "location": "San Francisco" } } } ] }, { "role": "user", "content": [ { "toolResult": { "content": [ { "json": { "weather": "50 degrees and raining" } } ], "toolUseId": "tooluse_AOOBCtPEQ7SHozPsXCqgNg" } }, { "toolResult": { "content": [ { "json": { "weather": "70 degrees and sunny" } } ], "toolUseId": "tooluse_biMWVYVLSR-qN88TpGnkUA" } } ] } ], "toolConfig": { "tools": [ { "toolSpec": { "name": "get_current_weather", "description": "Get the current weather in a given location.", "inputSchema": { "json": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } } } ] } } headers: Content-Length: - '1233' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250225T135924Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-9908c30c-7111bc104cfa9ca0e12cdcd7;Parent=04fe5ed3554021ef;Sampled=1 amz-sdk-invocation-id: - bf45067c-2700-4770-bcd3-ae0dcfc72dfa amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse response: body: string: |- { "metrics": { "latencyMs": 642 }, "output": { "message": { "content": [ { "text": "I have obtained the current weather information for both Seattle and San Francisco using the provided tool. Now, I will present this information to the User.\n\nThe current weather is as follows:\n- In Seattle: It is 50 degrees and raining.\n- In San Francisco: It is 70 degrees and sunny.\n\nIf you need any more information, feel free to ask!" } ], "role": "assistant" } }, "stopReason": "end_turn", "usage": { "inputTokens": 553, "outputTokens": 80, "totalTokens": 633 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Tue, 25 Feb 2025 13:59:25 GMT Set-Cookie: test_set_cookie x-amzn-RequestId: - 03795cb8-e59d-4e27-8031-b988d40ebe0b status: code: 200 message: OK version: 1 test_converse_tool_call_with_content.yaml000066400000000000000000000175131527070072300464330ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today?" } ] } ], "toolConfig": { "tools": [ { "toolSpec": { "name": "get_current_weather", "description": "Get the current weather in a given location.", "inputSchema": { "json": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } } } ] } } headers: Content-Length: - '409' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250224T160609Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-d29c93a0-7ce2e06967915b60a2af1ad6;Parent=c94e59449648941b;Sampled=1 amz-sdk-invocation-id: - 4f653ab3-8873-4e56-bc7d-a5f007edc4ff amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse response: body: string: |- { "metrics": { "latencyMs": 658 }, "output": { "message": { "content": [ { "text": " To provide the weather information for both Seattle and San Francisco, I will use the `get_current_weather` tool for each city. I will start with Seattle and then proceed with San Francisco.\n" }, { "toolUse": { "input": { "location": "Seattle" }, "name": "get_current_weather", "toolUseId": "tooluse_tggNKJbGSrm48inRqf3Rvw" } }, { "toolUse": { "input": { "location": "San Francisco" }, "name": "get_current_weather", "toolUseId": "tooluse_bRV9WIcFSxyrLY6-MVkZRA" } } ], "role": "assistant" } }, "stopReason": "tool_use", "usage": { "inputTokens": 415, "outputTokens": 190, "totalTokens": 605 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Mon, 24 Feb 2025 16:06:10 GMT Set-Cookie: test_set_cookie x-amzn-RequestId: - c8537d75-1a24-432f-a26e-1712281540cf status: code: 200 message: OK - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today?" } ] }, { "role": "assistant", "content": [ { "text": " To provide the weather information for both Seattle and San Francisco, I will use the `get_current_weather` tool for each city. I will start with Seattle and then proceed with San Francisco.\n" }, { "toolUse": { "toolUseId": "tooluse_tggNKJbGSrm48inRqf3Rvw", "name": "get_current_weather", "input": { "location": "Seattle" } } }, { "toolUse": { "toolUseId": "tooluse_bRV9WIcFSxyrLY6-MVkZRA", "name": "get_current_weather", "input": { "location": "San Francisco" } } } ] }, { "role": "user", "content": [ { "toolResult": { "content": [ { "json": { "weather": "50 degrees and raining" } } ], "toolUseId": "tooluse_tggNKJbGSrm48inRqf3Rvw" } }, { "toolResult": { "content": [ { "json": { "weather": "70 degrees and sunny" } } ], "toolUseId": "tooluse_bRV9WIcFSxyrLY6-MVkZRA" } } ] } ], "toolConfig": { "tools": [ { "toolSpec": { "name": "get_current_weather", "description": "Get the current weather in a given location.", "inputSchema": { "json": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } } } ] } } headers: Content-Length: - '1216' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250224T160610Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-33a7171a-fd011f6ea22ee79e64d821a3;Parent=776cdf535745c221;Sampled=1 amz-sdk-invocation-id: - 71d9e344-c6dc-4b2a-bafe-f3ae65989ee0 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse response: body: string: |- { "metrics": { "latencyMs": 496 }, "output": { "message": { "content": [ { "text": " I have received the weather information for both cities. Now I will compile this information and present it to the User.\n\nThe current weather in Seattle is 50 degrees and it's raining. In San Francisco, it's 70 degrees and sunny today." } ], "role": "assistant" } }, "stopReason": "end_turn", "usage": { "inputTokens": 553, "outputTokens": 59, "totalTokens": 612 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Mon, 24 Feb 2025 16:06:11 GMT Set-Cookie: test_set_cookie x-amzn-RequestId: - ce574485-9823-4db9-8974-cdbeacd6e0f3 status: code: 200 message: OK version: 1 test_converse_with_content.yaml000066400000000000000000000036651527070072300444060ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] } ], "inferenceConfig": { "maxTokens": 10, "temperature": 0.8, "topP": 1, "stopSequences": [ "|" ] } } headers: Content-Length: - '170' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.0.0 md/arch#arm64 lang/python#3.13.1 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250206T051231Z X-Amzn-Trace-Id: - Root=1-c114b02b-84cec2d763a71299ae73fb9f;Parent=6f540fd45e1dc3b3;Sampled=1 amz-sdk-invocation-id: - a8c71d0c-ae7b-4685-b696-6ba0ded398ef amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.titan-text-lite-v1/converse response: body: string: |- { "metrics": { "latencyMs": 608 }, "output": { "message": { "content": [ { "text": "Hi, how can I help you" } ], "role": "assistant" } }, "stopReason": "max_tokens", "usage": { "inputTokens": 8, "outputTokens": 10, "totalTokens": 18 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 06 Feb 2025 05:12:33 GMT Set-Cookie: test_set_cookie x-amzn-RequestId: - 180bdcbc-0fea-4b99-a0f0-b858dd830a83 status: code: 200 message: OK version: 1 test_converse_with_content_different_events.yaml000066400000000000000000000043711527070072300500130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "system": [ { "text": "You are a friendly app" } ], "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] }, { "role": "assistant", "content": [ { "text": "This is a test" } ] }, { "role": "user", "content": [ { "text": "Say again this is a test" } ] } ] } headers: Content-Length: - '258' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250213T113026Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-188a1ced-da75f3769b7d8ee0334fa4bf;Parent=f8c672d0277ff6e1;Sampled=1 amz-sdk-invocation-id: - fae72016-6b0e-4b6f-b15a-100aa7f19e48 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-v2/converse response: body: string: |- { "metrics": { "latencyMs": 408 }, "output": { "message": { "content": [ { "text": "This is a test" } ], "role": "assistant" } }, "stopReason": "end_turn", "usage": { "inputTokens": 37, "outputTokens": 8, "totalTokens": 45 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 13 Feb 2025 11:30:27 GMT Set-Cookie: test_set_cookie x-amzn-RequestId: - 5273de67-e76c-45ad-be0a-558957af8046 status: code: 200 message: OK version: 1 test_converse_with_invalid_model.yaml000066400000000000000000000027321527070072300455340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] } ] } headers: Content-Length: - '77' Content-Type: - application/json User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.0.0 md/arch#arm64 lang/python#3.13.1 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250206T051232Z X-Amzn-Trace-Id: - Root=1-d44c213c-792360fb9074bfd2f5334bad;Parent=973420d2413e6885;Sampled=1 amz-sdk-invocation-id: - 0e8c8731-2812-4dbf-add2-b75b6ad599df amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/does-not-exist/converse response: body: string: |- { "message": "The provided model identifier is invalid." } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 06 Feb 2025 05:12:33 GMT Set-Cookie: test_set_cookie x-amzn-ErrorType: - ValidationException:http://internal.amazon.com/coral/com.amazon.bedrock/ x-amzn-RequestId: - 7fdbc480-6d28-4d1c-abbc-bd7f17c7dfec status: code: 400 message: Bad Request version: 1 test_converse_with_prompt_caching.yaml000066400000000000000000001114441527070072300457240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "system": [ { "text": "You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. " }, { "cachePoint": { "type": "default" } } ], "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] } ], "inferenceConfig": { "maxTokens": 10 } } headers: Content-Length: - '16678' Content-Type: - application/json User-Agent: - Boto3/1.43.83 md/Botocore#1.43.83 ua/2.1 os/macos#25.5.0 md/arch#arm64 lang/python#3.12.13 md/pyimpl#CPython m/Z,b,D,g cfg/retry-mode#legacy Botocore/1.43.83 X-Amz-Date: - 20260831T130413Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-49418b48-4f487c417c3c21e81bfacc74;Parent=e2d4aa69c51a8141;Sampled=1 amz-sdk-invocation-id: - 6c60e656-c0b9-4fd0-85ed-51f2bdf4bf35 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.amazon.nova-micro-v1%3A0/converse response: body: string: |- { "metrics": { "latencyMs": 419 }, "output": { "message": { "content": [ { "text": "It looks like you're initiating a test scenario" } ], "role": "assistant" } }, "stopReason": "max_tokens", "usage": { "cacheReadInputTokenCount": 0, "cacheReadInputTokens": 0, "cacheWriteInputTokenCount": 2701, "cacheWriteInputTokens": 2701, "inputTokens": 4, "outputTokens": 10, "serverToolUsage": {}, "totalTokens": 2715 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Mon, 31 Aug 2026 13:04:14 GMT Set-Cookie: test_set_cookie x-amzn-RequestId: - c05db5d5-3eee-434e-ae38-cfa9f31b9f54 status: code: 200 message: OK - request: body: |- { "system": [ { "text": "You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. You are a helpful assistant for instrumentation tests. " }, { "cachePoint": { "type": "default" } } ], "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] } ], "inferenceConfig": { "maxTokens": 10 } } headers: Content-Length: - '16678' Content-Type: - application/json User-Agent: - Boto3/1.43.83 md/Botocore#1.43.83 ua/2.1 os/macos#25.5.0 md/arch#arm64 lang/python#3.12.13 md/pyimpl#CPython m/Z,b,D,g cfg/retry-mode#legacy Botocore/1.43.83 X-Amz-Date: - 20260831T130414Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-7e002974-66964ab4cbaee892971a236a;Parent=c2f09587db632842;Sampled=1 amz-sdk-invocation-id: - c00e2769-724c-4fc6-b9cb-e7a13624e183 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.amazon.nova-micro-v1%3A0/converse response: body: string: |- { "metrics": { "latencyMs": 385 }, "output": { "message": { "content": [ { "text": "Certainly! If this is a test, here\u2019" } ], "role": "assistant" } }, "stopReason": "max_tokens", "usage": { "cacheReadInputTokenCount": 2701, "cacheReadInputTokens": 2701, "cacheWriteInputTokenCount": 0, "cacheWriteInputTokens": 0, "inputTokens": 4, "outputTokens": 10, "serverToolUsage": {}, "totalTokens": 2715 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Mon, 31 Aug 2026 13:04:14 GMT Set-Cookie: test_set_cookie x-amzn-RequestId: - ab36149c-5363-4f3f-b8a8-6d71fcfe185d status: code: 200 message: OK version: 1 test_invoke_model_no_content[amazon.nova].yaml000066400000000000000000000045031527070072300473260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] } ], "inferenceConfig": { "max_new_tokens": 10, "temperature": 0.8, "topP": 1, "stopSequences": [ "|" ] }, "schemaVersion": "messages-v1" } headers: Content-Length: - '207' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250211T141037Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-c2ac5bb7-05fa324e3cb618c2b67e493b;Parent=f005000ccddbf918;Sampled=1 amz-sdk-invocation-id: - d988352d-9242-448f-b283-a92382ea97a9 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/invoke response: body: string: |- { "output": { "message": { "content": [ { "text": "It seems like you\u2019re running a test or" } ], "role": "assistant" } }, "stopReason": "max_tokens", "usage": { "inputTokens": 5, "outputTokens": 10, "totalTokens": 15, "cacheReadInputTokenCount": 0, "cacheWriteInputTokenCount": 0 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Tue, 11 Feb 2025 14:10:38 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Cache-Read-Input-Token-Count: - '0' X-Amzn-Bedrock-Cache-Write-Input-Token-Count: - '0' X-Amzn-Bedrock-Input-Token-Count: - '5' X-Amzn-Bedrock-Invocation-Latency: - '223' X-Amzn-Bedrock-Output-Token-Count: - '10' x-amzn-RequestId: - b302eeb1-efef-4054-9a64-6a343cc2ccf1 status: code: 200 message: OK version: 1 test_invoke_model_no_content[amazon.titan].yaml000066400000000000000000000033771527070072300475120ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "inputText": "Say this is a test", "textGenerationConfig": { "maxTokenCount": 10, "temperature": 0.8, "topP": 1, "stopSequences": [ "|" ] } } headers: Content-Length: - '137' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250211T141038Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-9c97c329-c012872e6f21fb9ddf4e7a98;Parent=60362f98aa20fa2d;Sampled=1 amz-sdk-invocation-id: - 8feb1f58-f561-497c-9b7e-8050c3d1fd88 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.titan-text-lite-v1/invoke response: body: string: |- { "inputTextTokenCount": 5, "results": [ { "tokenCount": 10, "outputText": "\nHello! I am a computer program designed to", "completionReason": "LENGTH" } ] } headers: Connection: - keep-alive Content-Type: - application/json Date: - Tue, 11 Feb 2025 14:10:39 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '5' X-Amzn-Bedrock-Invocation-Latency: - '623' X-Amzn-Bedrock-Output-Token-Count: - '10' x-amzn-RequestId: - 25260e3f-4a5f-47aa-afe2-99bca8f9bf68 status: code: 200 message: OK version: 1 test_invoke_model_no_content[anthropic.claude].yaml000066400000000000000000000042141527070072300503210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test", "type": "text" } ] } ], "anthropic_version": "bedrock-2023-05-31", "max_tokens": 10, "temperature": 0.8, "top_p": 1, "stop_sequences": [ "|" ] } headers: Content-Length: - '211' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250211T141039Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-8be6f3ea-614b53957b659455b7f8a1ff;Parent=9f44c46d2e6821c0;Sampled=1 amz-sdk-invocation-id: - 5d18ca20-a5ac-4403-99b5-aaa6ecf46b5d amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-v2/invoke response: body: string: |- { "id": "msg_bdrk_01QBSj9wkkgvNKMZZxMa6J77", "type": "message", "role": "assistant", "model": "claude-2.0", "content": [ { "type": "text", "text": "Okay, I just said \"This is a test" } ], "stop_reason": "max_tokens", "stop_sequence": null, "usage": { "input_tokens": 14, "output_tokens": 10 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Tue, 11 Feb 2025 14:10:40 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '14' X-Amzn-Bedrock-Invocation-Latency: - '506' X-Amzn-Bedrock-Output-Token-Count: - '10' x-amzn-RequestId: - a6cbb4fd-4a3f-4c19-8e68-23ad1622ddfe status: code: 200 message: OK version: 1 test_invoke_model_no_content[cohere.command-r].yaml000066400000000000000000000036251527070072300502240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "message": "Say this is a test", "max_tokens": 10, "temperature": 0.8, "p": 0.9, "stop_sequences": [ "|" ] } headers: Content-Length: - '106' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.3.0 md/arch#arm64 lang/python#3.10.16 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250410T165615Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-d11162cc-5e3ef3bd07c5f9eb3ad25214;Parent=0a40817ca75eead7;Sampled=1 amz-sdk-invocation-id: - 54acc108-181d-4437-a0a2-8293496f892d amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/cohere.command-r-v1%3A0/invoke response: body: string: |- { "response_id": "379ed018/64bcb4c2-902c-4646-9e54-3e5e90c3b11e", "text": "This is a test.<", "generation_id": "dcf7f3e3-a611-4fb5-85fe-ae09e6b2eaea", "chat_history": [ { "role": "USER", "message": "Say this is a test" }, { "role": "CHATBOT", "message": "This is a test.<" } ], "finish_reason": "STOP_SEQUENCE" } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 10 Apr 2025 16:56:15 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '5' X-Amzn-Bedrock-Invocation-Latency: - '141' X-Amzn-Bedrock-Output-Token-Count: - '5' x-amzn-RequestId: - 4d1ce10c-03d3-4579-8bab-a0e4eb14a8d0 status: code: 200 message: OK version: 1 test_invoke_model_no_content[cohere.command].yaml000066400000000000000000000034251527070072300477630ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "prompt": "Say this is a test", "max_tokens": 10, "temperature": 0.8, "p": 1, "stop_sequences": [ "|" ] } headers: Content-Length: - '103' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.3.0 md/arch#arm64 lang/python#3.10.16 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250410T183021Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-668b3eaf-1927d32dd30fc4f0fedf3a02;Parent=6b49f910f73172c6;Sampled=1 amz-sdk-invocation-id: - 98cb0162-a3bb-4d4e-bcbf-290664700ecf amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/cohere.command-light-text-v14/invoke response: body: string: |- { "id": "b68fba0b-67f6-46dd-a659-dab9a99ee354", "generations": [ { "id": "195692c2-6457-4ae0-9e5e-f0472a429cdc", "text": " I would be more than happy to assist you with", "finish_reason": "MAX_TOKENS" } ], "prompt": "Say this is a test" } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 10 Apr 2025 18:30:21 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '5' X-Amzn-Bedrock-Invocation-Latency: - '309' X-Amzn-Bedrock-Output-Token-Count: - '10' x-amzn-RequestId: - b68fba0b-67f6-46dd-a659-dab9a99ee354 status: code: 200 message: OK version: 1 test_invoke_model_no_content[meta.llama].yaml000066400000000000000000000031041527070072300471060ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "prompt": "Say this is a test", "max_gen_len": 10, "temperature": 0.8, "top_p": 1 } headers: Content-Length: - '83' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.3.0 md/arch#arm64 lang/python#3.10.16 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250410T192408Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-db2cb3ce-1a9a7362f6f0e5eb7dc0ce56;Parent=a301dca505171c7f;Sampled=1 amz-sdk-invocation-id: - 87acada6-b95b-4280-b002-50ef98edf99d amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/meta.llama3-1-70b-instruct-v1%3A0/invoke response: body: string: |- { "generation": " and you are a test subject. You are not", "prompt_token_count": 5, "generation_token_count": 10, "stop_reason": "length" } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 10 Apr 2025 19:24:09 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '5' X-Amzn-Bedrock-Invocation-Latency: - '617' X-Amzn-Bedrock-Output-Token-Count: - '10' x-amzn-RequestId: - 51b421ef-fd84-4c69-b933-8c928b735d17 status: code: 200 message: OK version: 1 test_invoke_model_no_content[mistral.mistral].yaml000066400000000000000000000031551527070072300502260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "prompt": "Say this is a test", "max_tokens": 10, "temperature": 0.8, "top_p": 1, "stop": [ "|" ] } headers: Content-Length: - '97' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.3.0 md/arch#arm64 lang/python#3.10.16 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250410T183022Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-e325095b-1fab9945412a3dcac8474715;Parent=ae78dbba36fcf5b0;Sampled=1 amz-sdk-invocation-id: - cbfbbad5-adc8-4a71-82f0-0777f6c78f83 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/mistral.mistral-7b-instruct-v0%3A2/invoke response: body: string: |- { "outputs": [ { "text": " to find out whether you think an animal is cute", "stop_reason": "length" } ] } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 10 Apr 2025 18:30:22 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '6' X-Amzn-Bedrock-Invocation-Latency: - '219' X-Amzn-Bedrock-Output-Token-Count: - '10' x-amzn-RequestId: - 97084131-1cef-4927-9bab-53a36390153e status: code: 200 message: OK version: 1 test_invoke_model_no_content_different_events[amazon.nova].yaml000066400000000000000000000052141527070072300527400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] }, { "role": "assistant", "content": [ { "text": "This is a test" } ] }, { "role": "user", "content": [ { "text": "Say again this is a test" } ] } ], "inferenceConfig": { "max_new_tokens": 10 }, "schemaVersion": "messages-v1", "system": [ { "text": "You are friendly model" } ] } headers: Content-Length: - '333' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250213T113030Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-ba0c8a29-b443cf8553f07da7a91a8206;Parent=976f285f1d94e9d5;Sampled=1 amz-sdk-invocation-id: - 01a2bce5-51df-4e24-bbc4-ec0fb28b35ae amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/invoke response: body: string: |- { "output": { "message": { "content": [ { "text": "Again, this is a test. If you need" } ], "role": "assistant" } }, "stopReason": "max_tokens", "usage": { "inputTokens": 24, "outputTokens": 10, "totalTokens": 34, "cacheReadInputTokenCount": 0, "cacheWriteInputTokenCount": 0 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 13 Feb 2025 11:30:30 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Cache-Read-Input-Token-Count: - '0' X-Amzn-Bedrock-Cache-Write-Input-Token-Count: - '0' X-Amzn-Bedrock-Input-Token-Count: - '24' X-Amzn-Bedrock-Invocation-Latency: - '182' X-Amzn-Bedrock-Output-Token-Count: - '10' x-amzn-RequestId: - 88719d47-9d62-4ae6-8623-07af41fb0044 status: code: 200 message: OK version: 1 test_invoke_model_no_content_different_events[anthropic.claude].yaml000066400000000000000000000047271527070072300537440ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test", "type": "text" } ] }, { "role": "assistant", "content": [ { "text": "This is a test", "type": "text" } ] }, { "role": "user", "content": [ { "text": "Say again this is a test", "type": "text" } ] } ], "anthropic_version": "bedrock-2023-05-31", "system": "You are a friendly model", "max_tokens": 10 } headers: Content-Length: - '357' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250213T113030Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-ca8c487d-14ab6e6ef139ee445552032f;Parent=76161c83e32d8750;Sampled=1 amz-sdk-invocation-id: - 65d2a14a-3292-44c0-852d-687472c3e477 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-v2/invoke response: body: string: |- { "id": "msg_bdrk_01CmG6WWSyyBzYoApTJb79vd", "type": "message", "role": "assistant", "model": "claude-2.0", "content": [ { "type": "text", "text": "This is a test" } ], "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 37, "output_tokens": 8 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 13 Feb 2025 11:30:32 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '37' X-Amzn-Bedrock-Invocation-Latency: - '1165' X-Amzn-Bedrock-Output-Token-Count: - '8' x-amzn-RequestId: - 39cf77df-c934-4701-926b-ae773dcc4a5a status: code: 200 message: OK version: 1 test_invoke_model_no_content_tool_call[amazon.nova].yaml000066400000000000000000000215351527070072300513620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today? Please expect one tool call for Seattle and one for San Francisco" } ] } ], "inferenceConfig": { "max_new_tokens": 1000 }, "schemaVersion": "messages-v1", "toolConfig": { "tools": [ { "toolSpec": { "name": "get_current_weather", "description": "Get the current weather in a given location.", "inputSchema": { "json": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } } } ] } } headers: Content-Length: - '552' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250325T130901Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-2541300b-2dbeda14c09ccd3d3b041879;Parent=1fc006fd156b7383;Sampled=1 amz-sdk-invocation-id: - 09f5a7ed-4163-46ec-a974-000c5662062a amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/invoke response: body: string: |- { "output": { "message": { "content": [ { "text": " To provide the current weather information for both Seattle and San Francisco, I will need to use the `get_current_weather` tool separately for each city. I will start by calling the tool for Seattle and then proceed to call it for San Francisco.\n" }, { "toolUse": { "name": "get_current_weather", "toolUseId": "4d8acf92-6a04-42b6-aaf7-f68937a339ef", "input": { "location": "Seattle" } } }, { "toolUse": { "name": "get_current_weather", "toolUseId": "db27bd9d-9f7d-4783-aa8b-59871e00e0bc", "input": { "location": "San Francisco" } } } ], "role": "assistant" } }, "stopReason": "tool_use", "usage": { "inputTokens": 427, "outputTokens": 172, "totalTokens": 599, "cacheReadInputTokenCount": 0, "cacheWriteInputTokenCount": 0 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Tue, 25 Mar 2025 13:09:02 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Cache-Read-Input-Token-Count: - '0' X-Amzn-Bedrock-Cache-Write-Input-Token-Count: - '0' X-Amzn-Bedrock-Input-Token-Count: - '427' X-Amzn-Bedrock-Invocation-Latency: - '430' X-Amzn-Bedrock-Output-Token-Count: - '172' x-amzn-RequestId: - adebeba4-5940-4c34-87a1-46cd45646bbe status: code: 200 message: OK - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today? Please expect one tool call for Seattle and one for San Francisco" } ] }, { "content": [ { "text": " To provide the current weather information for both Seattle and San Francisco, I will need to use the `get_current_weather` tool separately for each city. I will start by calling the tool for Seattle and then proceed to call it for San Francisco.\n" }, { "toolUse": { "name": "get_current_weather", "toolUseId": "4d8acf92-6a04-42b6-aaf7-f68937a339ef", "input": { "location": "Seattle" } } }, { "toolUse": { "name": "get_current_weather", "toolUseId": "db27bd9d-9f7d-4783-aa8b-59871e00e0bc", "input": { "location": "San Francisco" } } } ], "role": "assistant" }, { "role": "user", "content": [ { "toolResult": { "toolUseId": "4d8acf92-6a04-42b6-aaf7-f68937a339ef", "content": [ { "json": { "weather": "50 degrees and raining" } } ] } }, { "toolResult": { "toolUseId": "db27bd9d-9f7d-4783-aa8b-59871e00e0bc", "content": [ { "json": { "weather": "70 degrees and sunny" } } ] } } ] } ], "inferenceConfig": { "max_new_tokens": 1000 }, "schemaVersion": "messages-v1", "toolConfig": { "tools": [ { "toolSpec": { "name": "get_current_weather", "description": "Get the current weather in a given location.", "inputSchema": { "json": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } } } ] } } headers: Content-Length: - '1439' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250325T130902Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-2592d804-d8515e92a6320e1d293c3643;Parent=5406f9af558aaabe;Sampled=1 amz-sdk-invocation-id: - 4ad9ba7e-df5a-4a01-a3a1-40fce5d29ef2 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/invoke response: body: string: |- { "output": { "message": { "content": [ { "text": " I have received the weather information for both cities. Now I will compile this information and present it to the User.\n\nThe current weather in Seattle is 50 degrees and it is raining. In San Francisco, the weather is 70 degrees and sunny." } ], "role": "assistant" } }, "stopReason": "end_turn", "usage": { "inputTokens": 576, "outputTokens": 57, "totalTokens": 633, "cacheReadInputTokenCount": 0, "cacheWriteInputTokenCount": 0 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Tue, 25 Mar 2025 13:09:03 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Cache-Read-Input-Token-Count: - '0' X-Amzn-Bedrock-Cache-Write-Input-Token-Count: - '0' X-Amzn-Bedrock-Input-Token-Count: - '576' X-Amzn-Bedrock-Invocation-Latency: - '417' X-Amzn-Bedrock-Output-Token-Count: - '57' x-amzn-RequestId: - cedb9b10-f794-4ec4-ac67-f3e237511e62 status: code: 200 message: OK version: 1 test_invoke_model_no_content_tool_call[anthropic.claude].yaml000066400000000000000000000177441527070072300523650ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today? Please expect one tool call for Seattle and one for San Francisco", "type": "text" } ] } ], "anthropic_version": "bedrock-2023-05-31", "tools": [ { "name": "get_current_weather", "description": "Get the current weather in a given location.", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } ], "max_tokens": 1000 } headers: Content-Length: - '515' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250325T130903Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-8d12ecff-5ad6ccf1b77b1190770e7184;Parent=4b902787e2994328;Sampled=1 amz-sdk-invocation-id: - 67f1613f-e740-4deb-b887-a6fa61e21fa2 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-3-5-sonnet-20240620-v1%3A0/invoke response: body: string: |- { "id": "msg_bdrk_01VBpy5uMQLnyfKbPQskaPH3", "type": "message", "role": "assistant", "model": "claude-3-5-sonnet-20240620", "content": [ { "type": "text", "text": "Certainly! I'll check the current weather for both Seattle and San Francisco using the available tool. I'll make two separate calls to the get_current_weather function, one for each city." }, { "type": "tool_use", "id": "toolu_bdrk_019pyFnRNCLD8eRrPFryV45k", "name": "get_current_weather", "input": { "location": "Seattle" } }, { "type": "tool_use", "id": "toolu_bdrk_01DtdjkUePw2E5bVHGifpu3j", "name": "get_current_weather", "input": { "location": "San Francisco" } } ], "stop_reason": "tool_use", "stop_sequence": null, "usage": { "input_tokens": 392, "output_tokens": 135 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Tue, 25 Mar 2025 13:09:07 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '392' X-Amzn-Bedrock-Invocation-Latency: - '3788' X-Amzn-Bedrock-Output-Token-Count: - '135' x-amzn-RequestId: - f4badbe2-968d-4ec7-a7c9-0b6b53c1da8c status: code: 200 message: OK - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today? Please expect one tool call for Seattle and one for San Francisco", "type": "text" } ] }, { "role": "assistant", "content": [ { "type": "text", "text": "Certainly! I'll check the current weather for both Seattle and San Francisco using the available tool. I'll make two separate calls to the get_current_weather function, one for each city." }, { "type": "tool_use", "id": "toolu_bdrk_019pyFnRNCLD8eRrPFryV45k", "name": "get_current_weather", "input": { "location": "Seattle" } }, { "type": "tool_use", "id": "toolu_bdrk_01DtdjkUePw2E5bVHGifpu3j", "name": "get_current_weather", "input": { "location": "San Francisco" } } ] }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_bdrk_019pyFnRNCLD8eRrPFryV45k", "content": "50 degrees and raining" }, { "type": "tool_result", "tool_use_id": "toolu_bdrk_01DtdjkUePw2E5bVHGifpu3j", "content": "70 degrees and sunny" } ] } ], "anthropic_version": "bedrock-2023-05-31", "tools": [ { "name": "get_current_weather", "description": "Get the current weather in a given location.", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } ], "max_tokens": 1000 } headers: Content-Length: - '1299' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250325T130907Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-c68ede29-e6b06012c6c4c9f2f1b1e4b5;Parent=39c2a2da849c36cb;Sampled=1 amz-sdk-invocation-id: - 6d49de72-fb0f-453c-93d9-2c0b8290f7d0 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-3-5-sonnet-20240620-v1%3A0/invoke response: body: string: |- { "id": "msg_bdrk_01CTxAJ2MbeWdBb3r5SAyprB", "type": "message", "role": "assistant", "model": "claude-3-5-sonnet-20240620", "content": [ { "type": "text", "text": "Thank you for providing the weather information for both cities. Here's the current weather for Seattle and San Francisco today:\n\n1. Seattle: The current weather in Seattle is 50 degrees and raining.\n2. San Francisco: The current weather in San Francisco is 70 degrees and sunny.\n\nAs you can see, there's quite a difference in the weather between these two West Coast cities today. Seattle is experiencing cooler temperatures with rain, which is fairly typical for the Pacific Northwest. On the other hand, San Francisco is enjoying a warm, sunny day, which is pleasant for outdoor activities.\n\nIs there anything else you'd like to know about the weather in these cities or any other locations?" } ], "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 604, "output_tokens": 149 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Tue, 25 Mar 2025 13:09:12 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '604' X-Amzn-Bedrock-Invocation-Latency: - '4208' X-Amzn-Bedrock-Output-Token-Count: - '149' x-amzn-RequestId: - 9e07abe1-4450-41af-b2f1-ecd82d66493d status: code: 200 message: OK version: 1 test_invoke_model_with_content[amazon.nova].yaml000066400000000000000000000040421527070072300476630ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] } ], "inferenceConfig": { "max_new_tokens": 10, "temperature": 0.8, "topP": 1, "stopSequences": [ "|" ] }, "schemaVersion": "messages-v1" } headers: Content-Length: - '207' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.0.0 md/arch#arm64 lang/python#3.13.1 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250206T051236Z X-Amzn-Trace-Id: - Root=1-36143b12-70c21615c28ab43161636b63;Parent=9effb7c7b6cb2137;Sampled=1 amz-sdk-invocation-id: - ad4815dd-0511-4185-a5ff-7205d52f1116 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/invoke response: body: string: |- { "output": { "message": { "content": [ { "text": "Certainly, here's a test:\n\n---\n\n**" } ], "role": "assistant" } }, "stopReason": "max_tokens", "usage": { "inputTokens": 5, "outputTokens": 10, "totalTokens": 15 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 06 Feb 2025 05:12:37 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '5' X-Amzn-Bedrock-Invocation-Latency: - '186' X-Amzn-Bedrock-Output-Token-Count: - '10' x-amzn-RequestId: - 7e0027c0-ef54-4918-8cef-4c84a35594ab status: code: 200 message: OK version: 1 test_invoke_model_with_content[amazon.titan].yaml000066400000000000000000000032641527070072300500440ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "inputText": "Say this is a test", "textGenerationConfig": { "maxTokenCount": 10, "temperature": 0.8, "topP": 1, "stopSequences": [ "|" ] } } headers: Content-Length: - '137' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.0.0 md/arch#arm64 lang/python#3.13.1 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250206T051237Z X-Amzn-Trace-Id: - Root=1-9ddb1876-6e0f1537999198b967402e22;Parent=3d32c631b5f37173;Sampled=1 amz-sdk-invocation-id: - 4d399cfc-3ccc-4dcb-ba35-83177f50398c amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.titan-text-lite-v1/invoke response: body: string: |- { "inputTextTokenCount": 5, "results": [ { "tokenCount": 10, "outputText": " comment\nHello! I am writing this as a", "completionReason": "LENGTH" } ] } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 06 Feb 2025 05:12:38 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '5' X-Amzn-Bedrock-Invocation-Latency: - '647' X-Amzn-Bedrock-Output-Token-Count: - '10' x-amzn-RequestId: - 03388437-97c4-442b-bb12-a0eb58397f03 status: code: 200 message: OK version: 1 test_invoke_model_with_content[anthropic.claude].yaml000066400000000000000000000041041527070072300506560ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test", "type": "text" } ] } ], "anthropic_version": "bedrock-2023-05-31", "max_tokens": 10, "temperature": 0.8, "top_p": 1, "stop_sequences": [ "|" ] } headers: Content-Length: - '211' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.0.0 md/arch#arm64 lang/python#3.13.1 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250206T051238Z X-Amzn-Trace-Id: - Root=1-6b2173ac-0edeb464a48f377fe871f4d6;Parent=709b0744fe7b43be;Sampled=1 amz-sdk-invocation-id: - f2b1380f-2da0-439d-99f6-11ddb3d74c22 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-v2/invoke response: body: string: |- { "id": "msg_bdrk_01NCxHHwwdtMc7wioSxo2wBC", "type": "message", "role": "assistant", "model": "claude-2.0", "content": [ { "type": "text", "text": "Okay, I said \"This is a test\"" } ], "stop_reason": "max_tokens", "stop_sequence": null, "usage": { "input_tokens": 14, "output_tokens": 10 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 06 Feb 2025 05:12:40 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '14' X-Amzn-Bedrock-Invocation-Latency: - '1131' X-Amzn-Bedrock-Output-Token-Count: - '10' x-amzn-RequestId: - 173e5b06-61f1-4368-9926-0a3369f721eb status: code: 200 message: OK version: 1 test_invoke_model_with_content[cohere.command-r].yaml000066400000000000000000000037021527070072300505570ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "message": "Say this is a test", "max_tokens": 10, "temperature": 0.8, "p": 0.99, "stop_sequences": [ "|" ] } headers: Content-Length: - '107' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.3.0 md/arch#arm64 lang/python#3.10.16 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250410T224018Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-51673f20-3dd018601b078c785c032a50;Parent=c05b84f54e2c35ee;Sampled=1 amz-sdk-invocation-id: - c81a224e-d8b3-4416-aaca-858b478b7db4 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/cohere.command-r-v1%3A0/invoke response: body: string: |- { "response_id": "379ed018/aa2df2bf-edc8-483f-8cd0-d22d04ba34ba", "text": "This is a test. How are you doing today", "generation_id": "2d74c447-266d-4286-8425-a92ad7fc0cbc", "chat_history": [ { "role": "USER", "message": "Say this is a test" }, { "role": "CHATBOT", "message": "This is a test. How are you doing today" } ], "finish_reason": "MAX_TOKENS" } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 10 Apr 2025 22:40:18 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '5' X-Amzn-Bedrock-Invocation-Latency: - '196' X-Amzn-Bedrock-Output-Token-Count: - '10' x-amzn-RequestId: - c1fd38df-b669-4464-9f4b-4cf32c32fde8 status: code: 200 message: OK version: 1 test_invoke_model_with_content[cohere.command].yaml000066400000000000000000000034161527070072300503220ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "prompt": "Say this is a test", "max_tokens": 10, "temperature": 0.8, "p": 1, "stop_sequences": [ "|" ] } headers: Content-Length: - '103' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.3.0 md/arch#arm64 lang/python#3.10.16 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250410T223753Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-461bf523-e7a0388221ce8a1d7a31fc5b;Parent=a55a9398f0f3d364;Sampled=1 amz-sdk-invocation-id: - 17fbe175-373f-4c10-8047-8bba2fd8e22c amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/cohere.command-light-text-v14/invoke response: body: string: |- { "id": "a09c1c60-6608-482a-b98d-764e4d87fcd1", "generations": [ { "id": "8b38ce59-5f77-4b79-82c3-58eb72d4b1a2", "text": " Let it be a test of knowledge, skills,", "finish_reason": "MAX_TOKENS" } ], "prompt": "Say this is a test" } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 10 Apr 2025 22:37:53 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '5' X-Amzn-Bedrock-Invocation-Latency: - '248' X-Amzn-Bedrock-Output-Token-Count: - '10' x-amzn-RequestId: - a09c1c60-6608-482a-b98d-764e4d87fcd1 status: code: 200 message: OK version: 1 test_invoke_model_with_content[meta.llama].yaml000066400000000000000000000030771527070072300474560ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "prompt": "Say this is a test", "max_gen_len": 10, "temperature": 0.8, "top_p": 1 } headers: Content-Length: - '83' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.3.0 md/arch#arm64 lang/python#3.10.16 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250410T224059Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-9930be15-db431eb0bf3c21be1ed23dde;Parent=0c4e46fcf0474877;Sampled=1 amz-sdk-invocation-id: - c3afc5f6-9912-4892-b48e-dff457911e1c amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/meta.llama3-1-70b-instruct-v1%3A0/invoke response: body: string: |- { "generation": " post. This is a test post. This is", "prompt_token_count": 5, "generation_token_count": 10, "stop_reason": "length" } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 10 Apr 2025 22:40:59 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '5' X-Amzn-Bedrock-Invocation-Latency: - '604' X-Amzn-Bedrock-Output-Token-Count: - '10' x-amzn-RequestId: - 4b8da626-00b1-4535-b75b-a7e985e9877e status: code: 200 message: OK version: 1 test_invoke_model_with_content[mistral.mistral].yaml000066400000000000000000000031461527070072300505650ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "prompt": "Say this is a test", "max_tokens": 10, "temperature": 0.8, "top_p": 1, "stop": [ "|" ] } headers: Content-Length: - '97' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.3.0 md/arch#arm64 lang/python#3.10.16 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250410T224059Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-332feae9-215a9553da54b4789bf12414;Parent=f3899b50f888d5d7;Sampled=1 amz-sdk-invocation-id: - 2d410cd2-ef68-48af-9cad-b7ddf3b1d844 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/mistral.mistral-7b-instruct-v0%3A2/invoke response: body: string: |- { "outputs": [ { "text": "\n\nA man stands before a crowd of people", "stop_reason": "length" } ] } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 10 Apr 2025 22:41:00 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '6' X-Amzn-Bedrock-Invocation-Latency: - '174' X-Amzn-Bedrock-Output-Token-Count: - '10' x-amzn-RequestId: - 68547039-f30c-4aff-b368-31bb6d9b9d07 status: code: 200 message: OK version: 1 test_invoke_model_with_content_assistant_content_as_string.yaml000066400000000000000000000040341527070072300531200ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "say this is a test" }, { "role": "assistant", "content": "{" } ], "max_tokens": 10, "anthropic_version": "bedrock-2023-05-31" } headers: Content-Length: - '165' User-Agent: - Boto3/1.28.80 md/Botocore#1.31.80 ua/2.0 os/macos#24.5.0 md/arch#arm64 lang/python#3.12.0 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.31.80 X-Amz-Date: - 20250528T092318Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-313b2456-f7fd3ee19dd37dd03a4be55a;Parent=1533ee3b8e477491;Sampled=1 amz-sdk-invocation-id: - 0a756b13-c341-415c-b114-337ce59bdd11 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-3-5-haiku-20241022-v1%3A0/invoke response: body: string: |- { "id": "msg_bdrk_01Rxpterf4rGAoFZVDpEqFEF", "type": "message", "role": "assistant", "model": "claude-3-5-haiku-20241022", "content": [ { "type": "text", "text": "this is a test}" } ], "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 13, "output_tokens": 8 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Wed, 28 May 2025 09:23:18 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '13' X-Amzn-Bedrock-Invocation-Latency: - '608' X-Amzn-Bedrock-Output-Token-Count: - '8' x-amzn-RequestId: - 4458f14e-5cde-44fb-a1ea-881ed935fb73 status: code: 200 message: OK version: 1 test_invoke_model_with_content_different_events[amazon.nova].yaml000066400000000000000000000052161527070072300533010ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] }, { "role": "assistant", "content": [ { "text": "This is a test" } ] }, { "role": "user", "content": [ { "text": "Say again this is a test" } ] } ], "inferenceConfig": { "max_new_tokens": 10 }, "schemaVersion": "messages-v1", "system": [ { "text": "You are a friendly model" } ] } headers: Content-Length: - '335' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250213T113028Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-ebcc2db9-853da8761a55ed21fab8f371;Parent=39df94de2ed40f10;Sampled=1 amz-sdk-invocation-id: - 6928a695-340b-4f03-b1dd-ec4a42a32f49 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/invoke response: body: string: |- { "output": { "message": { "content": [ { "text": "Again, this is a test. If you need" } ], "role": "assistant" } }, "stopReason": "max_tokens", "usage": { "inputTokens": 25, "outputTokens": 10, "totalTokens": 35, "cacheReadInputTokenCount": 0, "cacheWriteInputTokenCount": 0 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 13 Feb 2025 11:30:29 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Cache-Read-Input-Token-Count: - '0' X-Amzn-Bedrock-Cache-Write-Input-Token-Count: - '0' X-Amzn-Bedrock-Input-Token-Count: - '25' X-Amzn-Bedrock-Invocation-Latency: - '212' X-Amzn-Bedrock-Output-Token-Count: - '10' x-amzn-RequestId: - 2d2b151c-a618-4f59-a6d6-e3caf0591a5e status: code: 200 message: OK version: 1 test_invoke_model_with_content_different_events[anthropic.claude].yaml000066400000000000000000000047261527070072300543020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test", "type": "text" } ] }, { "role": "assistant", "content": [ { "text": "This is a test", "type": "text" } ] }, { "role": "user", "content": [ { "text": "Say again this is a test", "type": "text" } ] } ], "anthropic_version": "bedrock-2023-05-31", "system": "You are a friendly model", "max_tokens": 10 } headers: Content-Length: - '357' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250213T113029Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-ebb1f004-773abbf27202dcf183809217;Parent=e7ecc8b27ef7ce36;Sampled=1 amz-sdk-invocation-id: - 656f63a7-9bca-490b-953e-1d4a126a79da amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-v2/invoke response: body: string: |- { "id": "msg_bdrk_01UnsouyLmsvQU98E9WzpGCp", "type": "message", "role": "assistant", "model": "claude-2.0", "content": [ { "type": "text", "text": "This is a test" } ], "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 37, "output_tokens": 8 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 13 Feb 2025 11:30:30 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '37' X-Amzn-Bedrock-Invocation-Latency: - '335' X-Amzn-Bedrock-Output-Token-Count: - '8' x-amzn-RequestId: - 3daf61e4-4dbc-4c62-af7b-a8b395e4156c status: code: 200 message: OK version: 1 test_invoke_model_with_content_tool_call[amazon.nova].yaml000066400000000000000000000214101527070072300517110ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today? Please expect one tool call for Seattle and one for San Francisco" } ] } ], "inferenceConfig": { "max_new_tokens": 1000 }, "schemaVersion": "messages-v1", "toolConfig": { "tools": [ { "toolSpec": { "name": "get_current_weather", "description": "Get the current weather in a given location.", "inputSchema": { "json": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } } } ] } } headers: Content-Length: - '552' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250325T104304Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-718e449f-1e7fbb291880eb98876a4cc3;Parent=fad0d08c42fc3875;Sampled=1 amz-sdk-invocation-id: - aecefc62-8b85-45ab-8191-e8449cd2762d amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/invoke response: body: string: |- { "output": { "message": { "content": [ { "text": " To provide the current weather in both Seattle and San Francisco, I will use the `get_current_weather` tool for each city separately. I will start with Seattle and then proceed to San Francisco.\n" }, { "toolUse": { "name": "get_current_weather", "toolUseId": "a385bacb-2011-4f60-b0d3-1455602f4438", "input": { "location": "Seattle" } } }, { "toolUse": { "name": "get_current_weather", "toolUseId": "9b02c6fa-4676-4ab5-b2e4-015e86994df7", "input": { "location": "San Francisco" } } } ], "role": "assistant" } }, "stopReason": "tool_use", "usage": { "inputTokens": 427, "outputTokens": 162, "totalTokens": 589, "cacheReadInputTokenCount": 0, "cacheWriteInputTokenCount": 0 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Tue, 25 Mar 2025 10:43:05 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Cache-Read-Input-Token-Count: - '0' X-Amzn-Bedrock-Cache-Write-Input-Token-Count: - '0' X-Amzn-Bedrock-Input-Token-Count: - '427' X-Amzn-Bedrock-Invocation-Latency: - '433' X-Amzn-Bedrock-Output-Token-Count: - '162' x-amzn-RequestId: - e2d1ae4d-3b03-47d1-b527-647a923c3844 status: code: 200 message: OK - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today? Please expect one tool call for Seattle and one for San Francisco" } ] }, { "content": [ { "text": " To provide the current weather in both Seattle and San Francisco, I will use the `get_current_weather` tool for each city separately. I will start with Seattle and then proceed to San Francisco.\n" }, { "toolUse": { "name": "get_current_weather", "toolUseId": "a385bacb-2011-4f60-b0d3-1455602f4438", "input": { "location": "Seattle" } } }, { "toolUse": { "name": "get_current_weather", "toolUseId": "9b02c6fa-4676-4ab5-b2e4-015e86994df7", "input": { "location": "San Francisco" } } } ], "role": "assistant" }, { "role": "user", "content": [ { "toolResult": { "toolUseId": "a385bacb-2011-4f60-b0d3-1455602f4438", "content": [ { "json": { "weather": "50 degrees and raining" } } ] } }, { "toolResult": { "toolUseId": "9b02c6fa-4676-4ab5-b2e4-015e86994df7", "content": [ { "json": { "weather": "70 degrees and sunny" } } ] } } ] } ], "inferenceConfig": { "max_new_tokens": 1000 }, "schemaVersion": "messages-v1", "toolConfig": { "tools": [ { "toolSpec": { "name": "get_current_weather", "description": "Get the current weather in a given location.", "inputSchema": { "json": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } } } ] } } headers: Content-Length: - '1387' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250325T104305Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-d911cb99-61b0e8799bdc802df78a2c16;Parent=fe95b1cfeecf2dcd;Sampled=1 amz-sdk-invocation-id: - d7528c91-631f-459f-9d4b-ba4cceb1e08f amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/invoke response: body: string: |- { "output": { "message": { "content": [ { "text": " I have received the weather information for both Seattle and San Francisco. Now I will compile this information and present it to the User.\n\nThe current weather in Seattle is 50 degrees and it is raining. In San Francisco, the weather is 70 degrees and sunny." } ], "role": "assistant" } }, "stopReason": "end_turn", "usage": { "inputTokens": 566, "outputTokens": 60, "totalTokens": 626, "cacheReadInputTokenCount": 0, "cacheWriteInputTokenCount": 0 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Tue, 25 Mar 2025 10:43:06 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Cache-Read-Input-Token-Count: - '0' X-Amzn-Bedrock-Cache-Write-Input-Token-Count: - '0' X-Amzn-Bedrock-Input-Token-Count: - '566' X-Amzn-Bedrock-Invocation-Latency: - '402' X-Amzn-Bedrock-Output-Token-Count: - '60' x-amzn-RequestId: - 085cca26-6cd7-4a4a-84bb-dbedfc44d806 status: code: 200 message: OK version: 1 test_invoke_model_with_content_tool_call[anthropic.claude].yaml000066400000000000000000000176761527070072300527300ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today? Please expect one tool call for Seattle and one for San Francisco", "type": "text" } ] } ], "anthropic_version": "bedrock-2023-05-31", "tools": [ { "name": "get_current_weather", "description": "Get the current weather in a given location.", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } ], "max_tokens": 1000 } headers: Content-Length: - '515' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250325T104306Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-707dba6e-fd8c6f2155d8bb2cca4f2be9;Parent=4b0c1b6755b4ba5f;Sampled=1 amz-sdk-invocation-id: - d352356d-f036-41bb-9089-fc9401621c09 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-3-5-sonnet-20240620-v1%3A0/invoke response: body: string: |- { "id": "msg_bdrk_01Vcemt76oWJo739rm2hmaxn", "type": "message", "role": "assistant", "model": "claude-3-5-sonnet-20240620", "content": [ { "type": "text", "text": "Certainly! I'll check the current weather for both Seattle and San Francisco using the available tool. I'll make two separate calls to the get_current_weather function, one for each city." }, { "type": "tool_use", "id": "toolu_bdrk_01Y5MJKoHE4VJ5ZrhcVfM1gP", "name": "get_current_weather", "input": { "location": "Seattle" } }, { "type": "tool_use", "id": "toolu_bdrk_014yQPSMntXHRmzGYxCbmBHE", "name": "get_current_weather", "input": { "location": "San Francisco" } } ], "stop_reason": "tool_use", "stop_sequence": null, "usage": { "input_tokens": 392, "output_tokens": 135 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Tue, 25 Mar 2025 10:43:10 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '392' X-Amzn-Bedrock-Invocation-Latency: - '3712' X-Amzn-Bedrock-Output-Token-Count: - '135' x-amzn-RequestId: - e35fc4e1-e704-4785-985d-684916e41fd1 status: code: 200 message: OK - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today? Please expect one tool call for Seattle and one for San Francisco", "type": "text" } ] }, { "role": "assistant", "content": [ { "type": "text", "text": "Certainly! I'll check the current weather for both Seattle and San Francisco using the available tool. I'll make two separate calls to the get_current_weather function, one for each city." }, { "type": "tool_use", "id": "toolu_bdrk_01Y5MJKoHE4VJ5ZrhcVfM1gP", "name": "get_current_weather", "input": { "location": "Seattle" } }, { "type": "tool_use", "id": "toolu_bdrk_014yQPSMntXHRmzGYxCbmBHE", "name": "get_current_weather", "input": { "location": "San Francisco" } } ] }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_bdrk_01Y5MJKoHE4VJ5ZrhcVfM1gP", "content": "50 degrees and raining" }, { "type": "tool_result", "tool_use_id": "toolu_bdrk_014yQPSMntXHRmzGYxCbmBHE", "content": "70 degrees and sunny" } ] } ], "anthropic_version": "bedrock-2023-05-31", "tools": [ { "name": "get_current_weather", "description": "Get the current weather in a given location.", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } ], "max_tokens": 1000 } headers: Content-Length: - '1299' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250325T104310Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-60501822-2add5a653af51ee5ebd4ee49;Parent=66a9d0972d34fef2;Sampled=1 amz-sdk-invocation-id: - 011563b5-68d3-49d6-8951-87739673b32c amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-3-5-sonnet-20240620-v1%3A0/invoke response: body: string: |- { "id": "msg_bdrk_0177fGp1jEHWhhQXD31c6BEm", "type": "message", "role": "assistant", "model": "claude-3-5-sonnet-20240620", "content": [ { "type": "text", "text": "Based on the results from our weather tool, here's the current weather for Seattle and San Francisco:\n\n1. Seattle: The current weather in Seattle is 50 degrees and raining.\n2. San Francisco: The current weather in San Francisco is 70 degrees and sunny.\n\nAs you can see, the two cities are experiencing quite different weather conditions today. Seattle is having a cooler, rainy day, which is fairly typical for the Pacific Northwest. On the other hand, San Francisco is enjoying warmer temperatures and sunshine, making for a pleasant day in the Bay Area.\n\nIs there anything else you'd like to know about the weather in these cities or any other locations?" } ], "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 604, "output_tokens": 146 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Tue, 25 Mar 2025 10:43:15 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '604' X-Amzn-Bedrock-Invocation-Latency: - '4047' X-Amzn-Bedrock-Output-Token-Count: - '146' x-amzn-RequestId: - b34cbc84-44fe-4217-84f1-0b897a6880e0 status: code: 200 message: OK version: 1 test_invoke_model_with_content_user_content_as_string.yaml000066400000000000000000000037231527070072300520710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": "say this is a test" } ], "max_tokens": 10, "anthropic_version": "bedrock-2023-05-31" } headers: Content-Length: - '126' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250306T091535Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-fcd0825a-03f328cca8cde3e741cd83b4;Parent=5a8eccf8d7b031e7;Sampled=1 amz-sdk-invocation-id: - e8a6c9c5-8a45-4ad7-881b-0761d121abc7 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-3-5-sonnet-20240620-v1%3A0/invoke response: body: string: |- { "id": "msg_bdrk_01SjMAZgb8kNMweUzf3moSbU", "type": "message", "role": "assistant", "model": "claude-3-5-sonnet-20240620", "content": [ { "type": "text", "text": "This is a test." } ], "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 12, "output_tokens": 8 } } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 06 Mar 2025 09:15:36 GMT Set-Cookie: test_set_cookie X-Amzn-Bedrock-Input-Token-Count: - '12' X-Amzn-Bedrock-Invocation-Latency: - '544' X-Amzn-Bedrock-Output-Token-Count: - '8' x-amzn-RequestId: - e97cb4f9-61fe-4a62-a29a-a582ddd17414 status: code: 200 message: OK version: 1 test_invoke_model_with_invalid_model.yaml000066400000000000000000000023241527070072300463600ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: null headers: Content-Length: - '0' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.0.0 md/arch#arm64 lang/python#3.13.1 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250206T051240Z X-Amzn-Trace-Id: - Root=1-3d97333a-f0919ab70fdb8836533fe2df;Parent=c01e27c4adb2e184;Sampled=1 amz-sdk-invocation-id: - a166efb6-e7f0-4765-aa2c-ded9afd5cb87 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/does-not-exist/invoke response: body: string: |- { "message": "The provided model identifier is invalid." } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 06 Feb 2025 05:12:40 GMT Set-Cookie: test_set_cookie x-amzn-ErrorType: - ValidationException:http://internal.amazon.com/coral/com.amazon.bedrock/ x-amzn-RequestId: - 3df0e828-5c24-46cc-8dbc-8b282bfc5146 status: code: 400 message: Bad Request version: 1 test_invoke_model_with_response_stream_close_before_consumption[amazon.nova].yaml000066400000000000000000000117601527070072300565740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] } ], "inferenceConfig": { "max_new_tokens": 10, "temperature": 0.8, "topP": 1, "stopSequences": [ "|" ] }, "schemaVersion": "messages-v1" } headers: Content-Length: - '207' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.4.0 md/arch#arm64 lang/python#3.12.0 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250509T104611Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-5213a530-965d38a8ed67e35669b3df67;Parent=0a0288bcb5638afa;Sampled=1 amz-sdk-invocation-id: - 3c22bafe-b2d4-4bb1-95f4-81103f04cfa9 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/invoke-with-response-stream response: body: string: !!binary | AAAAsgAAAEvuKxwFCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SnRaWE56WVdkbFUzUmhj blFpT25zaWNtOXNaU0k2SW1GemMybHpkR0Z1ZENKOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5v cCJ9Z2/zBQAAAPIAAABLtthETAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpX NTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pU1hRaWZTd2lZMjl1ZEdW dWRFSnNiMk5yU1c1a1pYZ2lPakI5ZlE9PSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5 ekFCQ0RFRkdISUpLTE1OIn3kNVULAAAAzAAAAEuoyUKrCzpldmVudC10eXBlBwAFY2h1bmsNOmNv bnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRl cyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJ am93ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdIIn0XD1FEAAAB IwAAAEvJEcEACzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRF Wld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lJSE52ZFc1a2N5QnNhV3RsSUhsdmRTZHla U0JwYm1sMGFXRjBhVzVuSW4wc0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3hmWDA9IiwicCI6 ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowIn07 YX/vAAAAyQAAAEtgKc3bCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxp Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFt eHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam94ZlgwPSIsInAiOiJhYmNk ZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFIn2N06RmAAAA4gAAAEvWONPOCzpldmVudC10eXBl BwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA BWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZl eUowWlhoMElqb2lJR0VnZEdWemRDQnZjaUo5TENKamIyNTBaVzUwUW14dlkydEpibVJsZUNJNk1u MTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcCJ9phCwbgAAALgAAABLpJsEpAs6ZXZlbnQtdHlwZQcA BWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVl dmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5 amEwbHVaR1Y0SWpveWZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW4ifawRkW0AAAC4AAAAS6SbBKQL OmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKdFpYTnpZV2RsVTNSdmNDSTZleUp6ZEc5d1Vt VmhjMjl1SWpvaWJXRjRYM1J2YTJWdWN5SjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1uIn3/LlyA AAACPAAAAEutNbP9CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SnRaWFJoWkdGMFlTSTZl eUoxYzJGblpTSTZleUpwYm5CMWRGUnZhMlZ1Y3lJNk5Td2liM1YwY0hWMFZHOXJaVzV6SWpveE1D d2lZMkZqYUdWU1pXRmtTVzV3ZFhSVWIydGxia052ZFc1MElqb3dMQ0pqWVdOb1pWZHlhWFJsU1c1 d2RYUlViMnRsYmtOdmRXNTBJam93ZlN3aWJXVjBjbWxqY3lJNmUzMHNJblJ5WVdObElqcDdmWDBz SW1GdFlYcHZiaTFpWldSeWIyTnJMV2x1ZG05allYUnBiMjVOWlhSeWFXTnpJanA3SW1sdWNIVjBW RzlyWlc1RGIzVnVkQ0k2TlN3aWIzVjBjSFYwVkc5clpXNURiM1Z1ZENJNk1UQXNJbWx1ZG05allY UnBiMjVNWVhSbGJtTjVJam94Tmpjc0ltWnBjbk4wUW5sMFpVeGhkR1Z1WTNraU9qVTRMQ0pqWVdO b1pWSmxZV1JKYm5CMWRGUnZhMlZ1UTI5MWJuUWlPakFzSW1OaFkyaGxWM0pwZEdWSmJuQjFkRlJ2 YTJWdVEyOTFiblFpT2pCOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoifeNI JRA= headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Fri, 09 May 2025 10:46:12 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - 9c19e136-9600-42bd-9ecc-155abfd7b32b status: code: 200 message: OK version: 1 test_invoke_model_with_response_stream_close_before_consumption[amazon.titan].yaml000066400000000000000000000043441527070072300567500ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "inputText": "Say this is a test", "textGenerationConfig": { "maxTokenCount": 10, "temperature": 0.8, "topP": 1, "stopSequences": [ "|" ] } } headers: Content-Length: - '137' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.4.0 md/arch#arm64 lang/python#3.12.0 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250509T104612Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-aacd9195-44986c6d8f97a81aa0b3e92f;Parent=c40ea6db4768abf2;Sampled=1 amz-sdk-invocation-id: - 3f596967-dd06-445e-983e-573250f2a2ba amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.titan-text-lite-v1/invoke-with-response-stream response: body: string: !!binary | AAACAgAAAEuzJLUaCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SnZkWFJ3ZFhSVVpYaDBJ am9pSUdOdmJXMWxiblJjYmtobGJHeHZJU0JKSUdGdElHRWdZMjl0Y0hWMFpYSWdjSEp2WjNKaGJT QmtaWE5wWjI1bFpDSXNJbWx1WkdWNElqb3dMQ0owYjNSaGJFOTFkSEIxZEZSbGVIUlViMnRsYmtO dmRXNTBJam94TUN3aVkyOXRjR3hsZEdsdmJsSmxZWE52YmlJNklreEZUa2RVU0NJc0ltbHVjSFYw VkdWNGRGUnZhMlZ1UTI5MWJuUWlPalVzSW1GdFlYcHZiaTFpWldSeWIyTnJMV2x1ZG05allYUnBi MjVOWlhSeWFXTnpJanA3SW1sdWNIVjBWRzlyWlc1RGIzVnVkQ0k2TlN3aWIzVjBjSFYwVkc5clpX NURiM1Z1ZENJNk1UQXNJbWx1ZG05allYUnBiMjVNWVhSbGJtTjVJam8yTVRZc0ltWnBjbk4wUW5s MFpVeGhkR1Z1WTNraU9qWXhObjE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eCJ9/QFZ eg== headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Fri, 09 May 2025 10:46:12 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - 9fa89295-177e-46a5-a898-1ebacad2b339 status: code: 200 message: OK version: 1 test_invoke_model_with_response_stream_close_before_consumption[anthropic.claude].yaml000066400000000000000000000171041527070072300575660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test", "type": "text" } ] } ], "anthropic_version": "bedrock-2023-05-31", "max_tokens": 10, "temperature": 0.8, "top_p": 1, "stop_sequences": [ "|" ] } headers: Content-Length: - '211' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.4.0 md/arch#arm64 lang/python#3.12.0 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250509T104613Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-9e3f0b7e-0294af37e10f46587c2ff552;Parent=b47dc7d60cebe563;Sampled=1 amz-sdk-invocation-id: - 604b5dfc-d5b4-494b-bc9a-ec8df8a3005e amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-v2/invoke-with-response-stream response: body: string: !!binary | AAABqQAAAEsyR2kzCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaWJXVnpj MkZuWlY5emRHRnlkQ0lzSW0xbGMzTmhaMlVpT25zaWFXUWlPaUp0YzJkZlltUnlhMTh3TVZoeFpV dHRjbGhhUXpaalJXcFdNV1ZHVFhNelpuRWlMQ0owZVhCbElqb2liV1Z6YzJGblpTSXNJbkp2YkdV aU9pSmhjM05wYzNSaGJuUWlMQ0p0YjJSbGJDSTZJbU5zWVhWa1pTMHlMakFpTENKamIyNTBaVzUw SWpwYlhTd2ljM1J2Y0Y5eVpXRnpiMjRpT201MWJHd3NJbk4wYjNCZmMyVnhkV1Z1WTJVaU9tNTFi R3dzSW5WellXZGxJanA3SW1sdWNIVjBYM1J2YTJWdWN5STZNVFFzSW05MWRIQjFkRjkwYjJ0bGJu TWlPakY5ZlgwPSIsInAiOiJhYmMifXnGhg4AAADtAAAAS1RoRB8LOmV2ZW50LXR5cGUHAAVjaHVu aw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7 ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOXpkR0Z5ZENJc0ltbHVaR1Y0 SWpvd0xDSmpiMjUwWlc1MFgySnNiMk5ySWpwN0luUjVjR1VpT2lKMFpYaDBJaXdpZEdWNGRDSTZJ aUo5ZlE9PSIsInAiOiJhYmNkZWZnaGlqa2xtbm8ifaLt8+IAAAD3AAAAS344yzwLOmV2ZW50LXR5 cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBl BwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJ c0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJs ZUhRaU9pSlBhMkY1SW4xOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5In3RNmhQAAAA +gAAAEuGqA+NCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1 ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhCbElqb2lk R1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlzSW4xOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJz dHV2d3h5ekFCQ0RFRiJ9FoW9wwAAARAAAABLL5ADVgs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250 ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMi OiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lzSW1sdVpHVjRJam93TENK a1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJc0luUmxlSFFpT2lJZ1NTSjlmUT09 IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldY In0XxEMvAAAA/gAAAEtzKKlNCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFw cGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpv aVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUow ZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnYUdWaGNpSjlmUT09IiwicCI6ImFi Y2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUIifQeFt1sAAAEdAAAAS9cAx+cLOmV2ZW50LXR5cGUH AAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0lt bHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhR aU9pSmtJSGx2ZFNKOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZH SElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0NTYifRTiBqsAAADyAAAAS7bYREwLOmV2ZW50LXR5cGUH AAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0lt bHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhR aU9pSWdjMkY1SW4xOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdCJ9maZN8AAAAQkAAABLQmD2 pQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTpt ZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlq YTE5a1pXeDBZU0lzSW1sdVpHVjRJam93TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRGOWta V3gwWVNJc0luUmxlSFFpT2lJZ1hDSWlmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3 eHl6QUJDREVGR0hJSktMTU5PUFEifdg4lJMAAAD9AAAASzSI050LOmV2ZW50LXR5cGUHAAVjaHVu aw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7 ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0 SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSlRZ WGtpZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFIn3rpTfQAAAA7QAA AEtUaEQfCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pz b24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5 aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhCbElqb2lkR1Y0 ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnZEdocGN5SjlmUT09IiwicCI6ImFiY2RlZmdoaWprIn1S +sniAAABFQAAAEvncIwmCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxp Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVky OXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhC bElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnYVhNaWZYMD0iLCJwIjoiYWJjZGVmZ2hp amtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMiJ9og1TIwAAALAA AABLlOtPZQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9q c29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRG OWliRzlqYTE5emRHOXdJaXdpYVc1a1pYZ2lPakI5IiwicCI6ImFiY2RlZmdoaWprbG1uIn1pSvoe AAABHwAAAEutwJSHCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaWJXVnpj MkZuWlY5a1pXeDBZU0lzSW1SbGJIUmhJanA3SW5OMGIzQmZjbVZoYzI5dUlqb2liV0Y0WDNSdmEy VnVjeUlzSW5OMGIzQmZjMlZ4ZFdWdVkyVWlPbTUxYkd4OUxDSjFjMkZuWlNJNmV5SnZkWFJ3ZFhS ZmRHOXJaVzV6SWpveE1IMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkMifffP mzoAAAFnAAAAS2RiP4kLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pYldW emMyRm5aVjl6ZEc5d0lpd2lZVzFoZW05dUxXSmxaSEp2WTJzdGFXNTJiMk5oZEdsdmJrMWxkSEpw WTNNaU9uc2lhVzV3ZFhSVWIydGxia052ZFc1MElqb3hOQ3dpYjNWMGNIVjBWRzlyWlc1RGIzVnVk Q0k2TVRBc0ltbHVkbTlqWVhScGIyNU1ZWFJsYm1ONUlqbzBNemdzSW1acGNuTjBRbmwwWlV4aGRH VnVZM2tpT2pJNE1YMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElK S0xNTk9QUVJTVFVWVyJ9F3ii3Q== headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Fri, 09 May 2025 10:46:13 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - 931a776b-10df-4816-bd25-1702ffb34597 status: code: 200 message: OK version: 1 test_invoke_model_with_response_stream_handles_stream_error.yaml000066400000000000000000000043301527070072300532440ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "inputText": "Say this is a test", "textGenerationConfig": { "maxTokenCount": 10, "temperature": 0.8, "topP": 1, "stopSequences": [ "|" ] } } headers: Content-Length: - '137' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.0.0 md/arch#arm64 lang/python#3.13.1 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250206T051244Z X-Amzn-Trace-Id: - Root=1-6d8848b1-ff90662e586d784cfceb6d31;Parent=f81089be3b148d12;Sampled=1 amz-sdk-invocation-id: - 4159154e-4b3e-4e7d-adc6-38a7cf144ecd amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.titan-text-lite-v1/invoke-with-response-stream response: body: string: !!binary | AAACJgAAAEuHZTzeCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SnZkWFJ3ZFhSVVpYaDBJ am9pSUdOdmJXMWxiblJjYmtobGJHeHZJU0JKSUdGdElHRWdZMjl0Y0hWMFpYSWdjSEp2WjNKaGJT QmtaWE5wWjI1bFpDSXNJbWx1WkdWNElqb3dMQ0owYjNSaGJFOTFkSEIxZEZSbGVIUlViMnRsYmtO dmRXNTBJam94TUN3aVkyOXRjR3hsZEdsdmJsSmxZWE52YmlJNklreEZUa2RVU0NJc0ltbHVjSFYw VkdWNGRGUnZhMlZ1UTI5MWJuUWlPalVzSW1GdFlYcHZiaTFpWldSeWIyTnJMV2x1ZG05allYUnBi MjVOWlhSeWFXTnpJanA3SW1sdWNIVjBWRzlyWlc1RGIzVnVkQ0k2TlN3aWIzVjBjSFYwVkc5clpX NURiM1Z1ZENJNk1UQXNJbWx1ZG05allYUnBiMjVNWVhSbGJtTjVJam8yTXpjc0ltWnBjbk4wUW5s MFpVeGhkR1Z1WTNraU9qWXpObjE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJD REVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU2NyJ9N/iGKA== headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Thu, 06 Feb 2025 05:12:45 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - 1774ea5d-ba6c-450c-a1ea-32807f8d9b27 status: code: 200 message: OK version: 1 test_invoke_model_with_response_stream_invalid_model.yaml000066400000000000000000000023511527070072300516510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: null headers: Content-Length: - '0' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.0.0 md/arch#arm64 lang/python#3.13.1 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250206T051245Z X-Amzn-Trace-Id: - Root=1-aeb6fdcb-0e88bedd693e4370e96e981f;Parent=2d8d8f593ea037bf;Sampled=1 amz-sdk-invocation-id: - 4fe9ff37-80a1-4ceb-9387-a3908aac18f7 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/does-not-exist/invoke-with-response-stream response: body: string: |- { "message": "The provided model identifier is invalid." } headers: Connection: - keep-alive Content-Type: - application/json Date: - Thu, 06 Feb 2025 05:12:46 GMT Set-Cookie: test_set_cookie x-amzn-ErrorType: - ValidationException:http://internal.amazon.com/coral/com.amazon.bedrock/ x-amzn-RequestId: - c35768fc-ba55-40d6-8705-9168e0479c7c status: code: 400 message: Bad Request version: 1 test_invoke_model_with_response_stream_no_content[amazon.nova].yaml000066400000000000000000000225421527070072300536550ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] } ], "inferenceConfig": { "max_new_tokens": 10, "temperature": 0.8, "topP": 1, "stopSequences": [ "|" ] }, "schemaVersion": "messages-v1" } headers: Content-Length: - '207' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250213T140942Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-5ecd0236-4569edae20e05defebab7b00;Parent=0fa1f1768c7b8b5d;Sampled=1 amz-sdk-invocation-id: - 691b201f-920e-4147-8957-efb32f6b9509 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/invoke-with-response-stream response: body: string: !!binary | AAAApwAAAEtGKwT3CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SnRaWE56WVdkbFUzUmhj blFpT25zaWNtOXNaU0k2SW1GemMybHpkR0Z1ZENKOWZRPT0iLCJwIjoiYWJjZGUifZ0S/N0AAADo AAAAS5yIy28LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24v anNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVa V3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaVUzVnlaU0o5TENKamIyNTBaVzUwUW14dlky dEpibVJsZUNJNk1IMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNEIn24dKPn AAAAvwAAAEsWu9i0CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZ MnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam93ZlgwPSIsInAiOiJhYmNkZWZn aGlqa2xtbm9wcXJzdHUifXkwdd4AAADbAAAAS3oJCTkLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29u dGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVz IjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlT SjlMQ0pqYjI1MFpXNTBRbXh2WTJ0SmJtUmxlQ0k2TVgxOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9w cXJzdHUifZgCKpgAAADYAAAASz2pc+kLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBl BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIy NTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3hmWDA9Iiwi cCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1Qifd1gMGIA AAD+AAAAS3MoqU0LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRp b24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlky dEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlFbDBJbjBzSW1OdmJuUmxiblJDYkc5 amEwbHVaR1Y0SWpveWZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZH SElKS0xNTk9QUVJTVFVWV1hZWiJ9v77h3gAAALIAAABL7iscBQs6ZXZlbnQtdHlwZQcABWNodW5r DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Ynl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVa R1Y0SWpveWZYMD0iLCJwIjoiYWJjZGVmZ2gifWca+VsAAAD8AAAASwno+i0LOmV2ZW50LXR5cGUH AAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5 SjBaWGgwSWpvaUlITnZkVzVrY3lKOUxDSmpiMjUwWlc1MFFteHZZMnRKYm1SbGVDSTZNMzE5Iiwi cCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1QifRDZk9oA AACtAAAASwybHFYLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRp b24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlky dFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3pmWDA9IiwicCI6ImFiYyJ9bx6Z qQAAAQMAAABLCNDuBAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNh dGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2 WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pSUd4cGEyVWlmU3dpWTI5dWRHVnVk RUpzYjJOclNXNWtaWGdpT2pSOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpB QkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAifUf8WIAAAAC2AAAASxurusULOmV2ZW50LXR5cGUH AAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JH OWphMGx1WkdWNElqbzBmWDA9IiwicCI6ImFiY2RlZmdoaWprbCJ96cj/hgAAANkAAABLAMlaWQs6 ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlK a1pXeDBZU0k2ZXlKMFpYaDBJam9pSUhsdmRTSjlMQ0pqYjI1MFpXNTBRbXh2WTJ0SmJtUmxlQ0k2 TlgxOSIsInAiOiJhYmNkZWZnaGlqa2xtbm8ifTc/0x4AAADMAAAAS6jJQqsLOmV2ZW50LXR5cGUH AAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JH OWphMGx1WkdWNElqbzFmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVG R0giffYpOMUAAAEFAAAAS4eQG6QLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQ YXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBa VzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlIZGhiblFpZlN3aVky OXVkR1Z1ZEVKc2IyTnJTVzVrWlhnaU9qWjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0 dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIifYXvT9UAAADaAAAAS0dpIIkLOmV2 ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZi blJsYm5SQ2JHOWphMGx1WkdWNElqbzJmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3 eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVViJ9lECpMgAAANgAAABLPalz6Qs6ZXZlbnQtdHlwZQcA BWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVl dmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlK MFpYaDBJam9pSUhSdkluMHNJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam8zZlgwPSIsInAiOiJh YmNkZWZnaGlqa2xtbiJ9X2Y6nAAAAMQAAABLmLkJags6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250 ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMi OiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpv M2ZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoifRQObxwAAADoAAAAS5yIy28L OmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5 SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlHTnZiVzExYm1sallYUmxJbjBzSW1OdmJuUmxiblJDYkc5 amEwbHVaR1Y0SWpvNGZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyIn2XNLLFAAAA5wAAAEse 2Fy+CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24N Om1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lq cDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam80ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9w cXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQ1Njc4In0vrSKrAAAA/AAA AEsJ6PotCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pz b24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4 MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lJR0VpZlN3aVkyOXVkR1Z1ZEVKc2IyTnJTVzVr WlhnaU9qbDlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktM TU5PUFFSU1RVVldYIn39jPY4AAAAwgAAAEsX+fzKCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRl bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6 ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam81 ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3giff4k+OEAAAC0AAAAS2Fr6aULOmV2 ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKdFpYTnpZV2RsVTNSdmNDSTZleUp6ZEc5d1VtVmhj Mjl1SWpvaWJXRjRYM1J2YTJWdWN5SjlmUT09IiwicCI6ImFiY2RlZmdoaWoifXfIDi4AAAIkAAAA S/2lb74LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKdFpYUmhaR0YwWVNJNmV5SjFjMkZu WlNJNmV5SnBibkIxZEZSdmEyVnVjeUk2TlN3aWIzVjBjSFYwVkc5clpXNXpJam94TUN3aVkyRmph R1ZTWldGa1NXNXdkWFJVYjJ0bGJrTnZkVzUwSWpvd0xDSmpZV05vWlZkeWFYUmxTVzV3ZFhSVWIy dGxia052ZFc1MElqb3dmU3dpYldWMGNtbGpjeUk2ZTMwc0luUnlZV05sSWpwN2ZYMHNJbUZ0WVhw dmJpMWlaV1J5YjJOckxXbHVkbTlqWVhScGIyNU5aWFJ5YVdOeklqcDdJbWx1Y0hWMFZHOXJaVzVE YjNWdWRDSTZOU3dpYjNWMGNIVjBWRzlyWlc1RGIzVnVkQ0k2TVRBc0ltbHVkbTlqWVhScGIyNU1Z WFJsYm1ONUlqb3lNRGNzSW1acGNuTjBRbmwwWlV4aGRHVnVZM2tpT2pnMkxDSmpZV05vWlZKbFlX UkpibkIxZEZSdmEyVnVRMjkxYm5RaU9qQXNJbU5oWTJobFYzSnBkR1ZKYm5CMWRGUnZhMlZ1UTI5 MWJuUWlPakI5ZlE9PSIsInAiOiJhYiJ9rO4iMQ== headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Thu, 13 Feb 2025 14:09:43 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - d788a25f-4107-4d7e-9b70-be683e911d01 status: code: 200 message: OK version: 1 test_invoke_model_with_response_stream_no_content[amazon.titan].yaml000066400000000000000000000043351527070072300540310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "inputText": "Say this is a test", "textGenerationConfig": { "maxTokenCount": 10, "temperature": 0.8, "topP": 1, "stopSequences": [ "|" ] } } headers: Content-Length: - '137' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250213T140943Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-b8667c76-484fbe275a69822274a3b153;Parent=1c655aa74d5b9056;Sampled=1 amz-sdk-invocation-id: - 16bd4347-4f6d-4098-8f71-18fa3e3db97c amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.titan-text-lite-v1/invoke-with-response-stream response: body: string: !!binary | AAAB/AAAAEvCtCmICzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SnZkWFJ3ZFhSVVpYaDBJ am9pSUdOdmJXMWxiblJjYmtobGJHeHZJU0JKSUdGdElHRnVJRUZKSUcxdlpHVnNJR1JsZG1Wc2Iz QmxaQ0lzSW1sdVpHVjRJam93TENKMGIzUmhiRTkxZEhCMWRGUmxlSFJVYjJ0bGJrTnZkVzUwSWpv eE1Dd2lZMjl0Y0d4bGRHbHZibEpsWVhOdmJpSTZJa3hGVGtkVVNDSXNJbWx1Y0hWMFZHVjRkRlJ2 YTJWdVEyOTFiblFpT2pVc0ltRnRZWHB2YmkxaVpXUnliMk5yTFdsdWRtOWpZWFJwYjI1TlpYUnlh V056SWpwN0ltbHVjSFYwVkc5clpXNURiM1Z1ZENJNk5Td2liM1YwY0hWMFZHOXJaVzVEYjNWdWRD STZNVEFzSW1sdWRtOWpZWFJwYjI1TVlYUmxibU41SWpvMk1EWXNJbVpwY25OMFFubDBaVXhoZEdW dVkza2lPall3TlgxOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5eiJ9ImKI2Q== headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Thu, 13 Feb 2025 14:09:43 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - d1510378-4044-4e21-b8bc-793dc917cc4f status: code: 200 message: OK version: 1 test_invoke_model_with_response_stream_no_content[anthropic.claude].yaml000066400000000000000000000173741527070072300546600ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test", "type": "text" } ] } ], "anthropic_version": "bedrock-2023-05-31", "max_tokens": 10, "temperature": 0.8, "top_p": 1, "stop_sequences": [ "|" ] } headers: Content-Length: - '211' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250213T140944Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-d0737c0b-da5fdc0d56ffe1dba045b41c;Parent=7ec85426a39c4738;Sampled=1 amz-sdk-invocation-id: - bae563fd-a529-493e-a4fd-b149d9ce8ca1 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-v2/invoke-with-response-stream response: body: string: !!binary | AAAB2QAAAEvLlYn8CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaWJXVnpj MkZuWlY5emRHRnlkQ0lzSW0xbGMzTmhaMlVpT25zaWFXUWlPaUp0YzJkZlltUnlhMTh3TVV4clUw bzBTelY0V1RoclVHOTRPVGN6U21wSFNtOGlMQ0owZVhCbElqb2liV1Z6YzJGblpTSXNJbkp2YkdV aU9pSmhjM05wYzNSaGJuUWlMQ0p0YjJSbGJDSTZJbU5zWVhWa1pTMHlMakFpTENKamIyNTBaVzUw SWpwYlhTd2ljM1J2Y0Y5eVpXRnpiMjRpT201MWJHd3NJbk4wYjNCZmMyVnhkV1Z1WTJVaU9tNTFi R3dzSW5WellXZGxJanA3SW1sdWNIVjBYM1J2YTJWdWN5STZNVFFzSW05MWRIQjFkRjkwYjJ0bGJu TWlPakY5ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1O T1BRUlNUVVZXWFkifWttUokAAAEXAAAAS52w30YLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVu dC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoi ZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOXpkR0Z5ZENJc0ltbHVaR1Y0SWpvd0xDSmpi MjUwWlc1MFgySnNiMk5ySWpwN0luUjVjR1VpT2lKMFpYaDBJaXdpZEdWNGRDSTZJaUo5ZlE9PSIs InAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFla MDEyMzQifZCt+WAAAAEVAAAAS+dwjCYLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBl BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVY QmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJ NmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSlBhMkY1SW4xOSIsInAiOiJh YmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyIn0F qOnTAAAA+wAAAEu7yCY9CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxp Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVky OXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhC bElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlzSW4xOSIsInAiOiJhYmNkZWZnaGlqa2xt bm9wcXJzdHV2d3h5ekFCQ0RFRkcifZiFzkcAAAEUAAAAS9oQpZYLOmV2ZW50LXR5cGUHAAVjaHVu aw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7 ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0 SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSWdT U0o5ZlE9PSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BR UlNUVVZXWFlaMDEifYFpUVoAAADnAAAASx7YXL4LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVu dC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoi ZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmta V3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSWdhR1ZoY2lKOWZR PT0iLCJwIjoiYWJjZGUifZTUHdgAAADpAAAAS6Ho4t8LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29u dGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVz IjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xD SmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSmtJSGx2ZFNK OWZRPT0iLCJwIjoiYWJjZGVmZyJ9L4wO0QAAAPMAAABLi7ht/As6ZXZlbnQtdHlwZQcABWNodW5r DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Ynl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lzSW1sdVpHVjRJ am93TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJc0luUmxlSFFpT2lJZ2My RjVJbjE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dSJ9GxmkFQAAARUAAABL53CMJgs6ZXZl bnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdl LXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pX eDBZU0lzSW1sdVpHVjRJam93TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJ c0luUmxlSFFpT2lJZ1hDSWlmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJD REVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIifSUJEFgAAAEMAAAAS4qAedULOmV2ZW50LXR5cGUH AAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0lt bHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhR aU9pSlRZWGtpZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpL TE1OT1BRUlNUIn2++vJiAAABDgAAAEvwQCq1CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5 SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4 MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnZEdocGN5SjlmUT09 IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSIn2bOfJx AAABDQAAAEu34FBlCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVk R1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhCbElq b2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnYVhNaWZYMD0iLCJwIjoiYWJjZGVmZ2hpamts bW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFUifWuVUmsAAADFAAAAS6XZINoLOmV2 ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOXpk Rzl3SWl3aWFXNWtaWGdpT2pCOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RF RkdISSJ9pG6vOAAAATgAAABL3iFnkws6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhC bElqb2liV1Z6YzJGblpWOWtaV3gwWVNJc0ltUmxiSFJoSWpwN0luTjBiM0JmY21WaGMyOXVJam9p YldGNFgzUnZhMlZ1Y3lJc0luTjBiM0JmYzJWeGRXVnVZMlVpT201MWJHeDlMQ0oxYzJGblpTSTZl eUp2ZFhSd2RYUmZkRzlyWlc1eklqb3hNSDE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3 eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMSJ98M107QAAAUgAAABLJ/OHXAs6ZXZlbnQt dHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5 cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2liV1Z6YzJGblpWOXpkRzl3SWl3aVlXMWhl bTl1TFdKbFpISnZZMnN0YVc1MmIyTmhkR2x2YmsxbGRISnBZM01pT25zaWFXNXdkWFJVYjJ0bGJr TnZkVzUwSWpveE5Dd2liM1YwY0hWMFZHOXJaVzVEYjNWdWRDSTZNVEFzSW1sdWRtOWpZWFJwYjI1 TVlYUmxibU41SWpvMU9EWXNJbVpwY25OMFFubDBaVXhoZEdWdVkza2lPakk0TTMxOSIsInAiOiJh YmNkZWZnaGlqa2xtbm9wcXIifU0wj/U= headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Thu, 13 Feb 2025 14:09:44 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - fef4e97e-8d2b-4faf-a561-e27f49ec42e0 status: code: 200 message: OK version: 1 test_invoke_model_with_response_stream_no_content_different_events[amazon.nova].yaml000066400000000000000000000234451527070072300572720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] }, { "role": "assistant", "content": [ { "text": "This is a test" } ] }, { "role": "user", "content": [ { "text": "Say again this is a test" } ] } ], "inferenceConfig": { "max_new_tokens": 10 }, "schemaVersion": "messages-v1", "system": [ { "text": "You are a friendly model" } ] } headers: Content-Length: - '335' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250213T141144Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-a09ce52d-52219138a15355c5765b2b52;Parent=63b5a9a600291fc0;Sampled=1 amz-sdk-invocation-id: - 9e4a9f20-c0f9-4be7-abfe-2872259d1d32 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/invoke-with-response-stream response: body: string: !!binary | AAAAtwAAAEsmy5N1CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SnRaWE56WVdkbFUzUmhj blFpT25zaWNtOXNaU0k2SW1GemMybHpkR0Z1ZENKOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5v cHFyc3R1In2smnzGAAAA0wAAAEtKeUL4CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlw ZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5Smpi MjUwWlc1MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lRV2RoYVc0aWZT d2lZMjl1ZEdWdWRFSnNiMk5yU1c1a1pYZ2lPakI5ZlE9PSIsInAiOiJhYmNkZSJ9ZkQqpwAAAOEA AABLkZipHgs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9q c29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRH OXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpvd2ZYMD0iLCJwIjoiYWJjZGVmZ2hpamts bW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMiJ9D8dFwAAAAM4AAABL 0gkRyws6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZ U0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pTENKOUxDSmpiMjUwWlc1MFFteHZZMnRKYm1SbGVD STZNWDE5IiwicCI6ImFiY2RlZmdoIn0LqUUiAAAA4gAAAEvWONPOCzpldmVudC10eXBlBwAFY2h1 bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBs dVpHVjRJam94ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpL TE1OT1BRUlNUVVZXWFlaMDEyMyJ9g9JI0AAAAOcAAABLHthcvgs6ZXZlbnQtdHlwZQcABWNodW5r DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Ynl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJ am9pSUhSb2FYTWlmU3dpWTI5dWRHVnVkRUpzYjJOclNXNWtaWGdpT2pKOWZRPT0iLCJwIjoiYWJj ZGVmZ2hpamtsbW5vcHFyc3R1dnd4eSJ9Y/VCfwAAANcAAABLv/nkOAs6ZXZlbnQtdHlwZQcABWNo dW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu dHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEw bHVaR1Y0SWpveWZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElK S0xNTk9QUVJTIn109CNrAAAA4QAAAEuRmKkeCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5 SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lJR2x6SW4w c0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3pmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3Bx cnN0dXZ3In077I9kAAAA2wAAAEt6CQk5CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlw ZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5Smpi MjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam96ZlgwPSIs InAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXIn2Y cRd9AAAA9AAAAEs5mLHsCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxp Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFt eHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lJR0VpZlN3aVkyOXVkR1Z1ZEVK c2IyTnJTVzVrWlhnaU9qUjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJD REVGR0hJSktMTU5PUCJ964agtwAAAMgAAABLXUnkaws6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250 ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMi OiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpv MGZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNEIn0/JaMoAAAA1AAAAEv4 WZ7oCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24N Om1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4MFlT STZleUprWld4MFlTSTZleUowWlhoMElqb2lJSFJsYzNRaWZTd2lZMjl1ZEdWdWRFSnNiMk5yU1c1 a1pYZ2lPalY5ZlE9PSIsInAiOiJhYmNkZWYifTR53DAAAAC9AAAAS2x7i9QLOmV2ZW50LXR5cGUH AAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JH OWphMGx1WkdWNElqbzFmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnMifWGJ/tEAAADrAAAA S9sosb8LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gw WVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUxpSjlMQ0pqYjI1MFpXNTBRbXh2WTJ0SmJtUmxl Q0k2Tm4xOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLIn1bj3f5 AAAA4AAAAEus+ICuCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZ MnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam8yZlgwPSIsInAiOiJhYmNkZWZn aGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEifUip/SkAAAEH AAAAS/1QSMQLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24v anNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVa V3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlFbG1JbjBzSW1OdmJuUmxiblJDYkc5amEw bHVaR1Y0SWpvM2ZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElK S0xNTk9QUVJTVFVWV1hZWjAxMjM0NTY3OCJ9u1LWzQAAALkAAABLmfstFAs6ZXZlbnQtdHlwZQcA BWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVl dmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5 amEwbHVaR1Y0SWpvM2ZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vIn0THZEoAAAA4QAAAEuRmKke CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1l c3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZl eUprWld4MFlTSTZleUowWlhoMElqb2lJSGx2ZFNKOUxDSmpiMjUwWlc1MFFteHZZMnRKYm1SbGVD STZPSDE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3In1pnW0qAAAA3wAAAEuPia/5Czpl dmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh Z2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52 Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam80ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2 d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMCJ9zCnPkAAAAOMAAABL61j6fgs6ZXZlbnQt dHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5 cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZ U0k2ZXlKMFpYaDBJam9pSUc1bFpXUWlmU3dpWTI5dWRHVnVkRUpzYjJOclNXNWtaWGdpT2psOWZR PT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1In1tUIvDAAAAxgAAAEvieVoKCzpldmVudC10 eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlw ZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJu UkNiRzlqYTBsdVpHVjRJam81ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFC In36jRI9AAAA4wAAAEvrWPp+CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFw cGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SnRaWE56WVdk bFUzUnZjQ0k2ZXlKemRHOXdVbVZoYzI5dUlqb2liV0Y0WDNSdmEyVnVjeUo5ZlE9PSIsInAiOiJh YmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQi fT6v9b4AAAIuAAAAS7cVdx8LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBw bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKdFpYUmhaR0Yw WVNJNmV5SjFjMkZuWlNJNmV5SnBibkIxZEZSdmEyVnVjeUk2TWpVc0ltOTFkSEIxZEZSdmEyVnVj eUk2TVRBc0ltTmhZMmhsVW1WaFpFbHVjSFYwVkc5clpXNURiM1Z1ZENJNk1Dd2lZMkZqYUdWWGNt bDBaVWx1Y0hWMFZHOXJaVzVEYjNWdWRDSTZNSDBzSW0xbGRISnBZM01pT250OUxDSjBjbUZqWlNJ NmUzMTlMQ0poYldGNmIyNHRZbVZrY205amF5MXBiblp2WTJGMGFXOXVUV1YwY21samN5STZleUpw Ym5CMWRGUnZhMlZ1UTI5MWJuUWlPakkxTENKdmRYUndkWFJVYjJ0bGJrTnZkVzUwSWpveE1Dd2lh VzUyYjJOaGRHbHZia3hoZEdWdVkza2lPakUzT1N3aVptbHljM1JDZVhSbFRHRjBaVzVqZVNJNk5q QXNJbU5oWTJobFVtVmhaRWx1Y0hWMFZHOXJaVzVEYjNWdWRDSTZNQ3dpWTJGamFHVlhjbWwwWlVs dWNIVjBWRzlyWlc1RGIzVnVkQ0k2TUgxOSIsInAiOiJhYmNkZWZnaGlqa2wifelhO1Y= headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Thu, 13 Feb 2025 14:11:45 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - 5a860b54-1dc8-429c-9855-bbdb14bd8746 status: code: 200 message: OK version: 1 test_invoke_model_with_response_stream_no_content_different_events[anthropic.claude].yaml000066400000000000000000000133251527070072300602620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test", "type": "text" } ] }, { "role": "assistant", "content": [ { "text": "This is a test", "type": "text" } ] }, { "role": "user", "content": [ { "text": "Say again this is a test", "type": "text" } ] } ], "anthropic_version": "bedrock-2023-05-31", "system": "You are a friendly model", "max_tokens": 10 } headers: Content-Length: - '357' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250213T141145Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-3b467cd3-1c0670b2a26cec8cb737e192;Parent=eeae78711a53442d;Sampled=1 amz-sdk-invocation-id: - e429adb3-764e-4929-9f5c-c30f2972174c amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-v2/invoke-with-response-stream response: body: string: !!binary | AAAB3QAAAEs+FS88CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaWJXVnpj MkZuWlY5emRHRnlkQ0lzSW0xbGMzTmhaMlVpT25zaWFXUWlPaUp0YzJkZlltUnlhMTh3TVZCSVdF UjJkbmR3VFVSaVFWSlpjM0poYTJOSE9Ua2lMQ0owZVhCbElqb2liV1Z6YzJGblpTSXNJbkp2YkdV aU9pSmhjM05wYzNSaGJuUWlMQ0p0YjJSbGJDSTZJbU5zWVhWa1pTMHlMakFpTENKamIyNTBaVzUw SWpwYlhTd2ljM1J2Y0Y5eVpXRnpiMjRpT201MWJHd3NJbk4wYjNCZmMyVnhkV1Z1WTJVaU9tNTFi R3dzSW5WellXZGxJanA3SW1sdWNIVjBYM1J2YTJWdWN5STZNemNzSW05MWRIQjFkRjkwYjJ0bGJu TWlPakY5ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1O T1BRUlNUVVZXWFlaMDEyIn03XqsQAAAA9AAAAEs5mLHsCzpldmVudC10eXBlBwAFY2h1bmsNOmNv bnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRl cyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTl6ZEdGeWRDSXNJbWx1WkdWNElqb3dM Q0pqYjI1MFpXNTBYMkpzYjJOcklqcDdJblI1Y0dVaU9pSjBaWGgwSWl3aWRHVjRkQ0k2SWlKOWZR PT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1diJ9n5F/3AAAAQEAAABLchC9ZAs6ZXZlbnQt dHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5 cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZ U0lzSW1sdVpHVjRJam93TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJc0lu UmxlSFFpT2lKVWFHbHpJbjE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVG R0hJIn2TE0apAAAA5wAAAEse2Fy+CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJs SWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZl eUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnYVhNaWZYMD0iLCJwIjoiYWJj ZGVmZ2hpIn2t9aV4AAAA+AAAAEv8aFztCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlw ZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBl WEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlT STZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnWVNKOWZRPT0iLCJwIjoi YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoifRz/PMwAAADkAAAAS1l4Jm4LOmV2ZW50LXR5cGUH AAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0lt bHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhR aU9pSWdkR1Z6ZENKOWZRPT0iLCJwIjoiYWIifVjsMQIAAADVAAAAS8U5t1gLOmV2ZW50LXR5cGUH AAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOXpkRzl3SWl3aWFX NWtaWGdpT2pCOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1O T1BRUlNUVVZXWFkifbyhSHYAAAE2AAAAS2ER2fILOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVu dC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoi ZXlKMGVYQmxJam9pYldWemMyRm5aVjlrWld4MFlTSXNJbVJsYkhSaElqcDdJbk4wYjNCZmNtVmhj Mjl1SWpvaVpXNWtYM1IxY200aUxDSnpkRzl3WDNObGNYVmxibU5sSWpwdWRXeHNmU3dpZFhOaFoy VWlPbnNpYjNWMGNIVjBYM1J2YTJWdWN5STZPSDE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0 dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzIn1GPfIoAAABVgAAAEv4I66/Czpl dmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh Z2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaWJXVnpjMkZuWlY5emRHOXdJaXdp WVcxaGVtOXVMV0psWkhKdlkyc3RhVzUyYjJOaGRHbHZiazFsZEhKcFkzTWlPbnNpYVc1d2RYUlVi MnRsYmtOdmRXNTBJam96Tnl3aWIzVjBjSFYwVkc5clpXNURiM1Z1ZENJNk9Dd2lhVzUyYjJOaGRH bHZia3hoZEdWdVkza2lPalEyTUN3aVptbHljM1JDZVhSbFRHRjBaVzVqZVNJNk16ZzFmWDA9Iiwi cCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGIn2cl+ym headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Thu, 13 Feb 2025 14:11:45 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - f2e57698-369e-455f-ba0c-ee76c7cc9b0e status: code: 200 message: OK version: 1 test_invoke_model_with_response_stream_no_content_tool_call[amazon.nova].yaml000066400000000000000000001234421527070072300557060ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today? Please give one tool call for Seattle and one for San Francisco" } ] } ], "inferenceConfig": { "max_new_tokens": 1000 }, "schemaVersion": "messages-v1", "toolConfig": { "tools": [ { "toolSpec": { "name": "get_current_weather", "description": "Get the current weather in a given location.", "inputSchema": { "json": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } } } ] } } headers: Content-Length: - '550' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250328T095545Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-cc955679-a8c45b468f9db1139b274c15;Parent=0d73ce35f14a3d3d;Sampled=1 amz-sdk-invocation-id: - abb47d5d-46b6-4ffe-a595-392788da86be amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/invoke-with-response-stream response: body: string: !!binary | AAAAvQAAAEtse4vUCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SnRaWE56WVdkbFUzUmhj blFpT25zaWNtOXNaU0k2SW1GemMybHpkR0Z1ZENKOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5v cHFyc3R1dnd4eXpBIn1NL1/3AAABAwAAAEsI0O4ECzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRl bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6 ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lQSFJv YVc1cmFXNW5JbjBzSW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpvd2ZYMD0iLCJwIjoiYWJjZGVm Z2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWVyJ9x+xhrAAAAMQAAABL mLkJags6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJ anA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpvd2ZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5v cHFyc3R1dnd4eXoifaHxL7kAAAD+AAAAS3MoqU0LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVu dC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoi ZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaVBpQlVh R1VpZlN3aVkyOXVkR1Z1ZEVKc2IyTnJTVzVrWlhnaU9qRjlmUT09IiwicCI6ImFiY2RlZmdoaWpr bG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVViJ9j8HqtQAAAMMAAABLKpnVegs6 ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1O dmJuUmxiblJDYkc5amEwbHVaR1Y0SWpveGZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1 dnd4eSJ9jih54QAAAQEAAABLchC9ZAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1 MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pSUZWelpYSWlmU3dp WTI5dWRHVnVkRUpzYjJOclNXNWtaWGdpT2pKOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFy c3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZIn09Iow2AAAAzgAAAEvSCRHLCzpldmVu dC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2Ut dHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5S bGJuUkNiRzlqYTBsdVpHVjRJam95ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5 ekFCQ0RFRkdISUoifSnJvDQAAADNAAAAS5WpaxsLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVu dC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoi ZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlHaGhj eUo5TENKamIyNTBaVzUwUW14dlkydEpibVJsZUNJNk0zMTkiLCJwIjoiYWJjIn3kKUJpAAAAswAA AEvTSzW1CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pz b24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5 d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam96ZlgwPSIsInAiOiJhYmNkZWZnaGkifTcj yJQAAAD5AAAAS8EIdV0LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14 dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlISmxjWFZsYzNSbFpDQjBhR1Vp ZlN3aVkyOXVkR1Z1ZEVKc2IyTnJTVzVrWlhnaU9qUjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6QUJDREUifZE1wvoAAADMAAAAS6jJQqsLOmV2ZW50LXR5cGUHAAVjaHVuaw06 Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5 dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdW NElqbzBmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0gifcuY1BkA AAENAAAAS7fgUGULOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRp b24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlky dEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlHTjFjbkpsYm5RZ2QyVmhkR2hsY2lC cGJpSjlMQ0pqYjI1MFpXNTBRbXh2WTJ0SmJtUmxlQ0k2TlgxOSIsInAiOiJhYmNkZWZnaGlqa2xt bm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVSJ9NBetQgAAANwAAABLyCnVKQs6ZXZl bnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdl LXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJu UmxiblJDYkc5amEwbHVaR1Y0SWpvMWZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4 eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1gifVbHrAMAAAEHAAAAS/1QSMQLOmV2ZW50LXR5cGUH AAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5 SjBaWGgwSWpvaUlIUjNieUo5TENKamIyNTBaVzUwUW14dlkydEpibVJsZUNJNk5uMTkiLCJwIjoi YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0 NTY3OCJ93reH+gAAAMMAAABLKpnVegs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1 MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpvMmZYMD0iLCJw IjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eSJ9xDuFYAAAANcAAABLv/nkOAs6ZXZlbnQtdHlw ZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUH AAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2 ZXlKMFpYaDBJam9pSUdScFptWmxjbVZ1ZENKOUxDSmpiMjUwWlc1MFFteHZZMnRKYm1SbGVDSTZO MzE5IiwicCI6ImFiY2RlIn1klrs/AAAAyQAAAEtgKc3bCzpldmVudC10eXBlBwAFY2h1bmsNOmNv bnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRl cyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJ am8zZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFIn1JzAZsAAABFwAA AEudsN9GCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pz b24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4 MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lJR3h2WTJGMGFXOXVjem9nVTJWaGRIUnNaU0Jo Ym1RZ1UyRnVJRVp5WVc1amFYTmpieUo5TENKamIyNTBaVzUwUW14dlkydEpibVJsZUNJNk9IMTki LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHIn0NoadzAAAAuAAAAEukmwSk CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1l c3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJ bU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam80ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbiJ9y6M3 rAAAANYAAABLgpnNiAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNh dGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2 WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pTGlCSklHTmhiaUo5TENKamIyNTBa VzUwUW14dlkydEpibVJsZUNJNk9YMTkiLCJwIjoiYWJjZGVmZ2gifRG9uKsAAAC9AAAAS2x7i9QL OmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0lt TnZiblJsYm5SQ2JHOWphMGx1WkdWNElqbzVmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnMi fbpDvR0AAAEFAAAAS4eQG6QLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBw bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUw UW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlIVnpaU0IwYUdVZ1hDSWlm U3dpWTI5dWRHVnVkRUpzYjJOclNXNWtaWGdpT2pFd2ZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5v cHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFUifdX1XJwAAADUAAAAS/hZnugLOmV2ZW50 LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10 eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJs Ym5SQ2JHOWphMGx1WkdWNElqb3hNSDE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6 QUJDREVGR0hJSktMTU5PUCJ93t/4vQAAAOsAAABL2yixvws6ZXZlbnQtdHlwZQcABWNodW5rDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0 ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9p WjJWMFgyTjFjbkpsYm5SZmQyVmhkR2hsY2x3aUluMHNJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJ am94TVgxOSIsInAiOiJhYmNkZWZnaGkifRbLt30AAADeAAAAS7LphkkLOmV2ZW50LXR5cGUHAAVj aHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZl bnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWph MGx1WkdWNElqb3hNWDE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJ SktMTU5PUFFSU1RVVldYWVoifaKlpSEAAADhAAAAS5GYqR4LOmV2ZW50LXR5cGUHAAVjaHVuaw06 Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5 dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpv aUlIUnZiMndnZEc4Z2IySjBZV2x1SW4wc0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3hNbjE5 IiwicCI6ImFiY2RlZmcifdw8CY8AAAC/AAAASxa72LQLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29u dGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVz IjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElq b3hNbjE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dSJ9pPMw3gAAAQgAAABLfwDfFQs6ZXZl bnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdl LXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pX eDBZU0k2ZXlKMFpYaDBJam9pSUhSb2FYTWlmU3dpWTI5dWRHVnVkRUpzYjJOclNXNWtaWGdpT2pF emZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJT VFVWV1hZWjAxMjM0NSJ9BtleugAAAMcAAABL3xlzugs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250 ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMi OiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpv eE0zMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkMiffkkJbEAAAD5AAAAS8EI dV0LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06 bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJ NmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlHbHVabTl5YldGMGFXOXVMaUo5TENKamIyNTBaVzUw UW14dlkydEpibVJsZUNJNk1UUjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6 QUJDREUifdkNZbEAAADUAAAAS/hZnugLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBl BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIy NTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3hOSDE5Iiwi cCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUCJ9uNstjQAAAOIA AABL1jjTzgs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9q c29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpX eDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pSUVrZ2QybHNiQ0o5TENKamIyNTBaVzUwUW14 dlkydEpibVJsZUNJNk1UVjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3Aife3+hwAAAADVAAAA S8U5t1gLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3 SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3hOWDE5IiwicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFEifRxkAS0AAADiAAAAS9Y4084LOmV2ZW50LXR5 cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBl BwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJ NmV5SjBaWGgwSWpvaUlHMWhhMlVpZlN3aVkyOXVkR1Z1ZEVKc2IyTnJTVzVrWlhnaU9qRTJmWDA9 IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0In35qHxbAAAAywAAAEsa6Z67CzpldmVudC10eXBl BwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA BWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNi RzlqYTBsdVpHVjRJam94Tm4xOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RF RkcifWL2nHYAAADxAAAAS/F4PpwLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQ YXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBa VzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlIUjNieUo5TENKamIy NTBaVzUwUW14dlkydEpibVJsZUNJNk1UZDlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0 dXZ3eHl6QUJDREVGR0hJIn20qMuFAAAA4wAAAEvrWPp+CzpldmVudC10eXBlBwAFY2h1bmsNOmNv bnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRl cyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJ am94TjMxOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BR UlNUVVZXWFlaMDEyMzQifWvnOlUAAADqAAAAS+ZImA8LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29u dGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVz IjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlI TmxjR0Z5WVhSbElIUnZiMndnWTJGc2JITXNJbjBzSW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpv eE9IMTkiLCJwIjoiYWJjZGVmZ2gifTPRTysAAADOAAAAS9IJEcsLOmV2ZW50LXR5cGUHAAVjaHVu aw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7 ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1 WkdWNElqb3hPSDE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSiJ9 SZv7WQAAAOIAAABL1jjTzgs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBs aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBR bXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pSUc5dVpTQm1iM0lnWldGamFD SjlMQ0pqYjI1MFpXNTBRbXh2WTJ0SmJtUmxlQ0k2TVRsOWZRPT0iLCJwIjoiYWJjZGVmZ2gifeGA +iYAAADaAAAAS0dpIIkLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14 dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3hPWDE5IiwicCI6ImFiY2Rl ZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVViJ9Eq1oOwAAAN0AAABL 9Un8mQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZ U0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pSUd4dlkyRjBhVzl1SW4wc0ltTnZiblJsYm5SQ2JH OWphMGx1WkdWNElqb3lNSDE5IiwicCI6ImFiY2RlZmdoaWprIn2VHcExAAAA3gAAAEuy6YZJCzpl dmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh Z2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52 Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam95TUgxOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2 d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaIn11I/wjAAAA0gAAAEt3GWtICzpldmVudC10 eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlw ZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlT STZleUowWlhoMElqb2lMand2SW4wc0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3lNWDE5Iiwi cCI6ImFiY2RlZmdoIn2tgvkOAAAAyAAAAEtdSeRrCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRl bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6 ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam95 TVgxOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0QifT5w5lAAAAEPAAAAS80g AwULOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06 bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJ NmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaWRHaHBibXRwYm1jaWZTd2lZMjl1ZEdWdWRFSnNiMk5y U1c1a1pYZ2lPakl5ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdI SUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQ1Njc4In2K5UJaAAAAsAAAAEuU609lCzpldmVudC10eXBl BwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA BWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNi RzlqYTBsdVpHVjRJam95TW4xOSIsInAiOiJhYmNkZWYifSQOfsUAAAECAAAASzWwx7QLOmV2ZW50 LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10 eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gw WVNJNmV5SjBaWGgwSWpvaVBseHVJbjBzSW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpveU0zMTki LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZ WjAxMjMifVD14TYAAAC7AAAAS+M7fnQLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBl BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIy NTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3lNMzE5Iiwi cCI6ImFiY2RlZmdoaWprbG1ub3BxIn1anJC8AAABPwAAAEtsAbuDCzpldmVudC10eXBlBwAFY2h1 bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEdGeWRDSTZleUp6ZEdGeWRDSTZleUowYjI5 c1ZYTmxJanA3SW5SdmIyeFZjMlZKWkNJNkltUTRPVGxrWVRnNUxUVmhOamN0TkdGbU1pMWhZVFUw TFRVMFpqWmtNbUZoWkdRME1DSXNJbTVoYldVaU9pSm5aWFJmWTNWeWNtVnVkRjkzWldGMGFHVnlJ bjE5TENKamIyNTBaVzUwUW14dlkydEpibVJsZUNJNk1qUjlmUT09IiwicCI6ImFiY2RlZmdoaSJ9 5j3KswAAAP0AAABLNIjTnQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBs aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBR bXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMGIyOXNWWE5sSWpwN0ltbHVjSFYwSWpvaWUx d2liRzlqWVhScGIyNWNJanBjSWxObFlYUjBiR1ZjSW4waWZYMHNJbU52Ym5SbGJuUkNiRzlqYTBs dVpHVjRJam95TkgxOSIsInAiOiJhYmMifawii5YAAADQAAAASw3ZOCgLOmV2ZW50LXR5cGUHAAVj aHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZl bnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWph MGx1WkdWNElqb3lOSDE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJ SktMIn2luZBuAAABSgAAAEtdM9Q8CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUw Wlc1MFFteHZZMnRUZEdGeWRDSTZleUp6ZEdGeWRDSTZleUowYjI5c1ZYTmxJanA3SW5SdmIyeFZj MlZKWkNJNkltSmxOMkk1WldKaExUSTVNVFF0TkdFd1lTMWlZVEZrTFdVNE5qTmhPREk0TVdNeE9D SXNJbTVoYldVaU9pSm5aWFJmWTNWeWNtVnVkRjkzWldGMGFHVnlJbjE5TENKamIyNTBaVzUwUW14 dlkydEpibVJsZUNJNk1qWjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0In2zxOrsAAAB EQAAAEsS8CrmCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRF Wld4MFlTSTZleUprWld4MFlTSTZleUowYjI5c1ZYTmxJanA3SW1sdWNIVjBJam9pZTF3aWJHOWpZ WFJwYjI1Y0lqcGNJbE5oYmlCR2NtRnVZMmx6WTI5Y0luMGlmWDBzSW1OdmJuUmxiblJDYkc5amEw bHVaR1Y0SWpveU5uMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vIn0kJSA6AAAAwwAAAEsqmdV6Czpl dmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh Z2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52 Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam95Tm4xOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2 d3h5In3IuHR1AAAA0AAAAEsN2TgoCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SnRaWE56 WVdkbFUzUnZjQ0k2ZXlKemRHOXdVbVZoYzI5dUlqb2lkRzl2YkY5MWMyVWlmWDA9IiwicCI6ImFi Y2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUCJ9p7lBxgAAAlUAAABLORem wQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTpt ZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUp0WlhSaFpHRjBZU0k2ZXlKMWMyRm5aU0k2 ZXlKcGJuQjFkRlJ2YTJWdWN5STZOREkzTENKdmRYUndkWFJVYjJ0bGJuTWlPakUyTUN3aVkyRmph R1ZTWldGa1NXNXdkWFJVYjJ0bGJrTnZkVzUwSWpvd0xDSmpZV05vWlZkeWFYUmxTVzV3ZFhSVWIy dGxia052ZFc1MElqb3dmU3dpYldWMGNtbGpjeUk2ZTMwc0luUnlZV05sSWpwN2ZYMHNJbUZ0WVhw dmJpMWlaV1J5YjJOckxXbHVkbTlqWVhScGIyNU5aWFJ5YVdOeklqcDdJbWx1Y0hWMFZHOXJaVzVE YjNWdWRDSTZOREkzTENKdmRYUndkWFJVYjJ0bGJrTnZkVzUwSWpveE5qQXNJbWx1ZG05allYUnBi MjVNWVhSbGJtTjVJam8wTkRJc0ltWnBjbk4wUW5sMFpVeGhkR1Z1WTNraU9qVTRMQ0pqWVdOb1pW SmxZV1JKYm5CMWRGUnZhMlZ1UTI5MWJuUWlPakFzSW1OaFkyaGxWM0pwZEdWSmJuQjFkRlJ2YTJW dVEyOTFiblFpT2pCOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZH SElKS0xNTk9QUSJ9DZpD9w== headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Fri, 28 Mar 2025 09:55:46 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - 9bf238d7-0bf5-4875-8724-e6c2ff941ba0 status: code: 200 message: OK - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today? Please give one tool call for Seattle and one for San Francisco" } ] }, { "role": "assistant", "content": [ { "text": " The User has requested the current weather in two different locations: Seattle and San Francisco. I can use the \"get_current_weather\" tool to obtain this information. I will make two separate tool calls, one for each location.\n" }, { "toolUse": { "toolUseId": "d899da89-5a67-4af2-aa54-54f6d2aadd40", "name": "get_current_weather", "input": { "location": "Seattle" } } }, { "toolUse": { "toolUseId": "be7b9eba-2914-4a0a-ba1d-e863a8281c18", "name": "get_current_weather", "input": { "location": "San Francisco" } } } ] }, { "role": "user", "content": [ { "toolResult": { "toolUseId": "d899da89-5a67-4af2-aa54-54f6d2aadd40", "content": [ { "json": { "weather": "50 degrees and raining" } } ] } }, { "toolResult": { "toolUseId": "be7b9eba-2914-4a0a-ba1d-e863a8281c18", "content": [ { "json": { "weather": "70 degrees and sunny" } } ] } } ] } ], "inferenceConfig": { "max_new_tokens": 1000 }, "schemaVersion": "messages-v1", "toolConfig": { "tools": [ { "toolSpec": { "name": "get_current_weather", "description": "Get the current weather in a given location.", "inputSchema": { "json": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } } } ] } } headers: Content-Length: - '1419' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250328T095546Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-149e307b-cdc3a5488e73393d9aaca023;Parent=797734885975d04a;Sampled=1 amz-sdk-invocation-id: - 74c51134-6231-4283-9969-3b9d30af7934 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/invoke-with-response-stream response: body: string: !!binary | AAAAqQAAAEv5G7qWCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SnRaWE56WVdkbFUzUmhj blFpT25zaWNtOXNaU0k2SW1GemMybHpkR0Z1ZENKOWZRPT0iLCJwIjoiYWJjZGVmZyJ92nUjYgAA AQ0AAABLt+BQZQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlv bi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0 RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pUEhSb2FXNXJhVzVuSW4wc0ltTnZiblJs Ym5SQ2JHOWphMGx1WkdWNElqb3dmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6 QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU2In3CYIM7AAAAugAAAEveW1fECzpldmVu dC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2Ut dHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5S bGJuUkNiRzlqYTBsdVpHVjRJam93ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wIn29kWftAAAB AAAAAEtPcJTUCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRF Wld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lQaUJKSW4wc0ltTnZiblJsYm5SQ2JHOWph MGx1WkdWNElqb3hmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJ SktMTU5PUFFSU1RVVldYWVowMSJ9tRgXQwAAAOQAAABLWXgmbgs6ZXZlbnQtdHlwZQcABWNodW5r DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Ynl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVa R1Y0SWpveGZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xN Tk9QUVJTVFVWV1hZWjAxMjM0NSJ9N5g5GAAAAP8AAABLTkiA/Qs6ZXZlbnQtdHlwZQcABWNodW5r DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Ynl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJ am9pSUdoaGRtVWdjbVZqWldsMlpXUWlmU3dpWTI5dWRHVnVkRUpzYjJOclNXNWtaWGdpT2pKOWZR PT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKSyJ9b+UvSgAAANAA AABLDdk4KAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9q c29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRH OXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpveWZYMD0iLCJwIjoiYWJjZGVmZ2hpamts bW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0wifT6gPnsAAADsAAAAS2kIba8LOmV2ZW50LXR5cGUH AAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5 SjBaWGgwSWpvaUlIUm9aU0IzWldGMGFHVnlJbjBzSW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpv emZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1diJ95BjjVgAAALsAAABL4zt+dAs6ZXZl bnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdl LXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJu UmxiblJDYkc5amEwbHVaR1Y0SWpvemZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHEifR/K8NgA AAD2AAAAS0NY4owLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRp b24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlky dEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlHbHVabTl5YldGMGFXOXVJR1p2Y2lK OUxDSmpiMjUwWlc1MFFteHZZMnRKYm1SbGVDSTZOSDE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3Bx cnN0dXZ3eHl6QUIifUXT110AAADZAAAASwDJWlkLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVu dC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoi ZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqbzBm WDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RV In3W/9dIAAABAwAAAEsI0O4ECzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFw cGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1 MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lJR0p2ZEdnZ1UyVmhkSFJz WlNKOUxDSmpiMjUwWlc1MFFteHZZMnRKYm1SbGVDSTZOWDE5IiwicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSUyJ9mukwAAAAALYAAABLG6u6xQs6ZXZlbnQt dHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5 cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxi blJDYkc5amEwbHVaR1Y0SWpvMWZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsIn0O1VkRAAAA/gAAAEtz KKlNCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24N Om1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4MFlT STZleUprWld4MFlTSTZleUowWlhoMElqb2lJR0Z1WkNCVFlXNGdSbkpoYm1OcGMyTnZMaUJPYjNj aWZTd2lZMjl1ZEdWdWRFSnNiMk5yU1c1a1pYZ2lPalo5ZlE9PSIsInAiOiJhYmNkZWZnaGlqa2xt bm9wcXJzdHV2d3gifSkeAZ0AAAC2AAAASxurusULOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVu dC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoi ZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqbzJm WDA9IiwicCI6ImFiY2RlZmdoaWprbCJ9/IK06QAAAQMAAABLCNDuBAs6ZXZlbnQtdHlwZQcABWNo dW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu dHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpY aDBJam9pSUVrZ1kyRnVJSEJ5YjNacFpHVWlmU3dpWTI5dWRHVnVkRUpzYjJOclNXNWtaWGdpT2pk OWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk8ifQRO iGgAAADYAAAASz2pc+kLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14 dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqbzNmWDA9IiwicCI6ImFiY2Rl ZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1QifdXDjmMAAAEJAAAAS0Jg 9qULOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06 bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJ NmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlIUm9aU0JWYzJWeUluMHNJbU52Ym5SbGJuUkNiRzlq YTBsdVpHVjRJam80ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdI SUpLTE1OT1BRUlNUVVZXWFlaMDEyIn0pkPlgAAAAuQAAAEuZ+y0UCzpldmVudC10eXBlBwAFY2h1 bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBs dVpHVjRJam80ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm8ifeF7mJUAAAEaAAAAS2UgG/cLOmV2 ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5Smta V3gwWVNJNmV5SjBaWGgwSWpvaUlIZHBkR2dnZEdobElISmxjWFZsYzNSbFpDSjlMQ0pqYjI1MFpX NTBRbXh2WTJ0SmJtUmxlQ0k2T1gxOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFC Q0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQ1NjcifS+7vKUAAADhAAAAS5GYqR4LOmV2ZW50 LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10 eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJs Ym5SQ2JHOWphMGx1WkdWNElqbzVmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6 QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIifYpLXlEAAAD9AAAASzSI050LOmV2ZW50LXR5 cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBl BwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJ NmV5SjBaWGgwSWpvaUlHUmxkR0ZwYkhNaWZTd2lZMjl1ZEdWdWRFSnNiMk5yU1c1a1pYZ2lPakV3 ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRIn2i XtanAAAArAAAAEsx+zXmCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxp Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFt eHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam94TUgxOSIsInAiOiJhYiJ9 hwc2DAAAAMwAAABLqMlCqws6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBs aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBR bXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pTGp3dkluMHNJbU52Ym5SbGJu UkNiRzlqYTBsdVpHVjRJam94TVgxOSIsInAiOiJhYiJ9c78fagAAAMIAAABLF/n8ygs6ZXZlbnQt dHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5 cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxi blJDYkc5amEwbHVaR1Y0SWpveE1YMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4In07 Db3dAAAA9wAAAEt+OMs8CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxp Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFt eHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lkR2hwYm10cGJtY2lmU3dpWTI5 dWRHVnVkRUpzYjJOclNXNWtaWGdpT2pFeWZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1 dnd4eXpBQkNERUZHSElKSyJ9N48JAgAAAM4AAABL0gkRyws6ZXZlbnQtdHlwZQcABWNodW5rDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0 ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0 SWpveE1uMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKIn3aB0ty AAAA3wAAAEuPia/5CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZ MnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lQbHh1WEc0aWZTd2lZMjl1ZEdWdWRF SnNiMk5yU1c1a1pYZ2lPakV6ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcSJ9N7YWzgAAAOEA AABLkZipHgs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9q c29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRH OXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpveE0zMTkiLCJwIjoiYWJjZGVmZ2hpamts bW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMiJ9jFAQKgAAAQIAAABL NbDHtAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZ U0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pVkdobEluMHNJbU52Ym5SbGJuUkNiRzlqYTBsdVpH VjRJam94TkgxOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1O T1BRUlNUVVZXWFlaMDEyMyJ95ZivugAAAN8AAABLj4mv+Qs6ZXZlbnQtdHlwZQcABWNodW5rDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0 ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0 SWpveE5IMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9Q UVJTVFVWV1hZWjAifXA8o9wAAAEJAAAAS0Jg9qULOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVu dC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoi ZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlHTjFj bkpsYm5RaWZTd2lZMjl1ZEdWdWRFSnNiMk5yU1c1a1pYZ2lPakUxZlgwPSIsInAiOiJhYmNkZWZn aGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyIn06JltBAAAA wwAAAEsqmdV6CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRU ZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam94TlgxOSIsInAiOiJhYmNkZWZnaGlq a2xtbm9wcXJzdHV2d3h5In2Ut+TbAAABIwAAAEvJEcEACzpldmVudC10eXBlBwAFY2h1bmsNOmNv bnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRl cyI6ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lJ SGRsWVhSb1pYSWdhVzRnVTJWaGRIUnNaU0JwY3lBaWZTd2lZMjl1ZEdWdWRFSnNiMk5yU1c1a1pY Z2lPakUyZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1O T1BRUlNUVVZXWFlaMDEyMzQ1Njc4In05XxO1AAAA5QAAAEtkGA/eCzpldmVudC10eXBlBwAFY2h1 bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBs dVpHVjRJam94Tm4xOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpL TE1OT1BRUlNUVVZXWFlaMDEyMzQ1NiJ9qydgAQAAAOEAAABLkZipHgs6ZXZlbnQtdHlwZQcABWNo dW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu dHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpY aDBJam9pTlRBZ1pHVm5jbVZsY3lCaGJtUWdhWFFpZlN3aVkyOXVkR1Z1ZEVKc2IyTnJTVzVrWlhn aU9qRTNmWDA9IiwicCI6ImFiYyJ9io3kmAAAAOEAAABLkZipHgs6ZXZlbnQtdHlwZQcABWNodW5r DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Ynl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVa R1Y0SWpveE4zMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xN Tk9QUVJTVFVWV1hZWjAxMiJ9Ixa4TAAAAPAAAABLzBgXLAs6ZXZlbnQtdHlwZQcABWNodW5rDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0 ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9p SUdseklISmhhVzVwYm1jdUlFbHVJbjBzSW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpveE9IMTki LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1diJ9wf+umAAAANcAAABLv/nkOAs6ZXZlbnQtdHlw ZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUH AAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJD Ykc5amEwbHVaR1Y0SWpveE9IMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNE RUZHSElKS0xNTk9QUVJTIn1GPnmoAAABEgAAAEtVUFA2CzpldmVudC10eXBlBwAFY2h1bmsNOmNv bnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRl cyI6ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lJ Rk5oYmlCR2NtRnVZMmx6WTI4c0lIUm9aU0o5TENKamIyNTBaVzUwUW14dlkydEpibVJsZUNJNk1U bDlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFS U1RVViJ9d30foAAAAL8AAABLFrvYtAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1 MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpveE9YMTkiLCJw IjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1In3q9PIPAAABGAAAAEsf4EiXCzpldmVudC10eXBlBwAF Y2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2 ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUow WlhoMElqb2lJSGRsWVhSb1pYSWdhWE1nTnpBZ1pHVm5jbVZsY3lKOUxDSmpiMjUwWlc1MFFteHZZ MnRKYm1SbGVDSTZNakI5ZlE9PSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RF RkdISUpLTE1OT1BRUlNUVVZXWCJ9ttcxeQAAAMkAAABLYCnN2ws6ZXZlbnQtdHlwZQcABWNodW5r DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Ynl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVa R1Y0SWpveU1IMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERSJ9gsGuhAAA AN4AAABLsumGSQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlv bi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0 RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pSUdGdVpDQnpkVzV1ZVM0aWZTd2lZMjl1 ZEdWdWRFSnNiMk5yU1c1a1pYZ2lPakl4ZlgwPSIsInAiOiJhYmNkZWZnaCJ996MH6AAAALEAAABL qYtm1Qs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJ anA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpveU1YMTkiLCJwIjoiYWJjZGVmZyJ95Pb+2QAA AMQAAABLmLkJags6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlv bi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUp0WlhOellXZGxVM1J2Y0NJ NmV5SnpkRzl3VW1WaGMyOXVJam9pWlc1a1gzUjFjbTRpZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xt bm9wcXJzdHV2d3h5ekFCQ0QifZbgu+UAAAJgAAAAS1DWkTcLOmV2ZW50LXR5cGUHAAVjaHVuaw06 Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5 dGVzIjoiZXlKdFpYUmhaR0YwWVNJNmV5SjFjMkZuWlNJNmV5SnBibkIxZEZSdmEyVnVjeUk2TlRj eUxDSnZkWFJ3ZFhSVWIydGxibk1pT2pVNExDSmpZV05vWlZKbFlXUkpibkIxZEZSdmEyVnVRMjkx Ym5RaU9qQXNJbU5oWTJobFYzSnBkR1ZKYm5CMWRGUnZhMlZ1UTI5MWJuUWlPakI5TENKdFpYUnlh V056SWpwN2ZTd2lkSEpoWTJVaU9udDlmU3dpWVcxaGVtOXVMV0psWkhKdlkyc3RhVzUyYjJOaGRH bHZiazFsZEhKcFkzTWlPbnNpYVc1d2RYUlViMnRsYmtOdmRXNTBJam8xTnpJc0ltOTFkSEIxZEZS dmEyVnVRMjkxYm5RaU9qVTRMQ0pwYm5adlkyRjBhVzl1VEdGMFpXNWplU0k2TXpnd0xDSm1hWEp6 ZEVKNWRHVk1ZWFJsYm1ONUlqbzJOaXdpWTJGamFHVlNaV0ZrU1c1d2RYUlViMnRsYmtOdmRXNTBJ am93TENKallXTm9aVmR5YVhSbFNXNXdkWFJVYjJ0bGJrTnZkVzUwSWpvd2ZYMD0iLCJwIjoiYWJj ZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0NSJ9 Dhd0RQ== headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Fri, 28 Mar 2025 09:55:46 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - f7f7a941-75c5-4e55-8788-54537019e78d status: code: 200 message: OK version: 1 test_invoke_model_with_response_stream_no_content_tool_call[anthropic.claude].yaml000066400000000000000000000675221527070072300567100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today? Please give one tool call for Seattle and one for San Francisco", "type": "text" } ] } ], "anthropic_version": "bedrock-2023-05-31", "tools": [ { "name": "get_current_weather", "description": "Get the current weather in a given location.", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } ], "max_tokens": 1000 } headers: Content-Length: - '513' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250325T163458Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-522fe453-45d148244e3e7762363841b4;Parent=df4528398bc97d09;Sampled=1 amz-sdk-invocation-id: - f8ea0f99-772d-4c2e-985c-7a253e7552e3 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-3-5-sonnet-20240620-v1%3A0/invoke-with-response-stream response: body: string: !!binary | AAABxAAAAEtT5drPCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaWJXVnpj MkZuWlY5emRHRnlkQ0lzSW0xbGMzTmhaMlVpT25zaWFXUWlPaUp0YzJkZlltUnlhMTh3TVV0RVdG WnlXbTlvWWtoRk1uQklVR1I2VmtabVF6Y2lMQ0owZVhCbElqb2liV1Z6YzJGblpTSXNJbkp2YkdV aU9pSmhjM05wYzNSaGJuUWlMQ0p0YjJSbGJDSTZJbU5zWVhWa1pTMHpMVFV0YzI5dWJtVjBMVEl3 TWpRd05qSXdJaXdpWTI5dWRHVnVkQ0k2VzEwc0luTjBiM0JmY21WaGMyOXVJanB1ZFd4c0xDSnpk Rzl3WDNObGNYVmxibU5sSWpwdWRXeHNMQ0oxYzJGblpTSTZleUpwYm5CMWRGOTBiMnRsYm5NaU9q TTVNaXdpYjNWMGNIVjBYM1J2YTJWdWN5STZPSDE5ZlE9PSIsInAiOiJhYmNkZWYifbiwJ/kAAAET AAAAS2gweYYLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24v anNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVk RjlpYkc5amExOXpkR0Z5ZENJc0ltbHVaR1Y0SWpvd0xDSmpiMjUwWlc1MFgySnNiMk5ySWpwN0lu UjVjR1VpT2lKMFpYaDBJaXdpZEdWNGRDSTZJaUo5ZlE9PSIsInAiOiJhYmNkZWZnaGlqa2xtbm9w cXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMCJ9XbJItQAAATEAAABL0zEF4gs6 ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5 a1pXeDBZU0lzSW1sdVpHVjRJam93TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gw WVNJc0luUmxlSFFpT2lKRFpYSjBZV2x1YkhraElFa25iR3dnZFhObElIUm9aU0JuWlhRaWZYMD0i LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZ In10XMrZAAABIwAAAEvJEcEACzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFw cGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpv aVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUow ZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUpmWTNWeWNtVnVkRjkzWldGMGFHVnlJ bjE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RV VldYWVowIn1QAxQ9AAABPQAAAEsWwejjCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlw ZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBl WEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlT STZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnWm5WdVkzUnBiMjRnZEc4 Z1kyaGxZMnNnZEdobElIZGxZWFJvWlhJaWZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1 dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0NTYifc5bypAAAAFEAAAAS+IDal0L OmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amEx OWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4 MFlTSXNJblJsZUhRaU9pSWdabTl5SUdKdmRHZ2dVMlZoZEhSc1pTQmhibVFnVTJGdUlFWnlZVzVq YVhOamJ5SjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktM TU5PUFFSU1RVVldYWVowMTIzNDUifSjBmUEAAAEMAAAAS4qAedULOmV2ZW50LXR5cGUHAAVjaHVu aw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7 ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0 SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSXVJ RWtuYkd3Z2JXRnJaU0o5ZlE9PSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RF RkdIIn2sWZ9oAAABHAAAAEvqYO5XCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJs SWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZl eUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnZEhkdklITmxjR0Z5WVhSbElu MTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVW V1gifU8ZjPIAAAERAAAASxLwKuYLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQ YXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJ am9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5 SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSWdkRzl2YkNCallXeHNjeUIzYVhS b2FXNGlmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREUiffuRFxMAAAD3 AAAAS344yzwLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24v anNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVk RjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRH VjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSWdkR2hsSUhOaGJXVWdablZ1WTNScGIyNGlmWDA9Iiwi cCI6ImFiY2RlIn0tKo5KAAAA8wAAAEuLuG38CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5 SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4 MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnWTJGc2JITWdZbXh2 WTJzdUluMTkiLCJwIjoiYWJjZGVmZ2hpIn0QasavAAAA1QAAAEvFObdYCzpldmVudC10eXBlBwAF Y2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2 ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTl6ZEc5d0lpd2lhVzVr WlhnaU9qQjkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9Q UVJTVFVWV1hZIn28oUh2AAABUgAAAEsNowh/CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5 SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTl6ZEdGeWRDSXNJbWx1WkdWNElqb3hMQ0pqYjI1 MFpXNTBYMkpzYjJOcklqcDdJblI1Y0dVaU9pSjBiMjlzWDNWelpTSXNJbWxrSWpvaWRHOXZiSFZm WW1SeWExOHdNVkp1TjBOaVlYSTJZVFJUTjNkbE4yZzJhVlYzY0UwaUxDSnVZVzFsSWpvaVoyVjBY Mk4xY25KbGJuUmZkMlZoZEdobGNpSXNJbWx1Y0hWMElqcDdmWDE5IiwicCI6ImFiY2RlZmdoaWpr bG1ub3AifRz8bL4AAAEdAAAAS9cAx+cLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBl BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVY QmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpveExDSmtaV3gwWVNJ NmV5SjBlWEJsSWpvaWFXNXdkWFJmYW5OdmJsOWtaV3gwWVNJc0luQmhjblJwWVd4ZmFuTnZiaUk2 SWlKOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9Q UVJTVFUifVWKi8MAAAEKAAAASwXAjHULOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBl BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVY QmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpveExDSmtaV3gwWVNJ NmV5SjBlWEJsSWpvaWFXNXdkWFJmYW5OdmJsOWtaV3gwWVNJc0luQmhjblJwWVd4ZmFuTnZiaUk2 SW50Y0lteHZZMkYwYVNKOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcCJ9uB2aJAAAAQUAAABL h5AbpAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWli RzlqYTE5a1pXeDBZU0lzSW1sdVpHVjRJam94TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pYVc1d2RY UmZhbk52Ymw5a1pXeDBZU0lzSW5CaGNuUnBZV3hmYW5OdmJpSTZJbTl1WENJNklGd2lVMlZoZENK OWZRPT0iLCJwIjoiYWJjZGVmZyJ9VyNkvgAAAQoAAABLBcCMdQs6ZXZlbnQtdHlwZQcABWNodW5r DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Ynl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lzSW1sdVpHVjRJ am94TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pYVc1d2RYUmZhbk52Ymw5a1pXeDBZU0lzSW5CaGNu UnBZV3hmYW5OdmJpSTZJblJzWlZ3aWZTSjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0 In3Db7AIAAAAqQAAAEv5G7qWCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFw cGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpv aVkyOXVkR1Z1ZEY5aWJHOWphMTl6ZEc5d0lpd2lhVzVrWlhnaU9qRjkiLCJwIjoiYWJjZGVmZyJ9 875sMQAAAVgAAABLRxMQ3gs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBs aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZ Mjl1ZEdWdWRGOWliRzlqYTE5emRHRnlkQ0lzSW1sdVpHVjRJam95TENKamIyNTBaVzUwWDJKc2Iy TnJJanA3SW5SNWNHVWlPaUowYjI5c1gzVnpaU0lzSW1sa0lqb2lkRzl2YkhWZlltUnlhMTh3TVUx M1dHbHpNa3R3ZFhaSVRHWkxNVlpOZEhsS00zQWlMQ0p1WVcxbElqb2laMlYwWDJOMWNuSmxiblJm ZDJWaGRHaGxjaUlzSW1sdWNIVjBJanA3ZlgxOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2 In3kprDCAAABGwAAAEtYQDJHCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFw cGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpv aVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3lMQ0prWld4MFlTSTZleUow ZVhCbElqb2lhVzV3ZFhSZmFuTnZibDlrWld4MFlTSXNJbkJoY25ScFlXeGZhbk52YmlJNklpSjlm UT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSUyJ9 HBoMygAAASsAAABL+WGKwQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBs aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZ Mjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lzSW1sdVpHVjRJam95TENKa1pXeDBZU0k2ZXlKMGVY QmxJam9pYVc1d2RYUmZhbk52Ymw5a1pXeDBZU0lzSW5CaGNuUnBZV3hmYW5OdmJpSTZJbnRjSW14 dlkyRjBhVzl1WENJNkluMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZH SElKS0xNTk9QUVJTIn3waLmPAAABBAAAAEu68DIUCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRl bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6 ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3lMQ0pr Wld4MFlTSTZleUowZVhCbElqb2lhVzV3ZFhSZmFuTnZibDlrWld4MFlTSXNJbkJoY25ScFlXeGZh bk52YmlJNklpQmNJbE5oYmlCR2NtRnVJbjE5IiwicCI6ImFiY2RlZmdoaWoifbmfLtEAAAEVAAAA S+dwjCYLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlp Ykc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpveUxDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWFXNXdk WFJmYW5OdmJsOWtaV3gwWVNJc0luQmhjblJwWVd4ZmFuTnZiaUk2SW1OcGMyTnZYQ0o5SW4xOSIs InAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFIn0ikqCnAAAAwgAAAEsX+fzKCzpl dmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh Z2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTl6 ZEc5d0lpd2lhVzVrWlhnaU9qSjkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNE RUYifZN6ltoAAAE+AAAAS1FhkjMLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQ YXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJ am9pYldWemMyRm5aVjlrWld4MFlTSXNJbVJsYkhSaElqcDdJbk4wYjNCZmNtVmhjMjl1SWpvaWRH OXZiRjkxYzJVaUxDSnpkRzl3WDNObGNYVmxibU5sSWpwdWRXeHNmU3dpZFhOaFoyVWlPbnNpYjNW MGNIVjBYM1J2YTJWdWN5STZNVE16ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5 ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQ1NjciffAZqvIAAAFNAAAAS+8TCCwLOmV2 ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pYldWemMyRm5aVjl6ZEc5d0lpd2lZ VzFoZW05dUxXSmxaSEp2WTJzdGFXNTJiMk5oZEdsdmJrMWxkSEpwWTNNaU9uc2lhVzV3ZFhSVWIy dGxia052ZFc1MElqb3pPVElzSW05MWRIQjFkRlJ2YTJWdVEyOTFiblFpT2prM0xDSnBiblp2WTJG MGFXOXVUR0YwWlc1amVTSTZNemt4TlN3aVptbHljM1JDZVhSbFRHRjBaVzVqZVNJNk1UQXpObjE5 IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnMifeVdPcA= headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Tue, 25 Mar 2025 16:34:59 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - b9ea026a-ac98-4d87-9352-99984599e41f status: code: 200 message: OK - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today? Please give one tool call for Seattle and one for San Francisco", "type": "text" } ] }, { "role": "assistant", "content": [ { "type": "text", "text": "Certainly! I'll use the get_current_weather function to check the weather for both Seattle and San Francisco. I'll make two separate tool calls within the same function calls block." }, { "type": "tool_use", "id": "toolu_bdrk_01Rn7Cbar6a4S7we7h6iUwpM", "name": "get_current_weather", "input": { "location": "Seattle" } }, { "type": "tool_use", "id": "toolu_bdrk_01MwXis2KpuvHLfK1VMtyJ3p", "name": "get_current_weather", "input": { "location": "San Francisco" } } ] }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_bdrk_01Rn7Cbar6a4S7we7h6iUwpM", "content": "50 degrees and raining" }, { "type": "tool_result", "tool_use_id": "toolu_bdrk_01MwXis2KpuvHLfK1VMtyJ3p", "content": "70 degrees and sunny" } ] } ], "anthropic_version": "bedrock-2023-05-31", "tools": [ { "name": "get_current_weather", "description": "Get the current weather in a given location.", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } ], "max_tokens": 1000 } headers: Content-Length: - '1291' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250325T163502Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-e42f5da2-f049566b669ca481c0e86624;Parent=1bdd6579c05809f1;Sampled=1 amz-sdk-invocation-id: - e8800abb-49e8-4f23-8cf1-b0bc695790ff amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-3-5-sonnet-20240620-v1%3A0/invoke-with-response-stream response: body: string: !!binary | AAABzQAAAEte9bi+CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaWJXVnpj MkZuWlY5emRHRnlkQ0lzSW0xbGMzTmhaMlVpT25zaWFXUWlPaUp0YzJkZlltUnlhMTh3TVRsWVFW bG9hMWhpZDBOdE1UaDFhRFZGVEc1bFl6Y2lMQ0owZVhCbElqb2liV1Z6YzJGblpTSXNJbkp2YkdV aU9pSmhjM05wYzNSaGJuUWlMQ0p0YjJSbGJDSTZJbU5zWVhWa1pTMHpMVFV0YzI5dWJtVjBMVEl3 TWpRd05qSXdJaXdpWTI5dWRHVnVkQ0k2VzEwc0luTjBiM0JmY21WaGMyOXVJanB1ZFd4c0xDSnpk Rzl3WDNObGNYVmxibU5sSWpwdWRXeHNMQ0oxYzJGblpTSTZleUpwYm5CMWRGOTBiMnRsYm5NaU9q WXdNaXdpYjNWMGNIVjBYM1J2YTJWdWN5STZNbjE5ZlE9PSIsInAiOiJhYmNkZWZnaGlqa2xtbm8i fT6bXREAAAD/AAAAS05IgP0LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBw bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9p WTI5dWRHVnVkRjlpYkc5amExOXpkR0Z5ZENJc0ltbHVaR1Y0SWpvd0xDSmpiMjUwWlc1MFgySnNi Mk5ySWpwN0luUjVjR1VpT2lKMFpYaDBJaXdpZEdWNGRDSTZJaUo5ZlE9PSIsInAiOiJhYmNkZWZn aGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkcifYTGj8MAAAEYAAAASx/gSJcLOmV2ZW50LXR5cGUH AAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0lt bHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhR aU9pSmNibHh1UW1GelpTSjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJD REVGR0hJSktMTU5PUFFSU1RVVldYIn2GKqAtAAABEwAAAEtoMHmGCzpldmVudC10eXBlBwAFY2h1 bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdW NElqb3dMQ0prWld4MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUpr SUc5dUlIUm9aU0J5WlhOMWJIUnpJR1p5YjIwZ2IzVnlJbjE5IiwicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHkiffifX68AAAEaAAAAS2UgG/cLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVu dC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoi ZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmta V3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSWdkMlZoZEdobGNp QjBiMjlzTENCb1pYSmxKM01pZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFC Q0RFRkdISUoifXIJZlcAAAEoAAAAS77B8BELOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10 eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlK MGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gw WVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSWdkR2hsSUdOMWNuSmxi blFnZDJWaGRHaGxjaUJtYjNJaWZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpB QkNERUZHSElKS0xNTk9QUVJTVCJ9QaR1XwAAASUAAABLRlE0oAs6ZXZlbnQtdHlwZQcABWNodW5r DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Ynl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lzSW1sdVpHVjRJ am93TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJc0luUmxlSFFpT2lJZ1Uy VmhkSFJzWlNCaGJtUWdVMkZ1SUVaeVlXNWphWE5qYnpwY2JseHVVMlZoZEhSc1pTSjlmUT09Iiwi cCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3In09E8tyAAABAgAAAEs1sMe0CzpldmVudC10eXBl BwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA BWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJ bWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVI UWlPaUk2SUZSb1pTQmpkWEp5Wlc1MElIZGxZWFJvWlhJZ2FXNGlmWDA9IiwicCI6ImFiY2RlZmdo In3pNDtbAAABDgAAAEvwQCq1CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFw cGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpv aVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUow ZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnVTJWaGRIUnNaU0JwY3lBMU1DQmta V2R5WldWeklHRnVaQ0J5SW4xOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wIn2L7dRhAAABDgAAAEvw QCq1CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24N Om1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJH OWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5 a1pXeDBZU0lzSW5SbGVIUWlPaUpoYVc1cGJtY3VYRzVUWVc0Z1JuSmhibU5wYzJOdk9pSjlmUT09 IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0In2QqC+BAAABGQAAAEsigGEnCzpldmVudC10eXBl BwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA BWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJ bWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVI UWlPaUlnVkdobElHTjFjbkpsYm5RZ2QyVmhkR2hsY2lCcGJpQlRZVzRnUm5KaGJtTnBjMk52SUds eklDSjlmUT09IiwicCI6ImFiY2RlZmcifarIy2wAAAD5AAAAS8EIdV0LOmV2ZW50LXR5cGUHAAVj aHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZl bnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVa R1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9p STNNQ0JrWldkeVpXVnpJR0Z1WkNCemRXNXVlUzRpZlgwPSIsInAiOiJhYmMifUCgNssAAAETAAAA S2gweYYLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlp Ykc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRk RjlrWld4MFlTSXNJblJsZUhRaU9pSmNibHh1UVhNZ2VXOTFJR05oYmlCelpXVXNJSFJvWlNCMGQy OGlmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dSJ9iibEcQAAAUsAAABLYFP9jAs6ZXZl bnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdl LXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pX eDBZU0lzSW1sdVpHVjRJam93TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJ c0luUmxlSFFpT2lJZ1kybDBhV1Z6SUdGeVpTQmxlSEJsY21sbGJtTnBibWNnY1hWcGRHVWdaR2xt Wm1WeVpXNTBJSGRsWVhSb1pYSWdZMjl1WkdsMGFXOXVjeUIwYjJSaGVTSjlmUT09IiwicCI6ImFi Y2RlZmdoaWprbG1ub3BxcnN0dXZ3eHkifWYWD5kAAAExAAAAS9MxBeILOmV2ZW50LXR5cGUHAAVj aHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZl bnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVa R1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9p SXVJRk5sWVhSMGJHVWdhWE1nYUdGMmFXNW5JR0VnWTI5dmJHVnlMQ0J5WVNKOWZRPT0iLCJwIjoi YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUSJ9JZcMbgAAAQQAAABL uvAyFAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWli RzlqYTE5a1pXeDBZU0lzSW1sdVpHVjRJam93TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRG OWtaV3gwWVNJc0luUmxlSFFpT2lKcGJua2daR0Y1TENCM2FHbGphQ0JwY3lCbVlXbHliSGtpZlgw PSIsInAiOiJhYmNkZWZnaGlqIn16ysiBAAABGgAAAEtlIBv3CzpldmVudC10eXBlBwAFY2h1bmsN OmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJi eXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElq b3dMQ0prWld4MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnZEhs d2FXTmhiQ0JtYjNJZ2RHaGxJRkJoWTJsbWFXTWdUbTl5ZEdoM1pYTjBMaUJQYmlKOWZRPT0iLCJw IjoiYWJjZGVmZ2hpamtsIn1jUGsNAAABJgAAAEsB8U5wCzpldmVudC10eXBlBwAFY2h1bmsNOmNv bnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRl cyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dM Q0prWld4MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnZEdobElH OTBhR1Z5SUdoaGJtUXNJRk5oYmlCR2NtRnVZMmx6WTI4Z2FYTWdaVzVxYjNscGJtY2lmWDA9Iiwi cCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0In25ObEhAAABOwAAAEuZgR1DCzpldmVudC10eXBlBwAF Y2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2 ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1 WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlP aUlnWVNCM1lYSnRJR0Z1WkNCemRXNXVlU0JrWVhrc0lIQmxjbVpsWTNRaWZYMD0iLCJwIjoiYWJj ZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0In13 gEV7AAABOgAAAEuk4TTzCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxp Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVky OXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhC bElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnWm05eUlHOTFkR1J2YjNJZ1lXTjBhWFpw ZEdsbGN5NWNibHh1U1hNaWZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNE RUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjMifcfKjpUAAAEMAAAAS4qAedULOmV2ZW50LXR5cGUH AAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0lt bHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhR aU9pSWdkR2hsY21VZ1lXNTVkR2hwYm1jZ1pXeHpaU0I1YjNVbkluMTkiLCJwIjoiYWJjZGVmZ2hp amtsbW5vcHFyIn2pC/d3AAABFgAAAEug0Pb2CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5 SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4 MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUprSUd4cGEyVWdkRzhn YTI1dmR5QmhZbTkxZENCMGFHVWdkMlZoZEdobGNpSjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1u b3AifcvpckIAAAE/AAAAS2wBu4MLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQ YXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJ am9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5 SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSWdhVzRnZEdobGMyVWdZMmwwYVdW eklHOXlJR0Z1ZVNCdmRHaGxjaUJzYjJOaGRHbHZibk1pZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xt bm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXIn2P8KtRAAAA6wAAAEvbKLG/Czpl dmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh Z2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlr Wld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZ U0lzSW5SbGVIUWlPaUkvSW4xOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcSJ9BD4nBQAAANYAAABL gpnNiAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWli RzlqYTE5emRHOXdJaXdpYVc1a1pYZ2lPakI5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3 eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVoifebszSUAAAErAAAAS/lhisELOmV2ZW50LXR5 cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBl BwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pYldWemMyRm5aVjlrWld4MFlTSXNJbVJsYkhS aElqcDdJbk4wYjNCZmNtVmhjMjl1SWpvaVpXNWtYM1IxY200aUxDSnpkRzl3WDNObGNYVmxibU5s SWpwdWRXeHNmU3dpZFhOaFoyVWlPbnNpYjNWMGNIVjBYM1J2YTJWdWN5STZNVE0zZlgwPSIsInAi OiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OTyJ9T6Fj3AAAAXQAAABL QyLS2ws6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2liV1Z6YzJGblpWOXpk Rzl3SWl3aVlXMWhlbTl1TFdKbFpISnZZMnN0YVc1MmIyTmhkR2x2YmsxbGRISnBZM01pT25zaWFX NXdkWFJVYjJ0bGJrTnZkVzUwSWpvMk1ESXNJbTkxZEhCMWRGUnZhMlZ1UTI5MWJuUWlPakV6Tnl3 aWFXNTJiMk5oZEdsdmJreGhkR1Z1WTNraU9qUTBOVEFzSW1acGNuTjBRbmwwWlV4aGRHVnVZM2tp T2pnNE5IMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9Q UVJTVFVWV1hZWjAxMjM0NSJ92l5aXQ== headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Tue, 25 Mar 2025 16:35:03 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - 5712a4fc-af29-49e3-878a-c10a7cf8236a status: code: 200 message: OK version: 1 test_invoke_model_with_response_stream_with_content[amazon.nova].yaml000066400000000000000000000221761527070072300542170ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] } ], "inferenceConfig": { "max_new_tokens": 10, "temperature": 0.8, "topP": 1, "stopSequences": [ "|" ] }, "schemaVersion": "messages-v1" } headers: Content-Length: - '207' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.0.0 md/arch#arm64 lang/python#3.13.1 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250206T051240Z X-Amzn-Trace-Id: - Root=1-9ea22376-e927eadde37afc8a74fe81e3;Parent=03543dbad0ffd6cb;Sampled=1 amz-sdk-invocation-id: - 9ff1b981-5ae6-407d-8320-2516e09455c0 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/invoke-with-response-stream response: body: string: !!binary | AAAAqAAAAEvEe5MmCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SnRaWE56WVdkbFUzUmhj blFpT25zaWNtOXNaU0k2SW1GemMybHpkR0Z1ZENKOWZRPT0iLCJwIjoiYWJjZGVmIn3MQ7GMAAAA 4AAAAEus+ICuCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRF Wld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lTWFFpZlN3aVkyOXVkR1Z1ZEVKc2IyTnJT VzVrWlhnaU9qQjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXYifVo1phsAAADOAAAA S9IJEcsLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3 SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3dmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6QUJDREVGR0hJSiJ9fSM+ngAAANUAAABLxTm3WAs6ZXZlbnQtdHlwZQcABWNo dW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu dHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpY aDBJam9pSUhOdmRXNWtjeUo5TENKamIyNTBaVzUwUW14dlkydEpibVJsZUNJNk1YMTkiLCJwIjoi YWJjZGVmZyJ9NS2lfQAAAN0AAABL9Un8mQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5 cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpq YjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpveGZYMD0i LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZ In333c0kAAAA4QAAAEuRmKkeCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFw cGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1 MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lJR3hwYTJVaWZTd2lZMjl1 ZEdWdWRFSnNiMk5yU1c1a1pYZ2lPako5ZlE9PSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzIn2q 80BUAAAAtQAAAEtcC8AVCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxp Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFt eHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam95ZlgwPSIsInAiOiJhYmNk ZWZnaGlqayJ9eAXbEAAAAQcAAABL/VBIxAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5 cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpq YjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pSUhsdmRTSjlM Q0pqYjI1MFpXNTBRbXh2WTJ0SmJtUmxlQ0k2TTMxOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJz dHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQ1Njc4In3uqMAoAAAAygAAAEsn ibcLCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24N Om1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lq cDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam96ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9w cXJzdHV2d3h5ekFCQ0RFRiJ9kUG11gAAAPUAAABLBPiYXAs6ZXZlbnQtdHlwZQcABWNodW5rDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0 ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9p SnlKOUxDSmpiMjUwWlc1MFFteHZZMnRKYm1SbGVDSTZOSDE5IiwicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVIn1Sx4LNAAAAxQAAAEul2SDaCzpldmVu dC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2Ut dHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5S bGJuUkNiRzlqYTBsdVpHVjRJam8wZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5 ekEifQfY9jYAAAD5AAAAS8EIdV0LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQ YXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBa VzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaWNtVWlmU3dpWTI5dWRH VnVkRUpzYjJOclNXNWtaWGdpT2pWOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4 eXpBQkNERUZHSElKS0xNTk9QUVJTVFUifYJwD/MAAADKAAAASyeJtwsLOmV2ZW50LXR5cGUHAAVj aHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZl bnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWph MGx1WkdWNElqbzFmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGIn1x UENqAAAA3QAAAEv1SfyZCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxp Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFt eHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lJR2x1YVhScFlYUnBibWNpZlN3 aVkyOXVkR1Z1ZEVKc2IyTnJTVzVrWlhnaU9qWjlmUT09IiwicCI6ImFiY2RlZmcifRJH/k8AAADP AAAAS+9pOHsLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24v anNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRk Rzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqbzJmWDA9IiwicCI6ImFiY2RlZmdoaWpr bG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSksifXOcqXkAAAD+AAAAS3MoqU0LOmV2ZW50LXR5cGUH AAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5 SjBaWGgwSWpvaUlHRWlmU3dpWTI5dWRHVnVkRUpzYjJOclNXNWtaWGdpT2pkOWZRPT0iLCJwIjoi YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWiJ9StXe RgAAANEAAABLMLkRmAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNh dGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2 WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpvM2ZYMD0iLCJwIjoiYWJjZGVm Z2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNIn0Gj9HNAAAA4QAAAEuRmKkeCzpldmVu dC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2Ut dHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZleUprWld4 MFlTSTZleUowWlhoMElqb2lJRzFsYzNOaFoyVWlmU3dpWTI5dWRHVnVkRUpzYjJOclNXNWtaWGdp T2poOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vIn1oTQ0dAAAA1AAAAEv4WZ7oCzpldmVudC10 eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlw ZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJu UkNiRzlqYTBsdVpHVjRJam80ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFC Q0RFRkdISUpLTE1OT1AifWJbJL4AAADUAAAAS/hZnugLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29u dGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVz IjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlH OXlJbjBzSW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpvNWZYMD0iLCJwIjoiYWJjZGVmZ2hpaiJ9 ZI1CPwAAANYAAABLgpnNiAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBs aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBR bXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpvNWZYMD0iLCJwIjoiYWJj ZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVIifdG24h4AAADRAAAASzC5 EZgLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06 bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKdFpYTnpZV2RsVTNSdmNDSTZleUp6ZEc5 d1VtVmhjMjl1SWpvaWJXRjRYM1J2YTJWdWN5SjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3Bx cnN0dXZ3eHl6QUJDREVGR0hJSktMTSJ9fb+xAQAAAZoAAABL1MarZQs6ZXZlbnQtdHlwZQcABWNo dW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu dHsiYnl0ZXMiOiJleUp0WlhSaFpHRjBZU0k2ZXlKMWMyRm5aU0k2ZXlKcGJuQjFkRlJ2YTJWdWN5 STZOU3dpYjNWMGNIVjBWRzlyWlc1eklqb3hNSDBzSW0xbGRISnBZM01pT250OUxDSjBjbUZqWlNJ NmUzMTlMQ0poYldGNmIyNHRZbVZrY205amF5MXBiblp2WTJGMGFXOXVUV1YwY21samN5STZleUpw Ym5CMWRGUnZhMlZ1UTI5MWJuUWlPalVzSW05MWRIQjFkRlJ2YTJWdVEyOTFiblFpT2pFd0xDSnBi blp2WTJGMGFXOXVUR0YwWlc1amVTSTZNVGd4TENKbWFYSnpkRUo1ZEdWTVlYUmxibU41SWpvMU9Y MTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4In3zcwkB headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Thu, 06 Feb 2025 05:12:41 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - 987c09e8-e706-4bea-a232-2d37b3eb3901 status: code: 200 message: OK version: 1 test_invoke_model_with_response_stream_with_content[amazon.titan].yaml000066400000000000000000000043101527070072300543610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "inputText": "Say this is a test", "textGenerationConfig": { "maxTokenCount": 10, "temperature": 0.8, "topP": 1, "stopSequences": [ "|" ] } } headers: Content-Length: - '137' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.0.0 md/arch#arm64 lang/python#3.13.1 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250206T051241Z X-Amzn-Trace-Id: - Root=1-b3576961-f62f047f38ec36c95b93b58a;Parent=7bb925e3ca7c7e24;Sampled=1 amz-sdk-invocation-id: - 10400543-271b-4721-907d-8042dcdc7e7e amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.titan-text-lite-v1/invoke-with-response-stream response: body: string: !!binary | AAACGwAAAEve1EDpCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SnZkWFJ3ZFhSVVpYaDBJ am9pWEc1SVpXeHNieUVnU1NCaGJTQmhJR052YlhCMWRHVnlJSEJ5YjJkeVlXMGdaR1Z6YVdkdVpX UWdkRzhpTENKcGJtUmxlQ0k2TUN3aWRHOTBZV3hQZFhSd2RYUlVaWGgwVkc5clpXNURiM1Z1ZENJ Nk1UQXNJbU52YlhCc1pYUnBiMjVTWldGemIyNGlPaUpNUlU1SFZFZ2lMQ0pwYm5CMWRGUmxlSFJV YjJ0bGJrTnZkVzUwSWpvMUxDSmhiV0Y2YjI0dFltVmtjbTlqYXkxcGJuWnZZMkYwYVc5dVRXVjBj bWxqY3lJNmV5SnBibkIxZEZSdmEyVnVRMjkxYm5RaU9qVXNJbTkxZEhCMWRGUnZhMlZ1UTI5MWJu UWlPakV3TENKcGJuWnZZMkYwYVc5dVRHRjBaVzVqZVNJNk5qTXhMQ0ptYVhKemRFSjVkR1ZNWVhS bGJtTjVJam8yTXpGOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZH SElKS0xNTk9QUVJTVFVWV1hZWjAifaS9D7U= headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Thu, 06 Feb 2025 05:12:42 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - a17c5c00-e8f2-4851-8183-2302bc145791 status: code: 200 message: OK version: 1 test_invoke_model_with_response_stream_with_content[anthropic.claude].yaml000066400000000000000000000165221527070072300552110ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test", "type": "text" } ] } ], "anthropic_version": "bedrock-2023-05-31", "max_tokens": 10, "temperature": 0.8, "top_p": 1, "stop_sequences": [ "|" ] } headers: Content-Length: - '211' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/macos#24.0.0 md/arch#arm64 lang/python#3.13.1 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250206T051242Z X-Amzn-Trace-Id: - Root=1-c6416126-cf7b23864d015faec4be246d;Parent=cc2911ff7826766a;Sampled=1 amz-sdk-invocation-id: - 60a3ea06-9b9d-4733-a2f2-4d68ffe877a8 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-v2/invoke-with-response-stream response: body: string: !!binary | AAAB1AAAAEszBU1NCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaWJXVnpj MkZuWlY5emRHRnlkQ0lzSW0xbGMzTmhaMlVpT25zaWFXUWlPaUp0YzJkZlltUnlhMTh3TVZkb09Y YzJWSFl5YjNCcmFXSTJOMWxRTTB3NVFqWWlMQ0owZVhCbElqb2liV1Z6YzJGblpTSXNJbkp2YkdV aU9pSmhjM05wYzNSaGJuUWlMQ0p0YjJSbGJDSTZJbU5zWVhWa1pTMHlMakFpTENKamIyNTBaVzUw SWpwYlhTd2ljM1J2Y0Y5eVpXRnpiMjRpT201MWJHd3NJbk4wYjNCZmMyVnhkV1Z1WTJVaU9tNTFi R3dzSW5WellXZGxJanA3SW1sdWNIVjBYM1J2YTJWdWN5STZNVFFzSW05MWRIQjFkRjkwYjJ0bGJu TWlPakY5ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1O T1BRUlNUIn26UNk0AAAA9AAAAEs5mLHsCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlw ZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBl WEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTl6ZEdGeWRDSXNJbWx1WkdWNElqb3dMQ0pqYjI1MFpX NTBYMkpzYjJOcklqcDdJblI1Y0dVaU9pSjBaWGgwSWl3aWRHVjRkQ0k2SWlKOWZRPT0iLCJwIjoi YWJjZGVmZ2hpamtsbW5vcHFyc3R1diJ9n5F/3AAAAQEAAABLchC9ZAs6ZXZlbnQtdHlwZQcABWNo dW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu dHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lzSW1sdVpH VjRJam93TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJc0luUmxlSFFpT2lK UGEyRjVJbjE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJIn0xNlnU AAAA/QAAAEs0iNOdCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVk R1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhCbElq b2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlzSW4xOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9w cXJzdHV2d3h5ekFCQ0RFRkdISSJ9MKZi+gAAAPQAAABLOZix7As6ZXZlbnQtdHlwZQcABWNodW5r DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Ynl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lzSW1sdVpHVjRJ am93TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJc0luUmxlSFFpT2lJZ1NT SjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXYifZMaxcoAAAD0AAAASzmYsewLOmV2 ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWta V3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlT SXNJblJsZUhRaU9pSWdkMmxzYkNKOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyIn3T4dfr AAAA8wAAAEuLuG38CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVk R1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhCbElq b2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnY21Wd1pXRjBJbjE5IiwicCI6ImFiY2RlZmdo aWprbG1ub3BxIn2wyX9kAAAA3AAAAEvIKdUpCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5 SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4 MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUk2SW4xOSIsInAiOiJh YiJ9sAU9OgAAAOYAAABLI7h1Dgs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElq b2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lzSW1sdVpHVjRJam93TENKa1pXeDBZU0k2ZXlK MGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJc0luUmxlSFFpT2lJZ1ZHaHBjeUo5ZlE9PSIsInAiOiJh YmNkIn0+ttr6AAAA4AAAAEus+ICuCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJs SWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZl eUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnYVhNaWZYMD0iLCJwIjoiYWIi fbBYs1UAAAD4AAAAS/xoXO0LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBw bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9p WTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBl WEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSWdZU0o5ZlE9PSIsInAiOiJhYmNkZWZn aGlqa2xtbm9wcXJzdHV2d3h5eiJ9HP88zAAAAP0AAABLNIjTnQs6ZXZlbnQtdHlwZQcABWNodW5r DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Ynl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lzSW1sdVpHVjRJ am93TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJc0luUmxlSFFpT2lJZ2RH VnpkQ0o5ZlE9PSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekEifaPt+BsAAAC7AAAA S+M7fnQLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlp Ykc5amExOXpkRzl3SWl3aWFXNWtaWGdpT2pCOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2 d3h5In0quI/LAAABBQAAAEuHkBukCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJs SWpvaWJXVnpjMkZuWlY5a1pXeDBZU0lzSW1SbGJIUmhJanA3SW5OMGIzQmZjbVZoYzI5dUlqb2li V0Y0WDNSdmEyVnVjeUlzSW5OMGIzQmZjMlZ4ZFdWdVkyVWlPbTUxYkd4OUxDSjFjMkZuWlNJNmV5 SnZkWFJ3ZFhSZmRHOXJaVzV6SWpveE1IMTkiLCJwIjoiYWJjIn08HpMfAAABcQAAAEuLwl2rCzpl dmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh Z2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaWJXVnpjMkZuWlY5emRHOXdJaXdp WVcxaGVtOXVMV0psWkhKdlkyc3RhVzUyYjJOaGRHbHZiazFsZEhKcFkzTWlPbnNpYVc1d2RYUlVi MnRsYmtOdmRXNTBJam94TkN3aWIzVjBjSFYwVkc5clpXNURiM1Z1ZENJNk1UQXNJbWx1ZG05allY UnBiMjVNWVhSbGJtTjVJam94TXpJeExDSm1hWEp6ZEVKNWRHVk1ZWFJsYm1ONUlqbzBNemw5ZlE9 PSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZX WFlaMDEyIn2UJaDt headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Thu, 06 Feb 2025 05:12:43 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - 83f6d884-3370-4485-8ca3-4551e0d49791 status: code: 200 message: OK version: 1 test_invoke_model_with_response_stream_with_content_different_events[amazon.nova].yaml000066400000000000000000000231671527070072300576320ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test" } ] }, { "role": "assistant", "content": [ { "text": "This is a test" } ] }, { "role": "user", "content": [ { "text": "Say again this is a test" } ] } ], "inferenceConfig": { "max_new_tokens": 10 }, "schemaVersion": "messages-v1", "system": [ { "text": "You are a friendly model" } ] } headers: Content-Length: - '335' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250213T141142Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-28ed5f06-78a0ef90426228c4d5ebde31;Parent=4113da3ba221a595;Sampled=1 amz-sdk-invocation-id: - e1dd3bf8-0b4f-47e7-84f9-6bef53b8b7d8 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/invoke-with-response-stream response: body: string: !!binary | AAAAuAAAAEukmwSkCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SnRaWE56WVdkbFUzUmhj blFpT25zaWNtOXNaU0k2SW1GemMybHpkR0Z1ZENKOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5v cHFyc3R1diJ9TyaZ3wAAANQAAABL+Fme6As6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5 cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpq YjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pVkdocGN5SjlM Q0pqYjI1MFpXNTBRbXh2WTJ0SmJtUmxlQ0k2TUgxOSIsInAiOiJhYmNkZWZnaGlqIn2IqYJIAAAA wgAAAEsX+fzKCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRU ZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam93ZlgwPSIsInAiOiJhYmNkZWZnaGlq a2xtbm9wcXJzdHV2d3gifYUcwhMAAADjAAAAS+tY+n4LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29u dGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVz IjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlH bHpJbjBzSW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpveGZYMD0iLCJwIjoiYWJjZGVmZ2hpamts bW5vcHFyc3R1dnd4eSJ9J0L/9wAAAMkAAABLYCnN2ws6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250 ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMi OiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpv eGZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERSJ9jdOkZgAAAPAAAABL zBgXLAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZ U0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pSUdFaWZTd2lZMjl1ZEdWdWRFSnNiMk5yU1c1a1pY Z2lPako5ZlE9PSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTCJ9 ruuoXwAAALYAAABLG6u6xQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBs aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBR bXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpveWZYMD0iLCJwIjoiYWJj ZGVmZ2hpamtsIn2fJhOOAAAA5wAAAEse2Fy+CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5 SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lJSFJsYzNR aWZTd2lZMjl1ZEdWdWRFSnNiMk5yU1c1a1pYZ2lPak45ZlE9PSIsInAiOiJhYmNkZWZnaGlqa2xt bm9wcXJzdHV2d3h5In3/2HgbAAAA1AAAAEv4WZ7oCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRl bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6 ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam96 ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1AifcsN PjAAAADwAAAAS8wYFywLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14 dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlHRm5ZV2x1SW4wc0ltTnZiblJs Ym5SQ2JHOWphMGx1WkdWNElqbzBmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6 QUJDREVGR0gifaIdtT0AAADcAAAAS8gp1SkLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10 eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlK amIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqbzBmWDA9 IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldY In3Z8oGWAAAA1gAAAEuCmc2ICzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFw cGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1 MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lMaUo5TENKamIyNTBaVzUw UW14dlkydEpibVJsZUNJNk5YMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcCJ9ex1DVwAAAMIAAABL F/n8ygs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJ anA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpvMWZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5v cHFyc3R1dnd4In2llAWqAAAA3QAAAEv1SfyZCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5 SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lJRWxtSW4w c0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqbzJmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3Bx cnMifT/cYeQAAADjAAAAS+tY+n4LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQ YXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBa VzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqbzJmWDA9IiwicCI6 ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIz NCJ9ttyqAgAAANoAAABLR2kgiQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpX NTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pSUhsdmRTSjlMQ0pqYjI1 MFpXNTBRbXh2WTJ0SmJtUmxlQ0k2TjMxOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wIn3JnmorAAAA ygAAAEsnibcLCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRU ZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam8zZlgwPSIsInAiOiJhYmNkZWZnaGlq a2xtbm9wcXJzdHV2d3h5ekFCQ0RFRiJ9jsN0TAAAAP4AAABLcyipTQs6ZXZlbnQtdHlwZQcABWNo dW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu dHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpY aDBJam9pSUc1bFpXUWlmU3dpWTI5dWRHVnVkRUpzYjJOclNXNWtaWGdpT2poOWZRPT0iLCJwIjoi YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWIn0GwPyfAAAA xwAAAEvfGXO6CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRU ZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam80ZlgwPSIsInAiOiJhYmNkZWZnaGlq a2xtbm9wcXJzdHV2d3h5ekFCQyJ9PFvNGQAAANQAAABL+Fme6As6ZXZlbnQtdHlwZQcABWNodW5r DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Ynl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJ am9pSUdGdWVTSjlMQ0pqYjI1MFpXNTBRbXh2WTJ0SmJtUmxlQ0k2T1gxOSIsInAiOiJhYmNkZWZn aGlqIn1/oYekAAAA1AAAAEv4WZ7oCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUw Wlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam81ZlgwPSIsInAi OiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1AifX/ON2kAAACtAAAA SwybHFYLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKdFpYTnpZV2RsVTNSdmNDSTZleUp6 ZEc5d1VtVmhjMjl1SWpvaWJXRjRYM1J2YTJWdWN5SjlmUT09IiwicCI6ImFiYyJ9Hvkn7gAAAjYA AABL54WrXAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9q c29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUp0WlhSaFpHRjBZU0k2ZXlKMWMy Rm5aU0k2ZXlKcGJuQjFkRlJ2YTJWdWN5STZNalVzSW05MWRIQjFkRlJ2YTJWdWN5STZNVEFzSW1O aFkyaGxVbVZoWkVsdWNIVjBWRzlyWlc1RGIzVnVkQ0k2TUN3aVkyRmphR1ZYY21sMFpVbHVjSFYw Vkc5clpXNURiM1Z1ZENJNk1IMHNJbTFsZEhKcFkzTWlPbnQ5TENKMGNtRmpaU0k2ZTMxOUxDSmhi V0Y2YjI0dFltVmtjbTlqYXkxcGJuWnZZMkYwYVc5dVRXVjBjbWxqY3lJNmV5SnBibkIxZEZSdmEy VnVRMjkxYm5RaU9qSTFMQ0p2ZFhSd2RYUlViMnRsYmtOdmRXNTBJam94TUN3aWFXNTJiMk5oZEds dmJreGhkR1Z1WTNraU9qRTVOeXdpWm1seWMzUkNlWFJsVEdGMFpXNWplU0k2TnpJc0ltTmhZMmhs VW1WaFpFbHVjSFYwVkc5clpXNURiM1Z1ZENJNk1Dd2lZMkZqYUdWWGNtbDBaVWx1Y0hWMFZHOXJa VzVEYjNWdWRDSTZNSDE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0In2+XMjJ headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Thu, 13 Feb 2025 14:11:43 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - 406cb91b-9ad8-4309-baf1-769a289b7df1 status: code: 200 message: OK version: 1 test_invoke_model_with_response_stream_with_content_different_events[anthropic.claude].yaml000066400000000000000000000134461527070072300606250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "Say this is a test", "type": "text" } ] }, { "role": "assistant", "content": [ { "text": "This is a test", "type": "text" } ] }, { "role": "user", "content": [ { "text": "Say again this is a test", "type": "text" } ] } ], "anthropic_version": "bedrock-2023-05-31", "system": "You are a friendly model", "max_tokens": 10 } headers: Content-Length: - '357' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250213T141143Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-a7421915-90c1cf280ff1d9482f772c88;Parent=9b34e4bcd8b69db4;Sampled=1 amz-sdk-invocation-id: - f8aca0ab-34a1-4185-9551-b5d28c4e0be9 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-v2/invoke-with-response-stream response: body: string: !!binary | AAABtwAAAEvtl0DQCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaWJXVnpj MkZuWlY5emRHRnlkQ0lzSW0xbGMzTmhaMlVpT25zaWFXUWlPaUp0YzJkZlltUnlhMTh3TVUweWFr TjNWbE5tZDFkaldFRnlaME5UZVRodE1XWWlMQ0owZVhCbElqb2liV1Z6YzJGblpTSXNJbkp2YkdV aU9pSmhjM05wYzNSaGJuUWlMQ0p0YjJSbGJDSTZJbU5zWVhWa1pTMHlMakFpTENKamIyNTBaVzUw SWpwYlhTd2ljM1J2Y0Y5eVpXRnpiMjRpT201MWJHd3NJbk4wYjNCZmMyVnhkV1Z1WTJVaU9tNTFi R3dzSW5WellXZGxJanA3SW1sdWNIVjBYM1J2YTJWdWN5STZNemNzSW05MWRIQjFkRjkwYjJ0bGJu TWlPakY5ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcSJ9c5WqNgAAAPcAAABLfjjLPAs6ZXZl bnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdl LXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5emRH RnlkQ0lzSW1sdVpHVjRJam93TENKamIyNTBaVzUwWDJKc2IyTnJJanA3SW5SNWNHVWlPaUowWlho MElpd2lkR1Y0ZENJNklpSjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHkifXQG 3pQAAAEFAAAAS4eQG6QLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5 dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJs SWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSlVhR2x6SW4xOSIsInAiOiJhYmNkZWZnaGlq a2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE0iffHQpi0AAAEaAAAAS2UgG/cLOmV2ZW50LXR5 cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBl BwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJ c0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJs ZUhRaU9pSWdhWE1pZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdI SUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQ1NjcifYA9TRcAAAEGAAAAS8AwYXQLOmV2ZW50LXR5cGUH AAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0lt bHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhR aU9pSWdZU0o5ZlE9PSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpL TE1OIn26R/hGAAABDQAAAEu34FBlCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJs SWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZl eUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnZEdWemRDSjlmUT09IiwicCI6 ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFEifUsoqQQAAADHAAAA S98Zc7oLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlp Ykc5amExOXpkRzl3SWl3aWFXNWtaWGdpT2pCOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2 d3h5ekFCQ0RFRkdISUpLIn289g6LAAABLgAAAEsxgQWxCzpldmVudC10eXBlBwAFY2h1bmsNOmNv bnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRl cyI6ImV5SjBlWEJsSWpvaWJXVnpjMkZuWlY5a1pXeDBZU0lzSW1SbGJIUmhJanA3SW5OMGIzQmZj bVZoYzI5dUlqb2laVzVrWDNSMWNtNGlMQ0p6ZEc5d1gzTmxjWFZsYm1ObElqcHVkV3hzZlN3aWRY TmhaMlVpT25zaWIzVjBjSFYwWDNSdmEyVnVjeUk2T0gxOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9w cXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVYifezNQVkAAAFWAAAAS/gjrr8LOmV2ZW50 LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10 eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pYldWemMyRm5aVjl6ZEc5d0lpd2lZVzFo ZW05dUxXSmxaSEp2WTJzdGFXNTJiMk5oZEdsdmJrMWxkSEpwWTNNaU9uc2lhVzV3ZFhSVWIydGxi a052ZFc1MElqb3pOeXdpYjNWMGNIVjBWRzlyWlc1RGIzVnVkQ0k2T0N3aWFXNTJiMk5oZEdsdmJr eGhkR1Z1WTNraU9qUXpNaXdpWm1seWMzUkNlWFJsVEdGMFpXNWplU0k2TXpVNGZYMD0iLCJwIjoi YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUYifYwQjVc= headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Thu, 13 Feb 2025 14:11:44 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - 2423a140-3984-4d26-8741-1484ad05d541 status: code: 200 message: OK version: 1 test_invoke_model_with_response_stream_with_content_tool_call[amazon.nova].yaml000066400000000000000000001226151527070072300562460ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today? Please give one tool call for Seattle and one for San Francisco" } ] } ], "inferenceConfig": { "max_new_tokens": 1000 }, "schemaVersion": "messages-v1", "toolConfig": { "tools": [ { "toolSpec": { "name": "get_current_weather", "description": "Get the current weather in a given location.", "inputSchema": { "json": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } } } ] } } headers: Content-Length: - '550' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250325T144018Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-14345596-4402c04cb91f2c76698dc743;Parent=caab38549f480041;Sampled=1 amz-sdk-invocation-id: - d36ab502-01eb-4144-89e9-ab364c3ad6af amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/invoke-with-response-stream response: body: string: !!binary | AAAApQAAAEs861eXCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SnRaWE56WVdkbFUzUmhj blFpT25zaWNtOXNaU0k2SW1GemMybHpkR0Z1ZENKOWZRPT0iLCJwIjoiYWJjIn1dU3b9AAAA+AAA AEv8aFztCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pz b24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4 MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lQSFJvYVc1cmFXNW5JbjBzSW1OdmJuUmxiblJD Ykc5amEwbHVaR1Y0SWpvd2ZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNE RUZHSElKS0wifajL48sAAADkAAAAS1l4Jm4LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10 eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlK amIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3dmWDA9 IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldY WVowMTIzNDUifbtHABEAAADQAAAASw3ZOCgLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10 eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlK amIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaVBpQlVhR1Vp ZlN3aVkyOXVkR1Z1ZEVKc2IyTnJTVzVrWlhnaU9qRjlmUT09IiwicCI6ImFiIn25bWaoAAAAsgAA AEvuKxwFCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pz b24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5 d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam94ZlgwPSIsInAiOiJhYmNkZWZnaCJ9cDKd mwAAAPkAAABLwQh1XQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNh dGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2 WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pSUZWelpYSWlmU3dpWTI5dWRHVnVk RUpzYjJOclNXNWtaWGdpT2pKOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpB QkNERUZHSElKS0xNTk9QUSJ9hcX/0gAAANkAAABLAMlaWQs6ZXZlbnQtdHlwZQcABWNodW5rDTpj b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0 ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0 SWpveWZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9Q UVJTVFUifVpq3h4AAADoAAAAS5yIy28LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBl BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIy NTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlHaGhjeUo5TENK amIyNTBaVzUwUW14dlkydEpibVJsZUNJNk0zMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1 dnd4eXpBQkNEIn0ubaSPAAAAuAAAAEukmwSkCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5 SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam96Zlgw PSIsInAiOiJhYmNkZWZnaGlqa2xtbiJ9rs+WSgAAAOAAAABLrPiArgs6ZXZlbnQtdHlwZQcABWNo dW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu dHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpY aDBJam9pSUhKbGNYVmxjM1JsWkNCMGFHVWlmU3dpWTI5dWRHVnVkRUpzYjJOclNXNWtaWGdpT2pS OWZRPT0iLCJwIjoiYWJjZGVmIn0WBTZCAAAArwAAAEt2W082CzpldmVudC10eXBlBwAFY2h1bmsN OmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJi eXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpH VjRJam8wZlgwPSIsInAiOiJhYmNkZSJ9A/fd7gAAAPUAAABLBPiYXAs6ZXZlbnQtdHlwZQcABWNo dW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu dHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpY aDBJam9pSUdOMWNuSmxiblFnZDJWaGRHaGxjaUJwYmlKOUxDSmpiMjUwWlc1MFFteHZZMnRKYm1S bGVDSTZOWDE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3In3C+bj/AAAAwAAAAEttOa+q CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1l c3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJ bU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam8xZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJz dHV2In3xyGKDAAABDAAAAEuKgHnVCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUw Wlc1MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lJRk5sWVhSMGJHVWdZ VzVrSUZOaGJpSjlMQ0pqYjI1MFpXNTBRbXh2WTJ0SmJtUmxlQ0k2Tm4xOSIsInAiOiJhYmNkZWZn aGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWCJ963WBJAAAANgAAABL Palz6Qs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJ anA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpvMmZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5v cHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVCJ9HxAA7AAAAQIAAABLNbDHtAs6ZXZlbnQt dHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5 cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZ U0k2ZXlKMFpYaDBJam9pSUVaeVlXNWphWE5qYnk0aWZTd2lZMjl1ZEdWdWRFSnNiMk5yU1c1a1pY Z2lPamQ5ZlE9PSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1O T1BRUiJ9LBudaAAAAK4AAABLSztmhgs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1 MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpvM2ZYMD0iLCJw IjoiYWJjZCJ9udoJSQAAARQAAABL2hCllgs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5 cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpq YjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pSUVrZ2JtVmxa Q0IwYnlCMWMyVWlmU3dpWTI5dWRHVnVkRUpzYjJOclNXNWtaWGdpT2poOWZRPT0iLCJwIjoiYWJj ZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0NSJ9 ke149AAAAKwAAABLMfs15gs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBs aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBR bXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpvNGZYMD0iLCJwIjoiYWIi feBIVcgAAADjAAAAS+tY+n4LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBw bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUw UW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlIUm9aU0JjSWlKOUxDSmpi MjUwWlc1MFFteHZZMnRKYm1SbGVDSTZPWDE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dSJ9 XCFiXwAAALkAAABLmfstFAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBs aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBR bXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpvNWZYMD0iLCJwIjoiYWJj ZGVmZ2hpamtsbW5vIn2CfEGxAAABCQAAAEtCYPalCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRl bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6 ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2laMlYw WDJOMWNuSmxiblJmZDJWaGRHaGxjbHdpSW4wc0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3hN SDE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTSJ96PS/RgAA ALUAAABLXAvAFQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlv bi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0 VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpveE1IMTkiLCJwIjoiYWJjZGVmZ2hp amsifa3oxsUAAADqAAAAS+ZImA8LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQ YXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBa VzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlIUnZiMndnWm05eUlu MHNJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam94TVgxOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9w cXJzdHV2d3gifTG/qNYAAAC0AAAAS2Fr6aULOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10 eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlK amIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3hNWDE5 IiwicCI6ImFiY2RlZmdoaWoifeNmOaEAAADxAAAAS/F4PpwLOmV2ZW50LXR5cGUHAAVjaHVuaw06 Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5 dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpv aUlHVmhZMmdnWTJsMGVTSjlMQ0pqYjI1MFpXNTBRbXh2WTJ0SmJtUmxlQ0k2TVRKOWZRPT0iLCJw IjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBIn11ND8/AAAArAAAAEsx+zXmCzpldmVudC10 eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlw ZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJu UkNiRzlqYTBsdVpHVjRJam94TW4xOSIsInAiOiJhYiJ9sqEFYgAAAOgAAABLnIjLbws6ZXZlbnQt dHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5 cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZ U0k2ZXlKMFpYaDBJam9pSUhObGNHRnlZWFJsYkhrdUlFa2dkMmxzYkNKOUxDSmpiMjUwWlc1MFFt eHZZMnRKYm1SbGVDSTZNVE45ZlE9PSIsInAiOiJhYmNkZWYiffB7RfsAAACzAAAAS9NLNbULOmV2 ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZi blJsYm5SQ2JHOWphMGx1WkdWNElqb3hNMzE5IiwicCI6ImFiY2RlZmdoaSJ9Y9vX5gAAANIAAABL dxlrSAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZ U0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pSUcxaGEyVWlmU3dpWTI5dWRHVnVkRUpzYjJOclNX NWtaWGdpT2pFMGZYMD0iLCJwIjoiYWJjZCJ92g3LtAAAAMoAAABLJ4m3Cws6ZXZlbnQtdHlwZQcA BWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVl dmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5 amEwbHVaR1Y0SWpveE5IMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUYi fWDb9qgAAADnAAAASx7YXL4LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBw bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUw UW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlIUjNieUo5TENKamIyNTBa VzUwUW14dlkydEpibVJsZUNJNk1UVjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3 eHkiffZ18igAAADQAAAASw3ZOCgLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQ YXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBa VzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3hOWDE5IiwicCI6 ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMIn3iWd4+AAABBAAAAEu68DIU CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1l c3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZl eUprWld4MFlTSTZleUowWlhoMElqb2lJSFJ2YjJ3aWZTd2lZMjl1ZEdWdWRFSnNiMk5yU1c1a1pY Z2lPakUyZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1O T1BRUlNUVVZXWFlaMDEifZS8YsEAAAC/AAAASxa72LQLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29u dGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVz IjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElq b3hObjE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dSJ9yXXDhAAAANYAAABLgpnNiAs6ZXZl bnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdl LXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pX eDBZU0k2ZXlKMFpYaDBJam9pSUdOaGJHeHpMQ0o5TENKamIyNTBaVzUwUW14dlkydEpibVJsZUNJ Nk1UZDlmUT09IiwicCI6ImFiY2QiffBLbZQAAACwAAAAS5TrT2ULOmV2ZW50LXR5cGUHAAVjaHVu aw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7 ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1 WkdWNElqb3hOMzE5IiwicCI6ImFiY2RlZiJ9hboguQAAAP8AAABLTkiA/Qs6ZXZlbnQtdHlwZQcA BWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVl dmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlK MFpYaDBJam9pSUc5dVpTQm1iM0lnWldGamFDSjlMQ0pqYjI1MFpXNTBRbXh2WTJ0SmJtUmxlQ0k2 TVRoOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKSyJ9GMZh swAAALkAAABLmfstFAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNh dGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2 WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpveE9IMTkiLCJwIjoiYWJjZGVm Z2hpamtsbW5vIn0WCfBdAAABCgAAAEsFwIx1CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5 SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lJR05wZEhr dVBDOGlmU3dpWTI5dWRHVnVkRUpzYjJOclNXNWtaWGdpT2pFNWZYMD0iLCJwIjoiYWJjZGVmZ2hp amtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjMiffD9n1EAAADe AAAAS7LphkkLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24v anNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRk Rzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3hPWDE5IiwicCI6ImFiY2RlZmdoaWpr bG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVoifXgABqwAAADZAAAASwDJ WlkLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06 bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJ NmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaWRHaHBibXRwYm1jaWZTd2lZMjl1ZEdWdWRFSnNiMk5y U1c1a1pYZ2lPakl3ZlgwPSIsInAiOiJhYmNkZWZnIn1w5IiFAAAAxAAAAEuYuQlqCzpldmVudC10 eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlw ZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJu UkNiRzlqYTBsdVpHVjRJam95TUgxOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5eiJ9 LsRm7wAAAOkAAABLoeji3ws6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBs aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBR bXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pUGx4dUluMHNJbU52Ym5SbGJu UkNiRzlqYTBsdVpHVjRJam95TVgxOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFC Q0RFIn0DD2TYAAAAyQAAAEtgKc3bCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUw Wlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam95TVgxOSIsInAi OiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFIn0o2NSyAAABVwAAAEvFQ4cPCzpldmVu dC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2Ut dHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEdGeWRDSTZleUp6ZEdG eWRDSTZleUowYjI5c1ZYTmxJanA3SW5SdmIyeFZjMlZKWkNJNklqTmpaRFZqTkdSbUxURTFOREF0 TkdabFpDMDVNek5tTFRjd04yWTVZMkU0TWpnMll5SXNJbTVoYldVaU9pSm5aWFJmWTNWeWNtVnVk RjkzWldGMGFHVnlJbjE5TENKamIyNTBaVzUwUW14dlkydEpibVJsZUNJNk1qSjlmUT09IiwicCI6 ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGRyJ9v9+JjgAAATEAAABL0zEF4gs6ZXZl bnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdl LXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pX eDBZU0k2ZXlKMGIyOXNWWE5sSWpwN0ltbHVjSFYwSWpvaWUxd2liRzlqWVhScGIyNWNJanBjSWxO bFlYUjBiR1ZjSW4waWZYMHNJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam95TW4xOSIsInAiOiJh YmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyIn2K F1mxAAAAxQAAAEul2SDaCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxp Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFt eHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam95TW4xOSIsInAiOiJhYmNk ZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekEifds1SaAAAAFOAAAAS6izcvwLOmV2ZW50LXR5cGUHAAVj aHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZl bnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkR0Z5ZENJNmV5SnpkR0Z5ZENJNmV5SjBi MjlzVlhObElqcDdJblJ2YjJ4VmMyVkpaQ0k2SWpKbE9UZGxNR0l3TFdKbFpqUXROR1EyTlMwNU56 YzBMV1UyTVdSbE4ySmpORFUwWXlJc0ltNWhiV1VpT2lKblpYUmZZM1Z5Y21WdWRGOTNaV0YwYUdW eUluMTlMQ0pqYjI1MFpXNTBRbXh2WTJ0SmJtUmxlQ0k2TWpSOWZRPT0iLCJwIjoiYWJjZGVmZ2hp amtsbW5vcHFyc3R1dnd4In3SOnqOAAABBwAAAEv9UEjECzpldmVudC10eXBlBwAFY2h1bmsNOmNv bnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRl cyI6ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowYjI5c1ZYTmxJ anA3SW1sdWNIVjBJam9pZTF3aWJHOWpZWFJwYjI1Y0lqcGNJbE5oYmlCR2NtRnVZMmx6WTI5Y0lu MGlmWDBzSW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpveU5IMTkiLCJwIjoiYWJjZGUifYxNAXQA AAC/AAAASxa72LQLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRp b24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlky dFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3lOSDE5IiwicCI6ImFiY2RlZmdo aWprbG1ub3BxcnN0dSJ92CO3QwAAANwAAABLyCnVKQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250 ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMi OiJleUp0WlhOellXZGxVM1J2Y0NJNmV5SnpkRzl3VW1WaGMyOXVJam9pZEc5dmJGOTFjMlVpZlgw PSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZX WFlaMDEifUVrSeoAAAJGAAAASx5XS5MLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBl BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKdFpY UmhaR0YwWVNJNmV5SjFjMkZuWlNJNmV5SnBibkIxZEZSdmEyVnVjeUk2TkRJM0xDSnZkWFJ3ZFhS VWIydGxibk1pT2pFMU5pd2lZMkZqYUdWU1pXRmtTVzV3ZFhSVWIydGxia052ZFc1MElqb3dMQ0pq WVdOb1pWZHlhWFJsU1c1d2RYUlViMnRsYmtOdmRXNTBJam93ZlN3aWJXVjBjbWxqY3lJNmUzMHNJ blJ5WVdObElqcDdmWDBzSW1GdFlYcHZiaTFpWldSeWIyTnJMV2x1ZG05allYUnBiMjVOWlhSeWFX TnpJanA3SW1sdWNIVjBWRzlyWlc1RGIzVnVkQ0k2TkRJM0xDSnZkWFJ3ZFhSVWIydGxia052ZFc1 MElqb3hOVFlzSW1sdWRtOWpZWFJwYjI1TVlYUmxibU41SWpvME16TXNJbVpwY25OMFFubDBaVXho ZEdWdVkza2lPall4TENKallXTm9aVkpsWVdSSmJuQjFkRlJ2YTJWdVEyOTFiblFpT2pBc0ltTmhZ MmhsVjNKcGRHVkpibkIxZEZSdmEyVnVRMjkxYm5RaU9qQjlmUT09IiwicCI6ImFiY2RlZmdoaWpr bG1ub3BxcnN0dXZ3eHl6QUIifU+69xo= headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Tue, 25 Mar 2025 14:40:18 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - 4b631f83-3042-446f-9552-70a93e64d9f3 status: code: 200 message: OK - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today? Please give one tool call for Seattle and one for San Francisco" } ] }, { "role": "assistant", "content": [ { "text": " The User has requested the current weather in Seattle and San Francisco. I need to use the \"get_current_weather\" tool for each city separately. I will make two tool calls, one for each city.\n" }, { "toolUse": { "toolUseId": "3cd5c4df-1540-4fed-933f-707f9ca8286c", "name": "get_current_weather", "input": { "location": "Seattle" } } }, { "toolUse": { "toolUseId": "2e97e0b0-bef4-4d65-9774-e61de7bc454c", "name": "get_current_weather", "input": { "location": "San Francisco" } } } ] }, { "role": "user", "content": [ { "toolResult": { "toolUseId": "3cd5c4df-1540-4fed-933f-707f9ca8286c", "content": [ { "json": { "weather": "50 degrees and raining" } } ] } }, { "toolResult": { "toolUseId": "2e97e0b0-bef4-4d65-9774-e61de7bc454c", "content": [ { "json": { "weather": "70 degrees and sunny" } } ] } } ] } ], "inferenceConfig": { "max_new_tokens": 1000 }, "schemaVersion": "messages-v1", "toolConfig": { "tools": [ { "toolSpec": { "name": "get_current_weather", "description": "Get the current weather in a given location.", "inputSchema": { "json": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } } } ] } } headers: Content-Length: - '1383' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250325T144019Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-28fe3f85-95dafaf23d34b554c77b907f;Parent=9e7cef38599a1f15;Sampled=1 amz-sdk-invocation-id: - a2ec7eb1-6482-4b23-8a2e-79952f46945d amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/invoke-with-response-stream response: body: string: !!binary | AAAA2wAAAEt6CQk5CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SnRaWE56WVdkbFUzUmhj blFpT25zaWNtOXNaU0k2SW1GemMybHpkR0Z1ZENKOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5v cHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0In1MbjWlAAAA/AAAAEsJ 6PotCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24N Om1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4MFlT STZleUprWld4MFlTSTZleUowWlhoMElqb2lQSFJvYVc1cmFXNW5JbjBzSW1OdmJuUmxiblJDYkc5 amEwbHVaR1Y0SWpvd2ZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZH SElKS0xNTk9QIn257bMkAAAA2AAAAEs9qXPpCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQt dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5 SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam93Zlgw PSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUIn0k 4PgxAAAA2wAAAEt6CQk5CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxp Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFt eHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lQaUJVYUdVZ2RHOXZiQ0o5TENK amIyNTBaVzUwUW14dlkydEpibVJsZUNJNk1YMTkiLCJwIjoiYWJjZGVmZ2hpIn1X/XRLAAAA5QAA AEtkGA/eCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pz b24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5 d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam94ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xt bm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQ1NiJ9QpKrTgAAAOoA AABL5kiYDws6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9q c29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpX eDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pSUdoaGN5SjlMQ0pqYjI1MFpXNTBRbXh2WTJ0 SmJtUmxlQ0k2TW4xOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRiJ9kQRG dQAAANMAAABLSnlC+As6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNh dGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2 WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpveWZYMD0iLCJwIjoiYWJjZGVm Z2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk8ifXBEvIMAAAEOAAAAS/BAKrULOmV2 ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5Smta V3gwWVNJNmV5SjBaWGgwSWpvaUlISmxkSFZ5Ym1Wa0lIUm9aU0JqZFhKeVpXNTBJbjBzSW1OdmJu UmxiblJDYkc5amEwbHVaR1Y0SWpvemZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4 eXpBQkNERUZHSElKS0xNTk9QUVIifcJLEH8AAADPAAAAS+9pOHsLOmV2ZW50LXR5cGUHAAVjaHVu aw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7 ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1 WkdWNElqb3pmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSksi fcMOLUgAAAEMAAAAS4qAedULOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBw bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUw UW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlIZGxZWFJvWlhJZ1ptOXlJ bjBzSW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpvMGZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5v cHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxIn0Td3V3AAAA1wAAAEu/+eQ4 CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1l c3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJ bU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam8wZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJz dHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlMifWjvBMQAAAERAAAASxLwKuYLOmV2ZW50LXR5cGUH AAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5 SjBaWGgwSWpvaUlHSnZkR2dnVTJWaGRIUnNaU0o5TENKamIyNTBaVzUwUW14dlkydEpibVJsZUNJ Nk5YMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJT VFVWV1hZWjAxMjM0NTYifWyeQxIAAADBAAAAS1BZhhoLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29u dGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVz IjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElq bzFmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3In1HRK7nAAABHAAAAEvqYO5XCzpl dmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh Z2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZleUpr Wld4MFlTSTZleUowWlhoMElqb2lJR0Z1WkNCVFlXNGdSbkpoYm1OcGMyTnZMaUJPYjNjaWZTd2lZ Mjl1ZEdWdWRFSnNiMk5yU1c1a1pYZ2lPalo5ZlE9PSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJz dHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEifQyx2dcAAADeAAAAS7LphkkLOmV2 ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZi blJsYm5SQ2JHOWphMGx1WkdWNElqbzJmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3 eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVoifXpuHuUAAAEHAAAAS/1QSMQLOmV2ZW50LXR5 cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBl BwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJ NmV5SjBaWGgwSWpvaUlFa2dkMmxzYkNKOUxDSmpiMjUwWlc1MFFteHZZMnRKYm1SbGVDSTZOMzE5 IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldY WVowMTIzNCJ9icP47wAAALoAAABL3ltXxAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5 cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpq YjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpvM2ZYMD0i LCJwIjoiYWJjZGVmZ2hpamtsbW5vcCJ9VNMdhAAAAPgAAABL/Ghc7Qs6ZXZlbnQtdHlwZQcABWNo dW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu dHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpY aDBJam9pSUhCeWIzWnBaR1VpZlN3aVkyOXVkR1Z1ZEVKc2IyTnJTVzVrWlhnaU9qaDlmUT09Iiwi cCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMIn0cLXNNAAAA4AAAAEus +ICuCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24N Om1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lq cDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam80ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9w cXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEifc1Vq6QAAADuAAAASxPIPs8L OmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5 SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlIUm9aU0JWYzJWeUluMHNJbU52Ym5SbGJuUkNiRzlqYTBs dVpHVjRJam81ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCIn0/PSyFAAAA swAAAEvTSzW1CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRU ZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam81ZlgwPSIsInAiOiJhYmNkZWZnaGki fUTpCiwAAAD+AAAAS3MoqU0LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBw bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUw UW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlIZHBkR2dnZEdobElHbHVa bTl5YldGMGFXOXVJbjBzSW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpveE1IMTkiLCJwIjoiYWJj ZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQiJ9gG89/wAAALQAAABLYWvppQs6ZXZlbnQtdHlwZQcA BWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVl dmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5 amEwbHVaR1Y0SWpveE1IMTkiLCJwIjoiYWJjZGVmZ2hpaiJ9SpFz4AAAAN0AAABL9Un8mQs6ZXZl bnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdl LXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pX eDBZU0k2ZXlKMFpYaDBJam9pTGp3dkluMHNJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam94TVgx OSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzIn3q3S2RAAAAxQAAAEul2SDaCzpldmVudC10eXBl BwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA BWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNi RzlqYTBsdVpHVjRJam94TVgxOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekEifRu+ F4wAAADgAAAAS6z4gK4LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14 dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaWRHaHBibXRwYm1jaWZTd2lZMjl1 ZEdWdWRFSnNiMk5yU1c1a1pYZ2lPakV5ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbiJ9LE2TnQAA ANgAAABLPalz6Qs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlv bi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0 VGRHOXdJanA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpveE1uMTkiLCJwIjoiYWJjZGVmZ2hp amtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVCJ9TDoSGAAAAOgAAABLnIjLbws6 ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlK a1pXeDBZU0k2ZXlKMFpYaDBJam9pUGx4dVhHNGlmU3dpWTI5dWRHVnVkRUpzYjJOclNXNWtaWGdp T2pFemZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoifY0IpRYAAADhAAAAS5GY qR4LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06 bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpw N0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3hNMzE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3Bx cnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIifYxQECoAAADpAAAAS6Ho4t8L OmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5 SmtaV3gwWVNJNmV5SjBaWGgwSWpvaVZHaGxJbjBzSW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpv eE5IMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERSJ9ER7hEwAAALMAAABL 00s1tQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0VGRHOXdJ anA3SW1OdmJuUmxiblJDYkc5amEwbHVaR1Y0SWpveE5IMTkiLCJwIjoiYWJjZGVmZ2hpIn0sgov2 AAAA4AAAAEus+ICuCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZ MnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lJR04xY25KbGJuUWlmU3dpWTI5dWRH VnVkRUpzYjJOclNXNWtaWGdpT2pFMWZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW4ifcLGafwAAADB AAAAS1BZhhoLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24v anNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRk Rzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3hOWDE5IiwicCI6ImFiY2RlZmdoaWpr bG1ub3BxcnN0dXZ3In3eiQEeAAABCgAAAEsFwIx1CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRl bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6 ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUowWlhoMElqb2lJSGRs WVhSb1pYSWdhVzRnVTJWaGRIUnNaU0JwY3lBaWZTd2lZMjl1ZEdWdWRFSnNiMk5yU1c1a1pYZ2lP akUyZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUoifXVaLBsA AADFAAAAS6XZINoLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRp b24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlky dFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3hObjE5IiwicCI6ImFiY2RlZmdo aWprbG1ub3BxcnN0dXZ3eHl6QSJ9GZ5piQAAAPQAAABLOZix7As6ZXZlbnQtdHlwZQcABWNodW5r DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Ynl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJ am9pTlRBZ1pHVm5jbVZsY3lCaGJtUWdjbUZwYm1sdVp5SjlMQ0pqYjI1MFpXNTBRbXh2WTJ0SmJt UmxlQ0k2TVRkOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW4ifamK9XoAAADZAAAASwDJWlkLOmV2 ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZi blJsYm5SQ2JHOWphMGx1WkdWNElqb3hOMzE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3 eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVIn1TzLKOAAAA2wAAAEt6CQk5CzpldmVudC10eXBlBwAF Y2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2 ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUow WlhoMElqb2lMaUo5TENKamIyNTBaVzUwUW14dlkydEpibVJsZUNJNk1UaDlmUT09IiwicCI6ImFi Y2RlZmdoaWprbG1ub3BxIn1LG/TKAAAA2QAAAEsAyVpZCzpldmVudC10eXBlBwAFY2h1bmsNOmNv bnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRl cyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlqYTBsdVpHVjRJ am94T0gxOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BR UlNUVSJ9MQPQswAAAPQAAABLOZix7As6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1 MFpXNTBRbXh2WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pSUZSb1pTSjlMQ0pq YjI1MFpXNTBRbXh2WTJ0SmJtUmxlQ0k2TVRsOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFy c3R1dnd4eXpBQkNERUZHSElKS0wifZ8ruDEAAADeAAAAS7LphkkLOmV2ZW50LXR5cGUHAAVjaHVu aw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7 ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1 WkdWNElqb3hPWDE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktM TU5PUFFSU1RVVldYWVoifXgABqwAAADVAAAAS8U5t1gLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29u dGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVz IjoiZXlKamIyNTBaVzUwUW14dlkydEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUlH TjFjbkpsYm5RaWZTd2lZMjl1ZEdWdWRFSnNiMk5yU1c1a1pYZ2lPakl3ZlgwPSIsInAiOiJhYmMi fW1/DK8AAADgAAAAS6z4gK4LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBw bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUw UW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdWNElqb3lNSDE5IiwicCI6ImFi Y2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMSJ9mw0B +AAAAQ0AAABLt+BQZQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNh dGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUpqYjI1MFpXNTBRbXh2 WTJ0RVpXeDBZU0k2ZXlKa1pXeDBZU0k2ZXlKMFpYaDBJam9pSUhkbFlYUm9aWElnYVc0Z1UyRnVJ RVp5WVc1amFYTmpieUJwY3lKOUxDSmpiMjUwWlc1MFFteHZZMnRKYm1SbGVDSTZNakY5ZlE9PSIs InAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFIn1MXAtBAAAA2AAAAEs9qXPpCzpl dmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh Z2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52 Ym5SbGJuUkNiRzlqYTBsdVpHVjRJam95TVgxOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2 d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUIn168Y5mAAABFgAAAEug0Pb2CzpldmVudC10eXBlBwAF Y2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2 ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRFWld4MFlTSTZleUprWld4MFlTSTZleUow WlhoMElqb2lJRGN3SUdSbFozSmxaWE1nWVc1a0lITjFibTU1SW4wc0ltTnZiblJsYm5SQ2JHOWph MGx1WkdWNElqb3lNbjE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJ SktMTU5PUFFSU1RVVldYWVoifd/LaPYAAADMAAAAS6jJQqsLOmV2ZW50LXR5cGUHAAVjaHVuaw06 Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5 dGVzIjoiZXlKamIyNTBaVzUwUW14dlkydFRkRzl3SWpwN0ltTnZiblJsYm5SQ2JHOWphMGx1WkdW NElqb3lNbjE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0gifRDaM/AA AAEDAAAASwjQ7gQLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRp b24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKamIyNTBaVzUwUW14dlky dEVaV3gwWVNJNmV5SmtaV3gwWVNJNmV5SjBaWGgwSWpvaUxpSjlMQ0pqYjI1MFpXNTBRbXh2WTJ0 SmJtUmxlQ0k2TWpOOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZH SElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0In38bbDJAAAArwAAAEt2W082CzpldmVudC10eXBlBwAF Y2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2 ZW50eyJieXRlcyI6ImV5SmpiMjUwWlc1MFFteHZZMnRUZEc5d0lqcDdJbU52Ym5SbGJuUkNiRzlq YTBsdVpHVjRJam95TTMxOSIsInAiOiJhYmNkZSJ90lbQowAAANkAAABLAMlaWQs6ZXZlbnQtdHlw ZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUH AAVldmVudHsiYnl0ZXMiOiJleUp0WlhOellXZGxVM1J2Y0NJNmV5SnpkRzl3VW1WaGMyOXVJam9p Wlc1a1gzUjFjbTRpZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdI SUpLTE1OT1BRUlNUVVZXWFkifTqBfD4AAAJNAAAAS2mHeoILOmV2ZW50LXR5cGUHAAVjaHVuaw06 Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5 dGVzIjoiZXlKdFpYUmhaR0YwWVNJNmV5SjFjMkZuWlNJNmV5SnBibkIxZEZSdmEyVnVjeUk2TlRZ NExDSnZkWFJ3ZFhSVWIydGxibk1pT2pVMkxDSmpZV05vWlZKbFlXUkpibkIxZEZSdmEyVnVRMjkx Ym5RaU9qQXNJbU5oWTJobFYzSnBkR1ZKYm5CMWRGUnZhMlZ1UTI5MWJuUWlPakI5TENKdFpYUnlh V056SWpwN2ZTd2lkSEpoWTJVaU9udDlmU3dpWVcxaGVtOXVMV0psWkhKdlkyc3RhVzUyYjJOaGRH bHZiazFsZEhKcFkzTWlPbnNpYVc1d2RYUlViMnRsYmtOdmRXNTBJam8xTmpnc0ltOTFkSEIxZEZS dmEyVnVRMjkxYm5RaU9qVTJMQ0pwYm5adlkyRjBhVzl1VEdGMFpXNWplU0k2TkRRMUxDSm1hWEp6 ZEVKNWRHVk1ZWFJsYm1ONUlqbzJNaXdpWTJGamFHVlNaV0ZrU1c1d2RYUlViMnRsYmtOdmRXNTBJ am93TENKallXTm9aVmR5YVhSbFNXNXdkWFJVYjJ0bGJrTnZkVzUwSWpvd2ZYMD0iLCJwIjoiYWJj ZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNIn0QhkpC headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Tue, 25 Mar 2025 14:40:19 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - 1c7282d2-8bd0-4979-ac7a-c6edde93f739 status: code: 200 message: OK version: 1 test_invoke_model_with_response_stream_with_content_tool_call[anthropic.claude].yaml000066400000000000000000000734441527070072300572470ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettesinteractions: - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today? Please give one tool call for Seattle and one for San Francisco", "type": "text" } ] } ], "anthropic_version": "bedrock-2023-05-31", "tools": [ { "name": "get_current_weather", "description": "Get the current weather in a given location.", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } ], "max_tokens": 1000 } headers: Content-Length: - '513' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250325T163434Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-9caf13be-645b4e873de5413a21fd5559;Parent=86c9f4d15772db4a;Sampled=1 amz-sdk-invocation-id: - cb26d944-c107-4af2-9e43-c712c373e0f2 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-3-5-sonnet-20240620-v1%3A0/invoke-with-response-stream response: body: string: !!binary | AAAB2QAAAEvLlYn8CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaWJXVnpj MkZuWlY5emRHRnlkQ0lzSW0xbGMzTmhaMlVpT25zaWFXUWlPaUp0YzJkZlltUnlhMTh3TVU1SGFI QmhlVXhGTlRKTWQxUjZZMmRuUzNWa1kzRWlMQ0owZVhCbElqb2liV1Z6YzJGblpTSXNJbkp2YkdV aU9pSmhjM05wYzNSaGJuUWlMQ0p0YjJSbGJDSTZJbU5zWVhWa1pTMHpMVFV0YzI5dWJtVjBMVEl3 TWpRd05qSXdJaXdpWTI5dWRHVnVkQ0k2VzEwc0luTjBiM0JmY21WaGMyOXVJanB1ZFd4c0xDSnpk Rzl3WDNObGNYVmxibU5sSWpwdWRXeHNMQ0oxYzJGblpTSTZleUpwYm5CMWRGOTBiMnRsYm5NaU9q TTVNaXdpYjNWMGNIVjBYM1J2YTJWdWN5STZOSDE5ZlE9PSIsInAiOiJhYmNkZWZnaGlqa2xtbm9w cXJzdHV2d3h5ekEifcxo58YAAAEXAAAAS52w30YLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVu dC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoi ZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOXpkR0Z5ZENJc0ltbHVaR1Y0SWpvd0xDSmpi MjUwWlc1MFgySnNiMk5ySWpwN0luUjVjR1VpT2lKMFpYaDBJaXdpZEdWNGRDSTZJaUo5ZlE9PSIs InAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFla MDEyMzQifZCt+WAAAAD5AAAAS8EIdV0LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBl BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVY QmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJ NmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSkRaWEowWVdsdWJIa2hJRWtp ZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm8ifdd8tHsAAAEbAAAAS1hAMkcLOmV2ZW50LXR5cGUH AAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0lt bHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhR aU9pSW5iR3dnZFhObElIUm9aU0JuWlhRaWZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1 dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTIn16qOayAAABFAAAAEvaEKWWCzpldmVudC10eXBlBwAF Y2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2 ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1 WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlP aUpmWTNWeWNtVnVkRjkzWldGMGFHVnlJR1oxYm1OMGFXOXVJbjE5IiwicCI6ImFiY2RlZmdoaWpr bG1ub3BxcnN0dXZ3eHl6In27RoiRAAABBAAAAEu68DIUCzpldmVudC10eXBlBwAFY2h1bmsNOmNv bnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRl cyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dM Q0prWld4MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnZEc4Z1ky aGxZMnNnZEdobElIZGxZWFJvWlhJZ1ptOXlJbjE5IiwicCI6ImFiY2RlZmdoaWoifUwnIAgAAAE6 AAAAS6ThNPMLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24v anNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVk RjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRH VjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSWdZbTkwYUNCVFpXRjBkR3hsSUdGdVpDQlRZVzRnUm5K aGJtTnBjMk52SW4xOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpL TE1OT1BRUlNUVVZXWFlaMDEyMyJ9sJyB3QAAATEAAABL0zEF4gs6ZXZlbnQtdHlwZQcABWNodW5r DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Ynl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lzSW1sdVpHVjRJ am93TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJc0luUmxlSFFpT2lJdUlF a25iR3dnYldGclpTQjBkMjhnYzJWd1lYSmhkR1VpZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9w cXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyIn2ePEn5AAABCQAAAEtCYPal CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1l c3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWph MTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pX eDBZU0lzSW5SbGVIUWlPaUlnWm5WdVkzUnBiMjRnWTJGc2JITWdkMmwwYUdsdUluMTkiLCJwIjoi YWJjZGVmZ2hpamtsbW5vcHFycyJ91VmQFwAAASAAAABLjrG70As6ZXZlbnQtdHlwZQcABWNodW5r DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi Ynl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lzSW1sdVpHVjRJ am93TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJc0luUmxlSFFpT2lJZ2RH aGxJSE5oYldVZ1lteHZZMnNpZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFC Q0RFRkdISUpLTE1OT1BRUlNUVVZXWCJ98HrGKgAAARMAAABLaDB5hgs6ZXZlbnQtdHlwZQcABWNo dW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu dHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lzSW1sdVpH VjRJam93TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJc0luUmxlSFFpT2lJ Z1ptOXlJSFJvWlhObElHTnBkR2xsY3k0aWZYMD0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1 dnd4eXpBQkNERUZHIn0uOeatAAAAxAAAAEuYuQlqCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRl bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6 ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTl6ZEc5d0lpd2lhVzVrWlhnaU9qQjkiLCJw IjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSCJ9/DIv/gAAAUwAAABL0nMhnAs6 ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz YWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5 emRHRnlkQ0lzSW1sdVpHVjRJam94TENKamIyNTBaVzUwWDJKc2IyTnJJanA3SW5SNWNHVWlPaUow YjI5c1gzVnpaU0lzSW1sa0lqb2lkRzl2YkhWZlltUnlhMTh3TVV0aGRHbEhhV3BXUm1JNWVYVk5V R051Vm1zek9HVWlMQ0p1WVcxbElqb2laMlYwWDJOMWNuSmxiblJmZDJWaGRHaGxjaUlzSW1sdWNI VjBJanA3ZlgxOSIsInAiOiJhYmNkZWZnaGlqIn1C2DPVAAAA/QAAAEs0iNOdCzpldmVudC10eXBl BwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA BWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJ bWx1WkdWNElqb3hMQ0prWld4MFlTSTZleUowZVhCbElqb2lhVzV3ZFhSZmFuTnZibDlrWld4MFlT SXNJbkJoY25ScFlXeGZhbk52YmlJNklpSjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ubyJ9eV3i iQAAAQsAAABLOKClxQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNh dGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1 ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lzSW1sdVpHVjRJam94TENKa1pXeDBZU0k2ZXlKMGVYQmxJ am9pYVc1d2RYUmZhbk52Ymw5a1pXeDBZU0lzSW5CaGNuUnBZV3hmYW5OdmJpSTZJbnRjSW14dlky RjBhVzhpZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcSJ9YJXmlgAAATYAAABLYRHZ8gs6ZXZl bnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdl LXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pX eDBZU0lzSW1sdVpHVjRJam94TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pYVc1d2RYUmZhbk52Ymw5 a1pXeDBZU0lzSW5CaGNuUnBZV3hmYW5OdmJpSTZJbTVjSWpvZ1hDSlRaV0YwZENKOWZRPT0iLCJw IjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAx MjMifb0l8mwAAAD3AAAAS344yzwLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQ YXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJ am9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpveExDSmtaV3gwWVNJNmV5 SjBlWEJsSWpvaWFXNXdkWFJmYW5OdmJsOWtaV3gwWVNJc0luQmhjblJwWVd4ZmFuTnZiaUk2SW14 bFhDSjlJbjE5IiwicCI6ImFiY2RlIn1aCch2AAAAugAAAEveW1fECzpldmVudC10eXBlBwAFY2h1 bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTl6ZEc5d0lpd2lhVzVrWlhn aU9qRjkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4In3PkCZGAAABVgAAAEv4I66/Czpl dmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh Z2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTl6 ZEdGeWRDSXNJbWx1WkdWNElqb3lMQ0pqYjI1MFpXNTBYMkpzYjJOcklqcDdJblI1Y0dVaU9pSjBi MjlzWDNWelpTSXNJbWxrSWpvaWRHOXZiSFZmWW1SeWExOHdNVlI2WWpNMVJYbGtTMGhYZWpSVlds QXpjSFZVWWtZaUxDSnVZVzFsSWpvaVoyVjBYMk4xY25KbGJuUmZkMlZoZEdobGNpSXNJbWx1Y0hW MElqcDdmWDE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0In0JMwU+AAABFQAAAEvncIwmCzpl dmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh Z2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlr Wld4MFlTSXNJbWx1WkdWNElqb3lMQ0prWld4MFlTSTZleUowZVhCbElqb2lhVzV3ZFhSZmFuTnZi bDlrWld4MFlTSXNJbkJoY25ScFlXeGZhbk52YmlJNklpSjlmUT09IiwicCI6ImFiY2RlZmdoaWpr bG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTSJ967ZY0QAAAScAAABLPJFnwAs6ZXZlbnQtdHlw ZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUH AAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lz SW1sdVpHVjRJam95TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pYVc1d2RYUmZhbk52Ymw5a1pXeDBZ U0lzSW5CaGNuUnBZV3hmYW5OdmJpSTZJbnRjSW14dlkyRjBhVzl1SW4xOSIsInAiOiJhYmNkZWZn aGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlMifb3cxEIAAAESAAAAS1VQUDYL OmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz c2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amEx OWtaV3gwWVNJc0ltbHVaR1Y0SWpveUxDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWFXNXdkWFJmYW5O dmJsOWtaV3gwWVNJc0luQmhjblJwWVd4ZmFuTnZiaUk2SWx3aU9pQmNJbE5oYmlBaWZYMD0iLCJw IjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4In0nyZ4GAAAA/AAAAEsJ6PotCzpldmVudC10eXBl BwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA BWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJ bWx1WkdWNElqb3lMQ0prWld4MFlTSTZleUowZVhCbElqb2lhVzV3ZFhSZmFuTnZibDlrWld4MFlT SXNJbkJoY25ScFlXeGZhbk52YmlJNklrWnlZVzVqYVhOamJ5SjlmUT09IiwicCI6ImFiIn3c7Eog AAABHAAAAEvqYO5XCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVk R1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3lMQ0prWld4MFlTSTZleUowZVhCbElq b2lhVzV3ZFhSZmFuTnZibDlrWld4MFlTSXNJbkJoY25ScFlXeGZhbk52YmlJNklsd2lmU0o5ZlE9 PSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1Aiffd3t8IA AAC/AAAASxa72LQLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRp b24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRH VnVkRjlpYkc5amExOXpkRzl3SWl3aWFXNWtaWGdpT2pKOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9w cXJzdHV2d3h5ekFCQyJ9Gq1O0QAAASQAAABLezEdEAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250 ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMi OiJleUowZVhCbElqb2liV1Z6YzJGblpWOWtaV3gwWVNJc0ltUmxiSFJoSWpwN0luTjBiM0JmY21W aGMyOXVJam9pZEc5dmJGOTFjMlVpTENKemRHOXdYM05sY1hWbGJtTmxJanB1ZFd4c2ZTd2lkWE5o WjJVaU9uc2liM1YwY0hWMFgzUnZhMlZ1Y3lJNk1UTTBmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1u b3BxcnN0dXZ3eHl6QUJDREVGR0gifak0N2MAAAFAAAAASxeDzJ0LOmV2ZW50LXR5cGUHAAVjaHVu aw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7 ImJ5dGVzIjoiZXlKMGVYQmxJam9pYldWemMyRm5aVjl6ZEc5d0lpd2lZVzFoZW05dUxXSmxaSEp2 WTJzdGFXNTJiMk5oZEdsdmJrMWxkSEpwWTNNaU9uc2lhVzV3ZFhSVWIydGxia052ZFc1MElqb3pP VElzSW05MWRIQjFkRlJ2YTJWdVEyOTFiblFpT2prd0xDSnBiblp2WTJGMGFXOXVUR0YwWlc1amVT STZNemN5TWl3aVptbHljM1JDZVhSbFRHRjBaVzVqZVNJNk56YzVmWDA9IiwicCI6ImFiY2RlZiJ9 uDbBJw== headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Tue, 25 Mar 2025 16:34:35 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - 17808f83-6c84-4f3d-9e81-4007fd8f848e status: code: 200 message: OK - request: body: |- { "messages": [ { "role": "user", "content": [ { "text": "What is the weather in Seattle and San Francisco today? Please give one tool call for Seattle and one for San Francisco", "type": "text" } ] }, { "role": "assistant", "content": [ { "type": "text", "text": "Certainly! I'll use the get_current_weather function to check the weather for both Seattle and San Francisco. I'll make two separate function calls within the same block for these cities." }, { "type": "tool_use", "id": "toolu_bdrk_01KatiGijVFb9yuMPcnVk38e", "name": "get_current_weather", "input": { "location": "Seattle" } }, { "type": "tool_use", "id": "toolu_bdrk_01Tzb35EydKHWz4UZP3puTbF", "name": "get_current_weather", "input": { "location": "San Francisco" } } ] }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_bdrk_01KatiGijVFb9yuMPcnVk38e", "content": "50 degrees and raining" }, { "type": "tool_result", "tool_use_id": "toolu_bdrk_01Tzb35EydKHWz4UZP3puTbF", "content": "70 degrees and sunny" } ] } ], "anthropic_version": "bedrock-2023-05-31", "tools": [ { "name": "get_current_weather", "description": "Get the current weather in a given location.", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city" } }, "required": [ "location" ] } } ], "max_tokens": 1000 } headers: Content-Length: - '1297' User-Agent: - Boto3/1.35.56 md/Botocore#1.35.56 ua/2.0 os/linux#6.1.0-1034-oem md/arch#x86_64 lang/python#3.10.12 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.35.56 X-Amz-Date: - 20250325T163438Z X-Amz-Security-Token: - test_aws_security_token X-Amzn-Trace-Id: - Root=1-feef9d11-6a09175d24533ea9968f07ff;Parent=952177b027cf0daa;Sampled=1 amz-sdk-invocation-id: - 5ac7874b-78a1-4e44-8fa5-7afeabdc1343 amz-sdk-request: - attempt=1 authorization: - Bearer test_aws_authorization method: POST uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-3-5-sonnet-20240620-v1%3A0/invoke-with-response-stream response: body: string: !!binary | AAABwwAAAEvhxQbfCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaWJXVnpj MkZuWlY5emRHRnlkQ0lzSW0xbGMzTmhaMlVpT25zaWFXUWlPaUp0YzJkZlltUnlhMTh3TVRScFFV Rk5TMjlLVW1SdVpYUnlSa1ZoZFRVMk9Vc2lMQ0owZVhCbElqb2liV1Z6YzJGblpTSXNJbkp2YkdV aU9pSmhjM05wYzNSaGJuUWlMQ0p0YjJSbGJDSTZJbU5zWVhWa1pTMHpMVFV0YzI5dWJtVjBMVEl3 TWpRd05qSXdJaXdpWTI5dWRHVnVkQ0k2VzEwc0luTjBiM0JmY21WaGMyOXVJanB1ZFd4c0xDSnpk Rzl3WDNObGNYVmxibU5sSWpwdWRXeHNMQ0oxYzJGblpTSTZleUpwYm5CMWRGOTBiMnRsYm5NaU9q WXdNeXdpYjNWMGNIVjBYM1J2YTJWdWN5STZNbjE5ZlE9PSIsInAiOiJhYmNkZSJ9UzZi8gAAAPIA AABLtthETAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9q c29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRG OWliRzlqYTE5emRHRnlkQ0lzSW1sdVpHVjRJam93TENKamIyNTBaVzUwWDJKc2IyTnJJanA3SW5S NWNHVWlPaUowWlhoMElpd2lkR1Y0ZENJNklpSjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3Bx cnN0In2THpBxAAABIgAAAEv0ceiwCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcA EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJs SWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZl eUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUpjYmx4dVFtRnpaU0o5ZlE9PSIs InAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFla MDEyMzQ1NjcifaXZFy8AAAEvAAAASwzhLAELOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10 eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlK MGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gw WVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSmtJRzl1SUhSb1pTQnla WE4xYkhSekxDQm9aWEpsSW4xOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RF RkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQifaZF9F4AAAE+AAAAS1FhkjMLOmV2ZW50LXR5cGUH AAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF ZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0lt bHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhR aU9pSW5jeUIwYUdVZ2QyVmhkR2hsY2lCcGJtWnZjbTFoZEdsdmJpQm1iM0lnVTJWaGRIUnNaU0Jo Ym1RZ1UyRnVJbjE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktM TU4ifVbXVJMAAAEnAAAASzyRZ8ALOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQ YXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJ am9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5 SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSWdSbkpoYm1OcGMyTnZJSFJ2WkdG NU9seHVYRzR4SW4xOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpL TE1OT1BRUlNUVVZXIn099AtGAAABOAAAAEveIWeTCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRl bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6 ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0pr Wld4MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUl1SUZObFlYUjBi R1U2SUZSb1pTQmpkWEp5Wlc1MElIZGxZWFJvWlhJZ2FXNGdVMlZoZEhSc1pTSjlmUT09IiwicCI6 ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMIn18xkatAAABEgAAAEtVUFA2 CzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1l c3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWph MTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pX eDBZU0lzSW5SbGVIUWlPaUlnYVhNZ05UQWdaR1ZuY21WbGN5QmhibVFnY21GcGJtbHVaeUo5ZlE9 PSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdCJ9mICI9AAAAP0AAABLNIjTnQs6ZXZlbnQtdHlw ZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUH AAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lz SW1sdVpHVjRJam93TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJc0luUmxl SFFpT2lJdVhHNHlMaUJUWVc0Z1JuSmhibU5wYzJOdkluMTkiLCJwIjoiYWJjZGVmZ2hpamsifcEE 25AAAAFKAAAAS10z1DwLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5 dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJs SWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSTZJRlJvWlNCamRYSnlaVzUwSUhkbFlYUm9a WElnYVc0Z1UyRnVJRVp5WVc1amFYTmpieUJwY3lKOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5v cHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjMifbnZTbQAAAEzAAAAS6nx VoILOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06 bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5 amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlr Wld4MFlTSXNJblJsZUhRaU9pSWdOekFnWkdWbmNtVmxjeUJoYm1RZ2MzVnVibmt1SW4xOSIsInAi OiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEy MzQ1Njc4In09zRUvAAAA+QAAAEvBCHVdCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlw ZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBl WEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlT STZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUpjYmx4dVFYTWdlVzkxSUdO aGJpQnpaV1VzSW4xOSIsInAiOiJhYmNkZWZnIn1QpKk+AAABJQAAAEtGUTSgCzpldmVudC10eXBl BwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA BWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJ bWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVI UWlPaUlnZEdobGNtVW5jeUJ4ZFdsMFpTQmhJR1JwWm1abGNtVnVZMlVnYVc0Z2RHaGxJbjE5Iiwi cCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREUifaUyG3gAAAE7AAAAS5mBHUMLOmV2 ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWta V3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlT SXNJblJsZUhRaU9pSWdkMlZoZEdobGNpQmlaWFIzWldWdUlIUm9aWE5sSUhSM2J5QlhaWE4wSW4x OSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZX WFlaMDEyMzQifd2z7pkAAAD4AAAAS/xoXO0LOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10 eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlK MGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gw WVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSWdRMjloYzNRZ1kybDBh V1Z6SUhSdlpHRjVMaUo5ZlE9PSIsInAiOiJhYiJ9Rso+GgAAATYAAABLYRHZ8gs6ZXZlbnQtdHlw ZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUH AAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lz SW1sdVpHVjRJam93TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJc0luUmxl SFFpT2lJZ1UyVmhkSFJzWlNCcGN5QmxlSEJsY21sbGJtTnBibWNnWTI5dmJHVnlJSFJsYlhCbGNt RjBkWEpsY3lCM2FYUm9JbjE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUIifTMg TKgAAAElAAAAS0ZRNKALOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGlj YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5 dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5SjBlWEJs SWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSWdjbUZwYml3Z2QyaHBZMmdnYVhNZ1ptRnBj bXg1SW4xOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BR UlNUVSJ9T2tXLQAAAScAAABLPJFnwAs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhC bElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lzSW1sdVpHVjRJam93TENKa1pXeDBZU0k2 ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJc0luUmxlSFFpT2lJZ2RIbHdhV05oYkNCbWIzSWdk R2hsSUdOcGRIa3VJRTl1SW4xOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RF RkdISUpLTE1OT1BRUlMifdjQGr4AAAEQAAAASy+QA1YLOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29u dGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVz IjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xD SmtaV3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSWdkR2hsSUc5 MGFHVnlJR2hoYm1Rc0lGTmhiaUJHY21GdVkybHpZMjhnYVhNaWZYMD0iLCJwIjoiYWJjZGVmZ2hp aiJ906llVwAAAR4AAABLkKC9Nws6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBh cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElq b2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lzSW1sdVpHVjRJam93TENKa1pXeDBZU0k2ZXlK MGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJc0luUmxlSFFpT2lJZ1pXNXFiM2xwYm1jZ2QyRnliV1Z5 SUhSbGJYQmxjbUYwZFhKbGN5QjNhWFJvSW4xOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2 d3gifd59qdwAAAEoAAAAS77B8BELOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQ YXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJ am9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gwWVNJNmV5 SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSWdjM1Z1Ym5rZ1kyOXVaR2wwYVc5 dWN5d2diV0ZyYVc1bklHbDBJR0VnY0d4bFlYTmhiblFnWkdGNUluMTkiLCJwIjoiYWJjZGVmZ2hp amtsbW5vcHFyc3R1diJ9H4Nw/wAAASsAAABL+WGKwQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250 ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMi OiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lzSW1sdVpHVjRJam93TENK a1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJc0luUmxlSFFpT2lJZ2FXNGdkR2hs SUVKaGVTQkJjbVZoTGx4dVhHNUpjeUo5ZlE9PSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2 d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXIn1ePiFEAAABGwAAAEtYQDJHCzpldmVudC10eXBl BwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA BWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJ bWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVI UWlPaUlnZEdobGNtVWdZVzU1ZEdocGJtY2daV3h6WlNCNWIzVWlmWDA9IiwicCI6ImFiY2RlZmdo aWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGRyJ9IJonHgAAAQwAAABLioB51Qs6ZXZlbnQtdHlwZQcA BWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVl dmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lzSW1s dVpHVjRJam93TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJc0luUmxlSFFp T2lJblpDQnNhV3RsSUhSdkluMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNE RUZHSElKS0wifRFLQMQAAAEgAAAAS46xu9ALOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10 eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlK MGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmtaV3gw WVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSWdhMjV2ZHlCaFltOTFk Q0IwYUdVZ2QyVmhkR2hsY2lKOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpB QkNERUZHSElKS0wifZVK5CQAAAEJAAAAS0Jg9qULOmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVu dC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoi ZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOWtaV3gwWVNJc0ltbHVaR1Y0SWpvd0xDSmta V3gwWVNJNmV5SjBlWEJsSWpvaWRHVjRkRjlrWld4MFlTSXNJblJsZUhRaU9pSWdhVzRnZEdobGMy VWdZMmwwYVdWeklHOXlJbjE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3In3JZtxRAAAB GAAAAEsf4EiXCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1 ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0prWld4MFlTSTZleUowZVhCbElqb2lk R1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUlnWVc1NUlHOTBhR1Z5SUd4dlkyRjBhVzl1Y3lKOWZR PT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSCJ9eWu/OgAAANwAAABL yCnVKQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u DTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWli RzlqYTE5a1pXeDBZU0lzSW1sdVpHVjRJam93TENKa1pXeDBZU0k2ZXlKMGVYQmxJam9pZEdWNGRG OWtaV3gwWVNJc0luUmxlSFFpT2lJL0luMTkiLCJwIjoiYWIifYtZDXoAAADFAAAAS6XZINoLOmV2 ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn ZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pWTI5dWRHVnVkRjlpYkc5amExOXpk Rzl3SWl3aWFXNWtaWGdpT2pCOSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RF RkdISSJ9pG6vOAAAARcAAABLnbDfRgs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUH ABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhC bElqb2liV1Z6YzJGblpWOWtaV3gwWVNJc0ltUmxiSFJoSWpwN0luTjBiM0JmY21WaGMyOXVJam9p Wlc1a1gzUjFjbTRpTENKemRHOXdYM05sY1hWbGJtTmxJanB1ZFd4c2ZTd2lkWE5oWjJVaU9uc2li M1YwY0hWMFgzUnZhMlZ1Y3lJNk1UUTJmWDA9IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dSJ9 RviKwQAAAWMAAABLkeKZSQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBs aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2li V1Z6YzJGblpWOXpkRzl3SWl3aVlXMWhlbTl1TFdKbFpISnZZMnN0YVc1MmIyTmhkR2x2YmsxbGRI SnBZM01pT25zaWFXNXdkWFJVYjJ0bGJrTnZkVzUwSWpvMk1ETXNJbTkxZEhCMWRGUnZhMlZ1UTI5 MWJuUWlPakUwTml3aWFXNTJiMk5oZEdsdmJreGhkR1Z1WTNraU9qVXdNak1zSW1acGNuTjBRbmww WlV4aGRHVnVZM2tpT2pVMU9IMTkiLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNE RUZHSElKS0xNTk8ifQgFpq0= headers: Connection: - keep-alive Content-Type: - application/vnd.amazon.eventstream Date: - Tue, 25 Mar 2025 16:34:38 GMT Set-Cookie: test_set_cookie Transfer-Encoding: - chunked X-Amzn-Bedrock-Content-Type: - application/json x-amzn-RequestId: - 8376cb65-4e45-4f96-a8fc-31cefffb8ca6 status: code: 200 message: OK version: 1 conftest.py000066400000000000000000000153651527070072300362530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Unit tests configuration module.""" import json import os import boto3 import pytest import yaml from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.instrumentation.botocore.environment_variables import ( OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, ) from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import ( InMemoryLogRecordExporter, SimpleLogRecordProcessor, ) from opentelemetry.sdk.metrics import ( MeterProvider, ) from opentelemetry.sdk.metrics.export import ( InMemoryMetricReader, ) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) @pytest.fixture(scope="function", name="span_exporter") def fixture_span_exporter(): exporter = InMemorySpanExporter() yield exporter @pytest.fixture(scope="function", name="log_exporter") def fixture_log_exporter(): exporter = InMemoryLogRecordExporter() yield exporter @pytest.fixture(scope="function", name="metric_reader") def fixture_metric_reader(): reader = InMemoryMetricReader() yield reader @pytest.fixture(scope="function", name="tracer_provider") def fixture_tracer_provider(span_exporter): provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(span_exporter)) return provider @pytest.fixture(scope="function", name="logger_provider") def fixture_logger_provider(log_exporter): provider = LoggerProvider() provider.add_log_record_processor(SimpleLogRecordProcessor(log_exporter)) return provider @pytest.fixture(scope="function", name="meter_provider") def fixture_meter_provider(metric_reader): meter_provider = MeterProvider( metric_readers=[metric_reader], ) return meter_provider @pytest.fixture def bedrock_runtime_client(): return boto3.client("bedrock-runtime") @pytest.fixture(autouse=True) def environment(): if not os.getenv("AWS_ACCESS_KEY_ID"): os.environ["AWS_ACCESS_KEY_ID"] = "test_aws_access_key_id" if not os.getenv("AWS_SECRET_ACCESS_KEY"): os.environ["AWS_SECRET_ACCESS_KEY"] = "test_aws_secret_key" if not os.getenv("AWS_DEFAULT_REGION"): os.environ["AWS_DEFAULT_REGION"] = "us-east-1" @pytest.fixture(scope="module") def vcr_config(): return { "filter_headers": [ ("cookie", "test_cookie"), ("authorization", "Bearer test_aws_authorization"), ("X-Amz-Security-Token", "test_aws_security_token"), ], "decode_compressed_response": True, "before_record_response": scrub_response_headers, } @pytest.fixture(scope="function") def instrument_no_content(tracer_provider, logger_provider, meter_provider): os.environ.update({OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "False"}) instrumentor = BotocoreInstrumentor() instrumentor.instrument( tracer_provider=tracer_provider, logger_provider=logger_provider, meter_provider=meter_provider, ) yield instrumentor os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None) instrumentor.uninstrument() @pytest.fixture(scope="function") def instrument_with_content(tracer_provider, logger_provider, meter_provider): os.environ.update({OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "True"}) instrumentor = BotocoreInstrumentor() instrumentor.instrument( tracer_provider=tracer_provider, logger_provider=logger_provider, meter_provider=meter_provider, ) yield instrumentor os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None) instrumentor.uninstrument() def scrub_response_headers(response): """ This scrubs sensitive response headers. Note they are case-sensitive! """ response["headers"]["Set-Cookie"] = "test_set_cookie" # SDK validates response length which we modify by pretty-printing JSON. # Content length does not affect our instrumentation so just drop it. response["headers"].pop("Content-Length", None) return response class LiteralBlockScalar(str): """Formats the string as a literal block scalar, preserving whitespace and without interpreting escape characters""" def literal_block_scalar_presenter(dumper, data): """Represents a scalar string as a literal block, via '|' syntax""" return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|") yaml.add_representer(LiteralBlockScalar, literal_block_scalar_presenter) def process_string_value(string_value): """Pretty-prints JSON or returns long strings as a LiteralBlockScalar""" if isinstance(string_value, bytes): return string_value try: json_data = json.loads(string_value) return LiteralBlockScalar(json.dumps(json_data, indent=2)) except (ValueError, TypeError): if len(string_value) > 80: return LiteralBlockScalar(string_value) return string_value def convert_body_to_literal(data): """Searches the data for body strings, attempting to pretty-print JSON""" if isinstance(data, dict): for key, value in data.items(): # Handle response body case (e.g., response.body.string) if key == "body" and isinstance(value, dict) and "string" in value: value["string"] = process_string_value(value["string"]) # Handle request body case (e.g., request.body) elif key == "body" and isinstance(value, str): data[key] = process_string_value(value) else: convert_body_to_literal(value) elif isinstance(data, list): for idx, choice in enumerate(data): data[idx] = convert_body_to_literal(choice) # botocore uses bytes types for string values in headers, making recordings # of them unreadable. It is fine to convert to strings where possible as vcr # will still match them correctly. elif isinstance(data, bytes): try: return data.decode("utf-8") except UnicodeDecodeError: pass return data class PrettyPrintJSONBody: """This makes request and response body recordings more readable.""" @staticmethod def serialize(cassette_dict): cassette_dict = convert_body_to_literal(cassette_dict) return yaml.dump(cassette_dict, default_flow_style=False, allow_unicode=True) @staticmethod def deserialize(cassette_string): return yaml.load(cassette_string, Loader=yaml.Loader) @pytest.fixture(scope="module", autouse=True) def fixture_vcr(vcr): vcr.register_serializer("yaml", PrettyPrintJSONBody) return vcr test_aiobotocore_instrumentation.py000066400000000000000000000425661527070072300433200ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint:disable=too-many-public-methods import asyncio import importlib.util import json import unittest from typing import Any from unittest.mock import Mock, call, patch import botocore.stub from botocore.exceptions import ClientError from opentelemetry import trace as trace_api from opentelemetry.instrumentation.auto_instrumentation import ( _load_instrumentors, ) from opentelemetry.instrumentation.botocore import AiobotocoreInstrumentor from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.semconv._incubating.attributes.cloud_attributes import ( CLOUD_REGION, ) from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_STATUS_CODE, ) from opentelemetry.semconv._incubating.attributes.rpc_attributes import ( RPC_METHOD, RPC_SERVICE, RPC_SYSTEM, ) from opentelemetry.semconv.attributes.exception_attributes import ( EXCEPTION_MESSAGE, EXCEPTION_STACKTRACE, EXCEPTION_TYPE, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.test.test_base import TestBase _REQUEST_ID_REGEX_MATCH = r"[A-Za-z0-9]{52}" aiobotocore_installed = importlib.util.find_spec("aiobotocore") is not None @unittest.skipIf(not aiobotocore_installed, "aiobotocore is not installed") class TestAiobotocoreInstrumentor(TestBase): """Aiobotocore integration testsuite""" def setUp(self): # pylint: disable-next=import-outside-toplevel import aiobotocore.session # noqa: PLC0415 super().setUp() AiobotocoreInstrumentor().instrument() self.session = aiobotocore.session.get_session() self.session.set_credentials( access_key="access-key", secret_key="secret-key", ) self.region = "us-west-2" def tearDown(self): super().tearDown() AiobotocoreInstrumentor().uninstrument() def _default_span_attributes(self, service: str, operation: str): return { RPC_SYSTEM: "aws-api", RPC_SERVICE: service, RPC_METHOD: operation, CLOUD_REGION: self.region, "retry_attempts": 0, HTTP_STATUS_CODE: 200, SERVER_ADDRESS: f"{service.lower()}.{self.region}.amazonaws.com", SERVER_PORT: 443, } def assert_only_span(self): spans = self.memory_exporter.get_finished_spans() self.assertEqual(1, len(spans)) return spans[0] def assert_span( self, service: str, operation: str, request_id=None, attributes=None, ): span = self.assert_only_span() expected = self._default_span_attributes(service, operation) if attributes: expected.update(attributes) span_attributes_request_id = "aws.request_id" if request_id is _REQUEST_ID_REGEX_MATCH: actual_request_id = span.attributes[span_attributes_request_id] self.assertRegex(actual_request_id, _REQUEST_ID_REGEX_MATCH) expected[span_attributes_request_id] = actual_request_id elif request_id is not None: expected[span_attributes_request_id] = request_id self.assertSpanHasAttributes(span, expected) self.assertEqual(f"{service}.{operation}", span.name) return span def _make_client(self, service: str): return self.session.create_client(service, region_name=self.region) @staticmethod def _make_response_meta(request_id: str) -> dict[str, Any]: return { "RequestId": request_id, "RetryAttempts": 0, "HTTPStatusCode": 200, } def test_traced_client_ec2(self): """Test basic EC2 client tracing with stubbed response.""" request_id = "fdcdcab1-ae5c-489e-9c33-4637c5dda355" async def _test(): async with self._make_client("ec2") as client: with botocore.stub.Stubber(client) as stubber: stubber.add_response( "describe_instances", { "Reservations": [], "ResponseMetadata": self._make_response_meta(request_id), }, ) await client.describe_instances() asyncio.run(_test()) self.assert_span("EC2", "DescribeInstances", request_id=request_id) def test_traced_client_s3(self): """Test S3 client tracing with stubbed response.""" request_id = "s3-request-id-12345" async def _test(): async with self._make_client("s3") as client: with botocore.stub.Stubber(client) as stubber: stubber.add_response( "list_buckets", { "Buckets": [], "Owner": {"ID": "owner-id"}, "ResponseMetadata": self._make_response_meta(request_id), }, ) await client.list_buckets() asyncio.run(_test()) self.assert_span("S3", "ListBuckets", request_id=request_id) def test_no_op_tracer_provider(self): """Test that no spans are created when using NoOpTracerProvider.""" AiobotocoreInstrumentor().uninstrument() AiobotocoreInstrumentor().instrument(tracer_provider=trace_api.NoOpTracerProvider()) async def _test(): async with self._make_client("ec2") as client: with botocore.stub.Stubber(client) as stubber: stubber.add_response( "describe_instances", { "Reservations": [], "ResponseMetadata": self._make_response_meta("test-id"), }, ) await client.describe_instances() asyncio.run(_test()) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) def test_not_recording(self): """Test behavior when span is not recording.""" mock_tracer = Mock() mock_span = Mock() mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span async def _test(): with patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer async with self._make_client("ec2") as client: with botocore.stub.Stubber(client) as stubber: stubber.add_response( "describe_instances", { "Reservations": [], "ResponseMetadata": self._make_response_meta("test-id"), }, ) await client.describe_instances() self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) asyncio.run(_test()) def test_client_error(self): """Test that ClientError is properly traced with error status.""" async def _test(): async with self._make_client("s3") as client: with botocore.stub.Stubber(client) as stubber: stubber.add_client_error( "get_object", service_error_code="NoSuchKey", service_message="The specified key does not exist.", http_status_code=404, ) with self.assertRaises(ClientError): await client.get_object(Bucket="test-bucket", Key="test-key") asyncio.run(_test()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(1, len(spans)) span = spans[0] self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) self.assertEqual("S3.GetObject", span.name) # Verify exception event was recorded self.assertEqual(1, len(span.events)) event = span.events[0] self.assertIn(EXCEPTION_STACKTRACE, event.attributes) self.assertIn(EXCEPTION_TYPE, event.attributes) self.assertIn(EXCEPTION_MESSAGE, event.attributes) def test_client_error_with_http_error_status(self): """ClientError with HTTP 4xx sets span status to ERROR.""" async def _test(): async with self._make_client("s3") as client: with botocore.stub.Stubber(client) as stubber: stubber.add_client_error( "get_object", service_error_code="NoSuchBucket", service_message="The specified bucket does not exist.", http_status_code=404, ) with self.assertRaises(ClientError): await client.get_object(Bucket="non-existent-bucket", Key="key") asyncio.run(_test()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(1, len(spans)) span = spans[0] self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) def test_client_error_304_not_modified(self): """ClientError with HTTP 304 leaves span status as UNSET.""" async def _test(): async with self._make_client("s3") as client: with botocore.stub.Stubber(client) as stubber: stubber.add_client_error( "get_object", service_error_code="304", service_message="Not Modified", http_status_code=304, ) with self.assertRaises(ClientError): await client.get_object(Bucket="test-bucket", Key="key") asyncio.run(_test()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(1, len(spans)) span = spans[0] self.assertIs(span.status.status_code, trace_api.StatusCode.UNSET) def test_suppress_instrumentation(self): """Test that instrumentation can be suppressed.""" async def _test(): async with self._make_client("ec2") as client: with botocore.stub.Stubber(client) as stubber: stubber.add_response( "describe_instances", { "Reservations": [], "ResponseMetadata": self._make_response_meta("test-id"), }, ) stubber.add_response( "describe_instances", { "Reservations": [], "ResponseMetadata": self._make_response_meta("test-id-2"), }, ) with suppress_instrumentation(): await client.describe_instances() await client.describe_instances() asyncio.run(_test()) self.assertEqual(0, len(self.get_finished_spans())) def test_request_hook(self): """Test that request hook is called with correct parameters.""" request_hook_service_attribute_name = "request_hook.service_name" request_hook_operation_attribute_name = "request_hook.operation_name" request_hook_api_params_attribute_name = "request_hook.api_params" def request_hook(span, service_name, operation_name, api_params): hook_attributes = { request_hook_service_attribute_name: service_name, request_hook_operation_attribute_name: operation_name, request_hook_api_params_attribute_name: json.dumps(api_params), } span.set_attributes(hook_attributes) AiobotocoreInstrumentor().uninstrument() AiobotocoreInstrumentor().instrument(request_hook=request_hook) request_id = "hook-test-request-id" async def _test(): async with self._make_client("s3") as client: with botocore.stub.Stubber(client) as stubber: stubber.add_response( "list_objects_v2", { "Contents": [], "ResponseMetadata": self._make_response_meta(request_id), }, expected_params={"Bucket": "test-bucket"}, ) await client.list_objects_v2(Bucket="test-bucket") asyncio.run(_test()) self.assert_span( "S3", "ListObjectsV2", request_id=request_id, attributes={ request_hook_service_attribute_name: "s3", request_hook_operation_attribute_name: "ListObjectsV2", request_hook_api_params_attribute_name: json.dumps({"Bucket": "test-bucket"}), }, ) def test_response_hook(self): """Test that response hook is called with correct parameters.""" response_hook_service_attribute_name = "response_hook.service_name" response_hook_operation_attribute_name = "response_hook.operation_name" response_hook_bucket_count_attribute_name = "response_hook.bucket_count" def response_hook(span, service_name, operation_name, result): hook_attributes = { response_hook_service_attribute_name: service_name, response_hook_operation_attribute_name: operation_name, response_hook_bucket_count_attribute_name: len(result["Buckets"]), } span.set_attributes(hook_attributes) AiobotocoreInstrumentor().uninstrument() AiobotocoreInstrumentor().instrument(response_hook=response_hook) request_id = "response-hook-test-id" async def _test(): async with self._make_client("s3") as client: with botocore.stub.Stubber(client) as stubber: stubber.add_response( "list_buckets", { "Buckets": [ {"Name": "bucket1"}, {"Name": "bucket2"}, ], "Owner": {"ID": "owner-id"}, "ResponseMetadata": self._make_response_meta(request_id), }, ) await client.list_buckets() asyncio.run(_test()) self.assert_span( "S3", "ListBuckets", request_id=request_id, attributes={ response_hook_service_attribute_name: "s3", response_hook_operation_attribute_name: "ListBuckets", response_hook_bucket_count_attribute_name: 2, }, ) def test_multiple_operations(self): """Test tracing multiple sequential operations.""" async def _test(): async with self._make_client("s3") as client: with botocore.stub.Stubber(client) as stubber: stubber.add_response( "list_buckets", { "Buckets": [], "Owner": {"ID": "owner-id"}, "ResponseMetadata": self._make_response_meta("req-1"), }, ) stubber.add_response( "list_buckets", { "Buckets": [], "Owner": {"ID": "owner-id"}, "ResponseMetadata": self._make_response_meta("req-2"), }, ) await client.list_buckets() await client.list_buckets() asyncio.run(_test()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(2, len(spans)) for span in spans: self.assertEqual("S3.ListBuckets", span.name) self.assertEqual("req-1", spans[0].attributes["aws.request_id"]) self.assertEqual("req-2", spans[1].attributes["aws.request_id"]) @patch("opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts") @patch("opentelemetry.instrumentation.auto_instrumentation._load._logger") def test_instruments_with_aiobotocore_installed(self, mock_logger, mock_dep): mock_distro = Mock() mock_dep.return_value = None mock_distro.load_instrumentor.return_value = None _load_instrumentors(mock_distro) self.assertEqual(len(mock_distro.load_instrumentor.call_args_list), 2) eps = [c[0][0].name for c in mock_distro.load_instrumentor.call_args_list] assert "aiobotocore" in eps assert "botocore" in eps mock_logger.debug.assert_has_calls( [ call("Instrumented %s", "aiobotocore"), call("Instrumented %s", "botocore"), ], any_order=True, ) test_botocore_bedrock.py000066400000000000000000003010021527070072300407540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint:disable=too-many-lines from __future__ import annotations import io import json from unittest import mock import boto3 import pytest from botocore.eventstream import EventStream, EventStreamError from botocore.response import StreamingBody from opentelemetry.instrumentation.botocore.extensions.bedrock_utils import ( ConverseStreamWrapper, InvokeModelWithResponseStreamWrapper, _Choice, ) from opentelemetry.semconv._incubating.attributes.error_attributes import ( ERROR_TYPE, ) from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import ( GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, ) from opentelemetry.trace.status import StatusCode from .bedrock_utils import ( assert_completion_attributes_from_streaming_body, assert_converse_completion_attributes, assert_message_in_logs, assert_metrics, assert_stream_completion_attributes, ) BOTO3_VERSION = tuple(int(x) for x in boto3.__version__.split(".")) def filter_message_keys(message, keys): return {k: v for k, v in message.items() if k in keys} @pytest.mark.skipif(BOTO3_VERSION < (1, 35, 56), reason="Converse API not available") @pytest.mark.vcr() def test_converse_with_content( span_exporter, log_exporter, metric_reader, bedrock_runtime_client, instrument_with_content, ): # pylint:disable=too-many-locals messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] llm_model_value = "amazon.titan-text-lite-v1" max_tokens, temperature, top_p, stop_sequences = 10, 0.8, 1, ["|"] response = bedrock_runtime_client.converse( messages=messages, modelId=llm_model_value, inferenceConfig={ "maxTokens": max_tokens, "temperature": temperature, "topP": top_p, "stopSequences": stop_sequences, }, ) (span,) = span_exporter.get_finished_spans() assert_converse_completion_attributes( span, llm_model_value, response, "chat", top_p, temperature, max_tokens, stop_sequences, ) logs = log_exporter.get_finished_logs() assert len(logs) == 2 user_content = filter_message_keys(messages[0], ["content"]) assert_message_in_logs(logs[0], "gen_ai.user.message", user_content, span) choice_body = { "index": 0, "finish_reason": "max_tokens", "message": { "content": [{"text": "Hi, how can I help you"}], "role": "assistant", }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_body, span) input_tokens = response["usage"]["inputTokens"] output_tokens = response["usage"]["outputTokens"] metrics = metric_reader.get_metrics_data().resource_metrics assert_metrics(metrics, "chat", llm_model_value, input_tokens, output_tokens) @pytest.mark.skipif( BOTO3_VERSION < (1, 38, 0), reason="Prompt cache token usage not in the bedrock-runtime service model", ) @pytest.mark.vcr() def test_converse_with_prompt_caching( span_exporter, bedrock_runtime_client, instrument_no_content, ): # A prefix is only cached once it clears the model minimum (1k tokens for # Nova Micro), so pad the system prompt past it. system_content = [ {"text": "You are a helpful assistant for instrumentation tests. " * 300}, {"cachePoint": {"type": "default"}}, ] messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] llm_model_value = "us.amazon.nova-micro-v1:0" # The first call writes the prefix to the cache, the second reads it back. responses = [ bedrock_runtime_client.converse( system=system_content, messages=messages, modelId=llm_model_value, inferenceConfig={"maxTokens": 10}, ) for _ in range(2) ] write_span, read_span = span_exporter.get_finished_spans() cache_write = responses[0]["usage"]["cacheWriteInputTokens"] assert cache_write > 0 written = write_span.attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] assert written == cache_write assert isinstance(written, int) cache_read = responses[1]["usage"]["cacheReadInputTokens"] assert cache_read > 0 read = read_span.attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] assert read == cache_read assert isinstance(read, int) @pytest.mark.skipif( BOTO3_VERSION < (1, 38, 0), reason="Prompt cache token usage not in the bedrock-runtime service model", ) @pytest.mark.vcr() def test_converse_stream_with_prompt_caching( span_exporter, bedrock_runtime_client, instrument_no_content, ): # A prefix is only cached once it clears the model minimum (1k tokens for # Nova Micro), so pad the system prompt past it. system_content = [ {"text": "You are a helpful assistant for streaming tests. " * 300}, {"cachePoint": {"type": "default"}}, ] messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] llm_model_value = "us.amazon.nova-micro-v1:0" # The first call writes the prefix to the cache, the second reads it back. usages = [] for _ in range(2): response = bedrock_runtime_client.converse_stream( system=system_content, messages=messages, modelId=llm_model_value, inferenceConfig={"maxTokens": 10}, ) # consume the stream in order to have it traced for event in response["stream"]: if "metadata" in event: usages.append(event["metadata"]["usage"]) write_span, read_span = span_exporter.get_finished_spans() cache_write = usages[0]["cacheWriteInputTokens"] assert cache_write > 0 written = write_span.attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] assert written == cache_write assert isinstance(written, int) cache_read = usages[1]["cacheReadInputTokens"] assert cache_read > 0 read = read_span.attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] assert read == cache_read assert isinstance(read, int) @pytest.mark.skipif(BOTO3_VERSION < (1, 35, 56), reason="Converse API not available") @pytest.mark.vcr() def test_converse_with_content_different_events( span_exporter, log_exporter, metric_reader, bedrock_runtime_client, instrument_with_content, ): # pylint:disable=too-many-locals messages = anthropic_claude_converse_messages() llm_model_value = "anthropic.claude-v2" system_content = anthropic_claude_converse_system() response = bedrock_runtime_client.converse( system=system_content, messages=messages, modelId=llm_model_value, ) (span,) = span_exporter.get_finished_spans() assert_converse_completion_attributes( span, llm_model_value, response, "chat", ) logs = log_exporter.get_finished_logs() assert len(logs) == 5 assert_message_in_logs(logs[0], "gen_ai.system.message", {"content": system_content}, span) user_message, assistant_message, last_user_message = messages user_content = filter_message_keys(user_message, ["content"]) assert_message_in_logs(logs[1], "gen_ai.user.message", user_content, span) assistant_content = filter_message_keys(assistant_message, ["content"]) assert_message_in_logs(logs[2], "gen_ai.assistant.message", assistant_content, span) last_user_content = filter_message_keys(last_user_message, ["content"]) assert_message_in_logs(logs[3], "gen_ai.user.message", last_user_content, span) choice_body = { "index": 0, "finish_reason": "end_turn", "message": { "content": [{"text": "This is a test"}], "role": "assistant", }, } assert_message_in_logs(logs[4], "gen_ai.choice", choice_body, span) input_tokens = response["usage"]["inputTokens"] output_tokens = response["usage"]["outputTokens"] metrics = metric_reader.get_metrics_data().resource_metrics assert_metrics(metrics, "chat", llm_model_value, input_tokens, output_tokens) def converse_tool_call(span_exporter, log_exporter, bedrock_runtime_client, expect_content): # pylint:disable=too-many-locals messages = [ { "role": "user", "content": [{"text": "What is the weather in Seattle and San Francisco today?"}], } ] tool_config = get_tool_config() llm_model_value = "amazon.nova-micro-v1:0" response_0 = bedrock_runtime_client.converse( messages=messages, modelId=llm_model_value, toolConfig=tool_config, ) # first content block is model thinking, so skip it tool_requests_ids = [ request["toolUse"]["toolUseId"] for request in response_0["output"]["message"]["content"] if "toolUse" in request ] assert len(tool_requests_ids) == 2 tool_call_result = { "role": "user", "content": [ { "toolResult": { "content": [{"json": {"weather": "50 degrees and raining"}}], "toolUseId": tool_requests_ids[0], }, }, { "toolResult": { "content": [{"json": {"weather": "70 degrees and sunny"}}], "toolUseId": tool_requests_ids[1], }, }, ], } messages.append(response_0["output"]["message"]) messages.append(tool_call_result) response_1 = bedrock_runtime_client.converse( messages=messages, modelId=llm_model_value, toolConfig=tool_config, ) (span_0, span_1) = span_exporter.get_finished_spans() assert_converse_completion_attributes( span_0, llm_model_value, response_0, "chat", ) assert_converse_completion_attributes( span_1, llm_model_value, response_1, "chat", ) logs = log_exporter.get_finished_logs() assert len(logs) == 8 # first span if expect_content: user_content = filter_message_keys(messages[0], ["content"]) else: user_content = {} assert_message_in_logs(logs[0], "gen_ai.user.message", user_content, span_0) function_call_0 = {"name": "get_current_weather"} function_call_1 = {"name": "get_current_weather"} if expect_content: function_call_0["arguments"] = {"location": "Seattle"} function_call_1["arguments"] = {"location": "San Francisco"} choice_body = { "index": 0, "finish_reason": "tool_use", "message": { "role": "assistant", "tool_calls": [ { "id": tool_requests_ids[0], "type": "function", "function": function_call_0, }, { "id": tool_requests_ids[1], "type": "function", "function": function_call_1, }, ], }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_body, span_0) # second span assert_message_in_logs(logs[2], "gen_ai.user.message", user_content, span_1) assistant_body = response_0["output"]["message"] assistant_body["tool_calls"] = choice_body["message"]["tool_calls"] assistant_body.pop("role") if not expect_content: assistant_body.pop("content") assert_message_in_logs( logs[3], "gen_ai.assistant.message", assistant_body, span_1, ) tool_message_0 = { "id": tool_requests_ids[0], "content": tool_call_result["content"][0]["toolResult"]["content"] if expect_content else None, } assert_message_in_logs(logs[4], "gen_ai.tool.message", tool_message_0, span_1) tool_message_1 = { "id": tool_requests_ids[1], "content": tool_call_result["content"][1]["toolResult"]["content"] if expect_content else None, } assert_message_in_logs(logs[5], "gen_ai.tool.message", tool_message_1, span_1) user_message_body = tool_call_result user_message_body.pop("role") if not expect_content: user_message_body.pop("content") assert_message_in_logs(logs[6], "gen_ai.user.message", user_message_body, span_1) choice_body = { "index": 0, "finish_reason": "end_turn", "message": { "role": "assistant", "content": [ { "text": " I have received the weather information for both cities. Now I will compile this information and present it to the User.\n\nThe current weather in Seattle is 50 degrees and it's raining. In San Francisco, it's 70 degrees and sunny today." } ], }, } if not expect_content: choice_body["message"].pop("content") assert_message_in_logs(logs[7], "gen_ai.choice", choice_body, span_1) @pytest.mark.skipif(BOTO3_VERSION < (1, 35, 56), reason="Converse API not available") @pytest.mark.vcr() def test_converse_tool_call_with_content( span_exporter, log_exporter, bedrock_runtime_client, instrument_with_content, ): converse_tool_call( span_exporter, log_exporter, bedrock_runtime_client, expect_content=True, ) @pytest.mark.skipif(BOTO3_VERSION < (1, 35, 56), reason="Converse API not available") @pytest.mark.vcr() def test_converse_no_content_different_events( span_exporter, log_exporter, bedrock_runtime_client, instrument_no_content, ): messages = anthropic_claude_converse_messages() llm_model_value = "anthropic.claude-v2" system_content = anthropic_claude_converse_system() response = bedrock_runtime_client.converse( system=system_content, messages=messages, modelId=llm_model_value, ) (span,) = span_exporter.get_finished_spans() assert_converse_completion_attributes( span, llm_model_value, response, "chat", ) logs = log_exporter.get_finished_logs() assert len(logs) == 5 assert_message_in_logs(logs[0], "gen_ai.system.message", None, span) assert_message_in_logs(logs[1], "gen_ai.user.message", None, span) assert_message_in_logs(logs[2], "gen_ai.assistant.message", None, span) assert_message_in_logs(logs[3], "gen_ai.user.message", None, span) choice_body = { "index": 0, "finish_reason": "end_turn", "message": {"role": "assistant"}, } assert_message_in_logs(logs[4], "gen_ai.choice", choice_body, span) @pytest.mark.skipif(BOTO3_VERSION < (1, 35, 56), reason="Converse API not available") @pytest.mark.vcr() def test_converse_no_content( span_exporter, log_exporter, bedrock_runtime_client, instrument_no_content, ): messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] llm_model_value = "amazon.titan-text-lite-v1" max_tokens, temperature, top_p, stop_sequences = 10, 0.8, 1, ["|"] response = bedrock_runtime_client.converse( messages=messages, modelId=llm_model_value, inferenceConfig={ "maxTokens": max_tokens, "temperature": temperature, "topP": top_p, "stopSequences": stop_sequences, }, ) (span,) = span_exporter.get_finished_spans() assert_converse_completion_attributes( span, llm_model_value, response, "chat", top_p, temperature, max_tokens, stop_sequences, ) logs = log_exporter.get_finished_logs() assert len(logs) == 2 assert_message_in_logs(logs[0], "gen_ai.user.message", None, span) choice_body = { "index": 0, "finish_reason": "max_tokens", "message": {"role": "assistant"}, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_body, span) @pytest.mark.skipif(BOTO3_VERSION < (1, 35, 56), reason="Converse API not available") @pytest.mark.vcr() def test_converse_tool_call_no_content( span_exporter, log_exporter, bedrock_runtime_client, instrument_no_content, ): converse_tool_call( span_exporter, log_exporter, bedrock_runtime_client, expect_content=False, ) @pytest.mark.skipif(BOTO3_VERSION < (1, 35, 56), reason="Converse API not available") @pytest.mark.vcr() def test_converse_with_invalid_model( span_exporter, log_exporter, metric_reader, bedrock_runtime_client, instrument_with_content, ): messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] llm_model_value = "does-not-exist" with pytest.raises(bedrock_runtime_client.exceptions.ValidationException): bedrock_runtime_client.converse( messages=messages, modelId=llm_model_value, ) (span,) = span_exporter.get_finished_spans() assert_converse_completion_attributes( span, llm_model_value, None, "chat", ) assert span.status.status_code == StatusCode.ERROR assert span.attributes[ERROR_TYPE] == "ValidationException" logs = log_exporter.get_finished_logs() user_content = filter_message_keys(messages[0], ["content"]) assert_message_in_logs(logs[0], "gen_ai.user.message", user_content, span) metrics = metric_reader.get_metrics_data().resource_metrics assert_metrics(metrics, "chat", llm_model_value, error_type="ValidationException") @pytest.mark.skipif(BOTO3_VERSION < (1, 35, 56), reason="ConverseStream API not available") @pytest.mark.vcr() def test_converse_stream_with_content( span_exporter, log_exporter, metric_reader, bedrock_runtime_client, instrument_with_content, ): # pylint:disable=too-many-locals messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] llm_model_value = "amazon.titan-text-lite-v1" max_tokens, temperature, top_p, stop_sequences = 10, 0.8, 1, ["|"] response = bedrock_runtime_client.converse_stream( messages=messages, modelId=llm_model_value, inferenceConfig={ "maxTokens": max_tokens, "temperature": temperature, "topP": top_p, "stopSequences": stop_sequences, }, ) # consume the stream in order to have it traced finish_reason = None input_tokens, output_tokens = None, None text = "" for event in response["stream"]: if "contentBlockDelta" in event: text += event["contentBlockDelta"]["delta"]["text"] if "messageStop" in event: finish_reason = (event["messageStop"]["stopReason"],) if "metadata" in event: usage = event["metadata"]["usage"] input_tokens = usage["inputTokens"] output_tokens = usage["outputTokens"] assert text assert finish_reason assert input_tokens assert output_tokens (span,) = span_exporter.get_finished_spans() assert_stream_completion_attributes( span, llm_model_value, input_tokens, output_tokens, finish_reason, "chat", top_p, temperature, max_tokens, stop_sequences, ) logs = log_exporter.get_finished_logs() assert len(logs) == 2 user_content = filter_message_keys(messages[0], ["content"]) assert_message_in_logs(logs[0], "gen_ai.user.message", user_content, span) choice_body = { "index": 0, "finish_reason": "max_tokens", "message": { "content": [{"text": "I am here and ready to assist"}], "role": "assistant", }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_body, span) metrics = metric_reader.get_metrics_data().resource_metrics assert_metrics(metrics, "chat", llm_model_value, input_tokens, output_tokens) @pytest.mark.skipif(BOTO3_VERSION < (1, 35, 56), reason="ConverseStream API not available") @pytest.mark.vcr() def test_converse_stream_with_content_different_events( span_exporter, log_exporter, metric_reader, bedrock_runtime_client, instrument_with_content, ): # pylint:disable=too-many-locals messages = anthropic_claude_converse_messages() llm_model_value = "anthropic.claude-v2" system_content = anthropic_claude_converse_system() response = bedrock_runtime_client.converse_stream( system=system_content, messages=messages, modelId=llm_model_value, ) # consume the stream in order to have it traced for _ in response["stream"]: pass (span,) = span_exporter.get_finished_spans() assert_stream_completion_attributes( span, llm_model_value, input_tokens=mock.ANY, output_tokens=mock.ANY, finish_reason=("end_turn",), operation_name="chat", ) logs = log_exporter.get_finished_logs() assert len(logs) == 5 assert_message_in_logs(logs[0], "gen_ai.system.message", {"content": system_content}, span) user_message, assistant_message, last_user_message = messages user_content = filter_message_keys(user_message, ["content"]) assert_message_in_logs(logs[1], "gen_ai.user.message", user_content, span) assistant_content = filter_message_keys(assistant_message, ["content"]) assert_message_in_logs(logs[2], "gen_ai.assistant.message", assistant_content, span) last_user_content = filter_message_keys(last_user_message, ["content"]) assert_message_in_logs(logs[3], "gen_ai.user.message", last_user_content, span) choice_body = { "index": 0, "finish_reason": "end_turn", "message": { "role": "assistant", "content": [{"text": "This is a test"}], }, } assert_message_in_logs(logs[4], "gen_ai.choice", choice_body, span) metrics = metric_reader.get_metrics_data().resource_metrics assert_metrics(metrics, "chat", llm_model_value, mock.ANY, mock.ANY) def _rebuild_stream_message(response): message = {"content": []} content_block = {} for stream_msg in response["stream"]: if "messageStart" in stream_msg: message.update(stream_msg["messageStart"]) elif "contentBlockStart" in stream_msg: start = stream_msg["contentBlockStart"]["start"] if "toolUse" in start: # toolUse.input is already decoded by the wrapper content_block = {"toolUse": start["toolUse"]} elif "contentBlockDelta" in stream_msg: delta = stream_msg["contentBlockDelta"]["delta"] if "text" in delta: content_block.setdefault("text", "") content_block["text"] += delta["text"] elif "toolUse" in delta: # toolUse.input is already decoded by the wrapper content_block["toolUse"].update(delta["toolUse"]) elif "contentBlockStop" in stream_msg: message["content"].append(content_block) elif "messageStop" in stream_msg: message.update(stream_msg["messageStop"]) return message def converse_stream_tool_call(span_exporter, log_exporter, bedrock_runtime_client, expect_content): # pylint:disable=too-many-locals,too-many-statements messages = [ { "role": "user", "content": [{"text": "What is the weather in Seattle and San Francisco today?"}], } ] tool_config = get_tool_config() llm_model_value = "amazon.nova-micro-v1:0" response_0 = bedrock_runtime_client.converse_stream( messages=messages, modelId=llm_model_value, toolConfig=tool_config, ) # consume the stream and assemble it as the non-streaming version response_0_message = _rebuild_stream_message(response_0) tool_requests_ids = [ request["toolUse"]["toolUseId"] for request in response_0_message["content"] if "toolUse" in request ] assert len(tool_requests_ids) == 2 tool_call_result = { "role": "user", "content": [ { "toolResult": { "content": [{"json": {"weather": "50 degrees and raining"}}], "toolUseId": tool_requests_ids[0], }, }, { "toolResult": { "content": [{"json": {"weather": "70 degrees and sunny"}}], "toolUseId": tool_requests_ids[1], }, }, ], } response_0_message.pop("stopReason") messages.append(response_0_message) messages.append(tool_call_result) response_1 = bedrock_runtime_client.converse_stream( messages=messages, modelId=llm_model_value, toolConfig=tool_config, ) # consume the stream to have it traced _ = _rebuild_stream_message(response_1) (span_0, span_1) = span_exporter.get_finished_spans() assert_stream_completion_attributes( span_0, llm_model_value, input_tokens=mock.ANY, output_tokens=mock.ANY, finish_reason=("tool_use",), operation_name="chat", ) assert_stream_completion_attributes( span_1, llm_model_value, input_tokens=mock.ANY, output_tokens=mock.ANY, finish_reason=("end_turn",), operation_name="chat", ) logs = log_exporter.get_finished_logs() assert len(logs) == 8 # first span if expect_content: user_content = filter_message_keys(messages[0], ["content"]) else: user_content = {} assert_message_in_logs(logs[0], "gen_ai.user.message", user_content, span_0) function_call_0 = {"name": "get_current_weather"} function_call_1 = {"name": "get_current_weather"} if expect_content: function_call_0["arguments"] = {"location": "Seattle"} function_call_1["arguments"] = {"location": "San Francisco"} choice_body = { "index": 0, "finish_reason": "tool_use", "message": { "role": "assistant", "tool_calls": [ { "id": tool_requests_ids[0], "type": "function", "function": function_call_0, }, { "id": tool_requests_ids[1], "type": "function", "function": function_call_1, }, ], }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_body, span_0) # second span assert_message_in_logs(logs[2], "gen_ai.user.message", user_content, span_1) assistant_body = response_0_message assistant_body["tool_calls"] = choice_body["message"]["tool_calls"] assistant_body.pop("role") if not expect_content: assistant_body.pop("content") assert_message_in_logs( logs[3], "gen_ai.assistant.message", assistant_body, span_1, ) tool_message_0 = { "id": tool_requests_ids[0], "content": tool_call_result["content"][0]["toolResult"]["content"] if expect_content else None, } assert_message_in_logs(logs[4], "gen_ai.tool.message", tool_message_0, span_1) tool_message_1 = { "id": tool_requests_ids[1], "content": tool_call_result["content"][1]["toolResult"]["content"] if expect_content else None, } assert_message_in_logs(logs[5], "gen_ai.tool.message", tool_message_1, span_1) user_message_body = tool_call_result user_message_body.pop("role") if not expect_content: user_message_body.pop("content") assert_message_in_logs(logs[6], "gen_ai.user.message", user_message_body, span_1) choice_body = { "index": 0, "finish_reason": "end_turn", "message": { "role": "assistant", "content": [ { "text": " I have received the weather information for both cities. Now I will provide the details to the User.\n\nThe current weather in Seattle is 50 degrees and raining. In San Francisco, the weather is 70 degrees and sunny." } ], }, } if not expect_content: choice_body["message"].pop("content") assert_message_in_logs(logs[7], "gen_ai.choice", choice_body, span_1) @pytest.mark.skipif(BOTO3_VERSION < (1, 35, 56), reason="ConverseStream API not available") @pytest.mark.vcr() def test_converse_stream_with_content_tool_call( span_exporter, log_exporter, bedrock_runtime_client, instrument_with_content, ): converse_stream_tool_call( span_exporter, log_exporter, bedrock_runtime_client, expect_content=True, ) @pytest.mark.skipif(BOTO3_VERSION < (1, 35, 56), reason="ConverseStream API not available") @pytest.mark.vcr() def test_converse_stream_tool_call_parsing_errors( span_exporter, log_exporter, bedrock_runtime_client, instrument_with_content, ): # pylint:disable=too-many-locals messages = [ { "role": "user", "content": [ { "text": "Use the get_cities_list tool to provide exactly 10 popular tourist cities in Japan. Call the tool with a cities array containing: Tokyo, Osaka, Kyoto, Hiroshima, Nara, Yokohama, Sapporo, Fukuoka, Sendai, and Nagoya" } ], } ] tool_config = { "tools": [ { "toolSpec": { "name": "get_cities_list", "description": "Get a list of cities", "inputSchema": { "json": { "type": "object", "properties": { "cities": { "type": "array", "items": {"type": "string"}, } }, } }, } } ] } llm_model_value = "anthropic.claude-3-sonnet-20240229-v1:0" response_0 = bedrock_runtime_client.converse_stream( messages=messages, modelId=llm_model_value, toolConfig=tool_config ) res = "" tool_use_id = None for chunk in response_0["stream"]: if "contentBlockStart" in chunk: start = chunk["contentBlockStart"]["start"] tool_use_id = start["toolUse"]["toolUseId"] elif "contentBlockDelta" in chunk: delta = chunk["contentBlockDelta"]["delta"] if "toolUse" in delta: res += delta["toolUse"].get("input") tool_use_input = json.loads(res) expected_tool_use_input = { "cities": [ "Tokyo", "Osaka", "Kyoto", "Hiroshima", "Nara", "Yokohama", "Sapporo", "Fukuoka", "Sendai", "Nagoya", ] } assert tool_use_input == expected_tool_use_input (span_0,) = span_exporter.get_finished_spans() assert_stream_completion_attributes( span_0, llm_model_value, input_tokens=mock.ANY, output_tokens=mock.ANY, finish_reason=("tool_use",), operation_name="chat", ) logs = log_exporter.get_finished_logs() assert len(logs) == 2 user_content = filter_message_keys(messages[0], ["content"]) assert_message_in_logs(logs[0], "gen_ai.user.message", user_content, span_0) function_call_0 = { "name": "get_cities_list", "arguments": expected_tool_use_input, } choice_body = { "index": 0, "finish_reason": "tool_use", "message": { "role": "assistant", "tool_calls": [ { "id": tool_use_id, "type": "function", "function": function_call_0, }, ], }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_body, span_0) @pytest.mark.skipif(BOTO3_VERSION < (1, 35, 56), reason="ConverseStream API not available") @pytest.mark.vcr() def test_converse_stream_no_content( span_exporter, log_exporter, bedrock_runtime_client, instrument_no_content, ): # pylint:disable=too-many-locals messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] llm_model_value = "amazon.titan-text-lite-v1" max_tokens, temperature, top_p, stop_sequences = 10, 0.8, 1, ["|"] response = bedrock_runtime_client.converse_stream( messages=messages, modelId=llm_model_value, inferenceConfig={ "maxTokens": max_tokens, "temperature": temperature, "topP": top_p, "stopSequences": stop_sequences, }, ) # consume the stream in order to have it traced finish_reason = None input_tokens, output_tokens = None, None text = "" for event in response["stream"]: if "contentBlockDelta" in event: text += event["contentBlockDelta"]["delta"]["text"] if "messageStop" in event: finish_reason = event["messageStop"]["stopReason"] if "metadata" in event: usage = event["metadata"]["usage"] input_tokens = usage["inputTokens"] output_tokens = usage["outputTokens"] assert text assert finish_reason assert input_tokens assert output_tokens (span,) = span_exporter.get_finished_spans() assert_stream_completion_attributes( span, llm_model_value, input_tokens, output_tokens, (finish_reason,), "chat", top_p, temperature, max_tokens, stop_sequences, ) logs = log_exporter.get_finished_logs() assert len(logs) == 2 assert_message_in_logs(logs[0], "gen_ai.user.message", None, span) choice_body = { "index": 0, "finish_reason": finish_reason, "message": {"role": "assistant"}, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_body, span) @pytest.mark.skipif(BOTO3_VERSION < (1, 35, 56), reason="ConverseStream API not available") @pytest.mark.vcr() def test_converse_stream_no_content_different_events( span_exporter, log_exporter, bedrock_runtime_client, instrument_no_content, ): messages = anthropic_claude_converse_messages() llm_model_value = "anthropic.claude-v2" system_content = anthropic_claude_converse_system() response = bedrock_runtime_client.converse_stream( system=system_content, messages=messages, modelId=llm_model_value, ) # consume the stream in order to have it traced for _ in response["stream"]: pass (span,) = span_exporter.get_finished_spans() assert_stream_completion_attributes( span, llm_model_value, input_tokens=mock.ANY, output_tokens=mock.ANY, finish_reason=("end_turn",), operation_name="chat", ) logs = log_exporter.get_finished_logs() assert len(logs) == 5 assert_message_in_logs(logs[0], "gen_ai.system.message", None, span) assert_message_in_logs(logs[1], "gen_ai.user.message", None, span) assert_message_in_logs(logs[2], "gen_ai.assistant.message", None, span) assert_message_in_logs(logs[3], "gen_ai.user.message", None, span) choice_body = { "index": 0, "finish_reason": "end_turn", "message": {"role": "assistant"}, } assert_message_in_logs(logs[4], "gen_ai.choice", choice_body, span) @pytest.mark.skipif(BOTO3_VERSION < (1, 35, 56), reason="ConverseStream API not available") @pytest.mark.vcr() def test_converse_stream_no_content_tool_call( span_exporter, log_exporter, bedrock_runtime_client, instrument_no_content, ): converse_stream_tool_call( span_exporter, log_exporter, bedrock_runtime_client, expect_content=False, ) @pytest.mark.skipif(BOTO3_VERSION < (1, 35, 56), reason="ConverseStream API not available") @pytest.mark.vcr() def test_converse_stream_handles_event_stream_error( span_exporter, log_exporter, metric_reader, bedrock_runtime_client, instrument_with_content, ): # pylint:disable=too-many-locals messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] llm_model_value = "amazon.titan-text-lite-v1" max_tokens, temperature, top_p, stop_sequences = 10, 0.8, 1, ["|"] response = bedrock_runtime_client.converse_stream( messages=messages, modelId=llm_model_value, inferenceConfig={ "maxTokens": max_tokens, "temperature": temperature, "topP": top_p, "stopSequences": stop_sequences, }, ) with mock.patch.object( EventStream, "_parse_event", side_effect=EventStreamError({"modelStreamErrorException": {}}, "ConverseStream"), ): with pytest.raises(EventStreamError): for _event in response["stream"]: pass (span,) = span_exporter.get_finished_spans() input_tokens, output_tokens, finish_reason = None, None, None assert_stream_completion_attributes( span, llm_model_value, input_tokens, output_tokens, finish_reason, "chat", top_p, temperature, max_tokens, stop_sequences, ) assert span.status.status_code == StatusCode.ERROR assert span.attributes[ERROR_TYPE] == "EventStreamError" logs = log_exporter.get_finished_logs() assert len(logs) == 1 user_content = filter_message_keys(messages[0], ["content"]) assert_message_in_logs(logs[0], "gen_ai.user.message", user_content, span) metrics = metric_reader.get_metrics_data().resource_metrics assert_metrics(metrics, "chat", llm_model_value, error_type="EventStreamError") @pytest.mark.skipif(BOTO3_VERSION < (1, 35, 56), reason="ConverseStream API not available") @pytest.mark.vcr() def test_converse_stream_with_invalid_model( span_exporter, log_exporter, bedrock_runtime_client, instrument_with_content, ): messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] llm_model_value = "does-not-exist" with pytest.raises(bedrock_runtime_client.exceptions.ValidationException): bedrock_runtime_client.converse_stream( messages=messages, modelId=llm_model_value, ) (span,) = span_exporter.get_finished_spans() assert_stream_completion_attributes( span, llm_model_value, operation_name="chat", ) assert span.status.status_code == StatusCode.ERROR assert span.attributes[ERROR_TYPE] == "ValidationException" logs = log_exporter.get_finished_logs() assert len(logs) == 1 user_content = filter_message_keys(messages[0], ["content"]) assert_message_in_logs(logs[0], "gen_ai.user.message", user_content, span) @pytest.mark.skipif(BOTO3_VERSION < (1, 35, 56), reason="ConverseStream API not available") @pytest.mark.vcr() def test_converse_stream_close_before_consumption( span_exporter, log_exporter, bedrock_runtime_client, instrument_with_content, ): messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] llm_model_value = "amazon.titan-text-lite-v1" max_tokens, temperature, top_p, stop_sequences = 10, 0.8, 1, ["|"] response = bedrock_runtime_client.converse_stream( messages=messages, modelId=llm_model_value, inferenceConfig={ "maxTokens": max_tokens, "temperature": temperature, "topP": top_p, "stopSequences": stop_sequences, }, ) # Close the stream without consuming it response["stream"].close() # Verify span is closed regardless of stream consumption (span,) = span_exporter.get_finished_spans() assert_stream_completion_attributes( span, llm_model_value, input_tokens=None, output_tokens=None, finish_reason=None, operation_name="chat", request_top_p=top_p, request_temperature=temperature, request_max_tokens=max_tokens, request_stop_sequences=stop_sequences, ) assert span.status.status_code == StatusCode.UNSET logs = log_exporter.get_finished_logs() assert len(logs) == 1 user_content = filter_message_keys(messages[0], ["content"]) assert_message_in_logs(logs[0], "gen_ai.user.message", user_content, span) def get_invoke_model_body( llm_model, max_tokens=None, temperature=None, top_p=None, stop_sequences=None, system=None, messages=None, tools=None, ): def set_if_not_none(config, key, value): if value is not None: config[key] = value prompt = "Say this is a test" if llm_model == "amazon.nova-micro-v1:0": config = {} set_if_not_none(config, "max_new_tokens", max_tokens) set_if_not_none(config, "temperature", temperature) set_if_not_none(config, "topP", top_p) set_if_not_none(config, "stopSequences", stop_sequences) body = { "messages": messages if messages else [{"role": "user", "content": [{"text": prompt}]}], "inferenceConfig": config, "schemaVersion": "messages-v1", } if system: body["system"] = system if tools: body["toolConfig"] = tools elif llm_model == "amazon.titan-text-lite-v1": config = {} set_if_not_none(config, "maxTokenCount", max_tokens) set_if_not_none(config, "temperature", temperature) set_if_not_none(config, "topP", top_p) set_if_not_none(config, "stopSequences", stop_sequences) body = {"inputText": prompt, "textGenerationConfig": config} elif "anthropic.claude" in llm_model: body = { "messages": messages if messages else [{"role": "user", "content": [{"text": prompt, "type": "text"}]}], "anthropic_version": "bedrock-2023-05-31", } if system: body["system"] = system # tools are available since 3+ if tools: body["tools"] = tools set_if_not_none(body, "max_tokens", max_tokens) set_if_not_none(body, "temperature", temperature) set_if_not_none(body, "top_p", top_p) set_if_not_none(body, "stop_sequences", stop_sequences) elif "cohere.command-r" in llm_model: body = { "message": prompt, "max_tokens": max_tokens, "temperature": temperature, "p": top_p, "stop_sequences": stop_sequences, } elif "cohere.command" in llm_model: body = { "prompt": prompt, "max_tokens": max_tokens, "temperature": temperature, "p": top_p, "stop_sequences": stop_sequences, } elif "meta.llama" in llm_model: body = { "prompt": prompt, "max_gen_len": max_tokens, "temperature": temperature, "top_p": top_p, } elif "mistral.mistral" in llm_model: body = { "prompt": prompt, "max_tokens": max_tokens, "temperature": temperature, "top_p": top_p, "stop": stop_sequences, } else: raise ValueError(f"No config for {llm_model}") return json.dumps(body) def get_model_name_from_family(llm_model): llm_model_name = { "amazon.titan": "amazon.titan-text-lite-v1", "amazon.nova": "amazon.nova-micro-v1:0", "anthropic.claude": "anthropic.claude-v2", "cohere.command-r": "cohere.command-r-v1:0", "cohere.command": "cohere.command-light-text-v14", "meta.llama": "meta.llama3-1-70b-instruct-v1:0", "mistral.mistral": "mistral.mistral-7b-instruct-v0:2", } return llm_model_name[llm_model] @pytest.mark.parametrize( "model_family", [ "amazon.nova", "amazon.titan", "anthropic.claude", "cohere.command-r", "cohere.command", "meta.llama", "mistral.mistral", ], ) @pytest.mark.vcr() def test_invoke_model_with_content( span_exporter, log_exporter, bedrock_runtime_client, instrument_with_content, model_family, ): # pylint:disable=too-many-locals llm_model_value = get_model_name_from_family(model_family) max_tokens, temperature, top_p, stop_sequences = ( 10, 0.8, 0.99 if model_family == "cohere.command-r" else 1, ["|"], ) body = get_invoke_model_body(llm_model_value, max_tokens, temperature, top_p, stop_sequences) response = bedrock_runtime_client.invoke_model( body=body, modelId=llm_model_value, ) (span,) = span_exporter.get_finished_spans() assert_completion_attributes_from_streaming_body( span, llm_model_value, response, "text_completion" if model_family == "amazon.titan" else "chat", top_p, temperature, max_tokens, None if model_family == "meta.llama" else stop_sequences, ) logs = log_exporter.get_finished_logs() assert len(logs) == 2 if model_family == "anthropic.claude": user_content = {"content": [{"text": "Say this is a test", "type": "text"}]} else: user_content = {"content": [{"text": "Say this is a test"}]} if model_family == "amazon.titan": message = {"content": " comment\nHello! I am writing this as a"} finish_reason = "LENGTH" elif model_family == "amazon.nova": message = { "role": "assistant", "content": [{"text": "Certainly, here's a test:\n\n---\n\n**"}], } finish_reason = "max_tokens" elif model_family == "anthropic.claude": message = { "role": "assistant", "content": [{"type": "text", "text": 'Okay, I said "This is a test"'}], } finish_reason = "max_tokens" elif model_family == "cohere.command-r": message = { "content": "This is a test. How are you doing today", } finish_reason = "MAX_TOKENS" elif model_family == "cohere.command": message = { "content": " Let it be a test of knowledge, skills,", } finish_reason = "MAX_TOKENS" elif model_family == "meta.llama": message = { "content": " post. This is a test post. This is", } finish_reason = "length" elif model_family == "mistral.mistral": message = { "content": "\n\nA man stands before a crowd of people", } finish_reason = "length" else: pytest.xfail("model family not handled: {model_family}") assert_message_in_logs(logs[0], "gen_ai.user.message", user_content, span) choice_body = { "index": 0, "finish_reason": finish_reason, "message": message, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_body, span) @pytest.mark.vcr() def test_invoke_model_with_content_user_content_as_string( span_exporter, log_exporter, bedrock_runtime_client, instrument_with_content, ): llm_model_value = "us.anthropic.claude-3-5-sonnet-20240620-v1:0" max_tokens = 10 body = json.dumps( { "messages": [{"role": "user", "content": "say this is a test"}], "max_tokens": max_tokens, "anthropic_version": "bedrock-2023-05-31", } ) response = bedrock_runtime_client.invoke_model( body=body, modelId=llm_model_value, ) (span,) = span_exporter.get_finished_spans() assert_completion_attributes_from_streaming_body( span, llm_model_value, response, "chat", request_max_tokens=max_tokens, ) logs = log_exporter.get_finished_logs() assert len(logs) == 2 user_content = {"content": "say this is a test"} assert_message_in_logs(logs[0], "gen_ai.user.message", user_content, span) message = { "role": "assistant", "content": [{"type": "text", "text": "This is a test."}], } choice_body = { "index": 0, "finish_reason": "end_turn", "message": message, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_body, span) @pytest.mark.vcr() def test_invoke_model_with_content_assistant_content_as_string( span_exporter, log_exporter, bedrock_runtime_client, instrument_with_content, ): llm_model_value = "us.anthropic.claude-3-5-haiku-20241022-v1:0" max_tokens = 10 body = json.dumps( { "messages": [ {"role": "user", "content": "say this is a test"}, {"role": "assistant", "content": "{"}, ], "max_tokens": max_tokens, "anthropic_version": "bedrock-2023-05-31", } ) response = bedrock_runtime_client.invoke_model( body=body, modelId=llm_model_value, ) (span,) = span_exporter.get_finished_spans() assert_completion_attributes_from_streaming_body( span, llm_model_value, response, "chat", request_max_tokens=max_tokens, ) logs = log_exporter.get_finished_logs() assert len(logs) == 3 user_content = {"content": "say this is a test"} assert_message_in_logs(logs[0], "gen_ai.user.message", user_content, span) assistant_content = {"content": "{"} assert_message_in_logs(logs[1], "gen_ai.assistant.message", assistant_content, span) assistant_response_message = { "role": "assistant", "content": [{"type": "text", "text": "this is a test}"}], } choice_body = { "index": 0, "finish_reason": "end_turn", "message": assistant_response_message, } assert_message_in_logs(logs[2], "gen_ai.choice", choice_body, span) @pytest.mark.parametrize( "model_family", ["amazon.nova", "anthropic.claude"], ) @pytest.mark.vcr() def test_invoke_model_with_content_different_events( span_exporter, log_exporter, bedrock_runtime_client, instrument_with_content, model_family, ): # pylint:disable=too-many-locals llm_model_value = get_model_name_from_family(model_family) max_tokens = 10 if llm_model_value == "amazon.nova-micro-v1:0": messages = amazon_nova_messages() system = amazon_nova_system() finish_reason = "max_tokens" choice_content = [{"text": "Again, this is a test. If you need"}] elif llm_model_value == "anthropic.claude-v2": messages = anthropic_claude_messages() system = anthropic_claude_system() finish_reason = "end_turn" choice_content = [{"type": "text", "text": "This is a test"}] else: pytest.xfail("llm_model_value not handled: {llm_model_value}") body = get_invoke_model_body( llm_model_value, system=system, messages=messages, max_tokens=max_tokens, ) response = bedrock_runtime_client.invoke_model( body=body, modelId=llm_model_value, ) (span,) = span_exporter.get_finished_spans() assert_completion_attributes_from_streaming_body( span, llm_model_value, response, "chat", request_max_tokens=max_tokens, ) logs = log_exporter.get_finished_logs() assert len(logs) == 5 assert_message_in_logs( logs[0], "gen_ai.system.message", {"content": [{"text": "You are a friendly model"}]}, span, ) user_message, assistant_message, last_user_message = messages user_content = filter_message_keys(user_message, ["content"]) assert_message_in_logs(logs[1], "gen_ai.user.message", user_content, span) assistant_content = filter_message_keys(assistant_message, ["content"]) assert_message_in_logs(logs[2], "gen_ai.assistant.message", assistant_content, span) last_user_content = filter_message_keys(last_user_message, ["content"]) assert_message_in_logs(logs[3], "gen_ai.user.message", last_user_content, span) choice_body = { "index": 0, "finish_reason": finish_reason, "message": {"role": "assistant", "content": choice_content}, } assert_message_in_logs(logs[4], "gen_ai.choice", choice_body, span) class AnthropicClaudeModel: @staticmethod def user_message(prompt: str): return { "text": prompt, "type": "text", } @staticmethod def tool_config(): return get_anthropic_tool_config() @staticmethod def assistant_message(response): return {"role": response["role"], "content": response["content"]} @staticmethod def tool_requests_ids(response): return [content["id"] for content in response["content"] if content["type"] == "tool_use"] @staticmethod def tool_requests_ids_from_stream(stream_content): return [item["id"] for item in stream_content if item["type"] == "tool_use"] @staticmethod def tool_calls_results(tool_requests_ids): assert len(tool_requests_ids) == 2 return { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_requests_ids[0], "content": "50 degrees and raining", }, { "type": "tool_result", "tool_use_id": tool_requests_ids[1], "content": "70 degrees and sunny", }, ], } @staticmethod def tool_messages(tool_requests_ids, tool_call_result, expect_content): tool_message_0 = { "id": tool_requests_ids[0], "content": tool_call_result["content"][0]["content"] if expect_content else None, } tool_message_1 = { "id": tool_requests_ids[1], "content": tool_call_result["content"][1]["content"] if expect_content else None, } return tool_message_0, tool_message_1 @staticmethod def choice_content(response): return response["content"] @staticmethod def get_stream_body_content(body): content = [] content_block = {} input_json_buf = "" for event in body: json_bytes = event["chunk"].get("bytes", b"") decoded = json_bytes.decode("utf-8") chunk = json.loads(decoded) if (message_type := chunk.get("type")) is not None: if message_type == "content_block_start": content_block = chunk["content_block"] elif message_type == "content_block_delta": if chunk["delta"]["type"] == "text_delta": content_block["text"] += chunk["delta"]["text"] elif chunk["delta"]["type"] == "input_json_delta": input_json_buf += chunk["delta"]["partial_json"] elif message_type == "content_block_stop": if input_json_buf: content_block["input"] = json.loads(input_json_buf) content.append(content_block) content_block = None input_json_buf = "" return content class AmazonNovaModel: @staticmethod def user_message(prompt: str): return { "text": prompt, } @staticmethod def assistant_message(response): return response["output"]["message"] @staticmethod def tool_config(): return get_tool_config() @staticmethod def tool_requests_ids(response): return [ content["toolUse"]["toolUseId"] for content in response["output"]["message"]["content"] if "toolUse" in content ] @staticmethod def tool_requests_ids_from_stream(stream_content): return [item["toolUse"]["toolUseId"] for item in stream_content if "toolUse" in item] @staticmethod def tool_calls_results(tool_requests_ids): assert len(tool_requests_ids) == 2 return { "role": "user", "content": [ { "toolResult": { "toolUseId": tool_requests_ids[0], "content": [{"json": {"weather": "50 degrees and raining"}}], } }, { "toolResult": { "toolUseId": tool_requests_ids[1], "content": [{"json": {"weather": "70 degrees and sunny"}}], } }, ], } @staticmethod def tool_messages(tool_requests_ids, tool_call_result, expect_content): tool_message_0 = { "id": tool_requests_ids[0], "content": tool_call_result["content"][0]["toolResult"]["content"] if expect_content else None, } tool_message_1 = { "id": tool_requests_ids[1], "content": tool_call_result["content"][1]["toolResult"]["content"] if expect_content else None, } return tool_message_0, tool_message_1 @staticmethod def choice_content(response): return response["output"]["message"]["content"] @staticmethod def get_stream_body_content(body): content = [] content_block = {} tool_use = {} for event in body: json_bytes = event["chunk"].get("bytes", b"") decoded = json_bytes.decode("utf-8") chunk = json.loads(decoded) if "contentBlockDelta" in chunk: delta = chunk["contentBlockDelta"]["delta"] if "text" in delta: content_block.setdefault("text", "") content_block["text"] += delta["text"] elif "toolUse" in delta: tool_use["toolUse"]["input"] = json.loads(delta["toolUse"]["input"]) elif "contentBlockStart" in chunk: if content_block: content.append(content_block) content_block = {} start = chunk["contentBlockStart"]["start"] if "toolUse" in start: tool_use = start elif "contentBlockStop" in chunk: if tool_use: content.append(tool_use) tool_use = {} elif "messageStop" in chunk: if content_block: content.append(content_block) content_block = {} return content def invoke_model_tool_call( span_exporter, log_exporter, bedrock_runtime_client, llm_model_value, llm_model_config, expect_content, ): # pylint:disable=too-many-locals,too-many-statements user_prompt = "What is the weather in Seattle and San Francisco today? Please expect one tool call for Seattle and one for San Francisco" user_msg = llm_model_config.user_message(user_prompt) messages = [{"role": "user", "content": [user_msg]}] max_tokens = 1000 tool_config = llm_model_config.tool_config() body = get_invoke_model_body( llm_model_value, messages=messages, tools=tool_config, max_tokens=max_tokens, ) response_0 = bedrock_runtime_client.invoke_model( body=body, modelId=llm_model_value, ) response_0_raw_body = response_0["body"].read() response_0_body = json.loads(response_0_raw_body) # replenish body for span assertions new_stream = io.BytesIO(response_0_raw_body) response_0["body"] = StreamingBody(new_stream, len(response_0_raw_body)) tool_requests_ids = llm_model_config.tool_requests_ids(response_0_body) assert len(tool_requests_ids) == 2 assistant_message = llm_model_config.assistant_message(response_0_body) tool_call_result = llm_model_config.tool_calls_results(tool_requests_ids) messages.append(assistant_message) messages.append(tool_call_result) body = get_invoke_model_body( llm_model_value, messages=messages, max_tokens=max_tokens, tools=tool_config, ) response_1 = bedrock_runtime_client.invoke_model( body=body, modelId=llm_model_value, ) response_1_raw_body = response_1["body"].read() response_1_body = json.loads(response_1_raw_body) # replenish body for span assertions new_stream = io.BytesIO(response_1_raw_body) response_1["body"] = StreamingBody(new_stream, len(response_1_raw_body)) (span_0, span_1) = span_exporter.get_finished_spans() assert_completion_attributes_from_streaming_body( span_0, llm_model_value, response_0, "chat", request_max_tokens=max_tokens, ) assert_completion_attributes_from_streaming_body( span_1, llm_model_value, response_1, "chat", request_max_tokens=max_tokens, ) logs = log_exporter.get_finished_logs() assert len(logs) == 8 # first span if expect_content: user_content = filter_message_keys(messages[0], ["content"]) else: user_content = {} assert_message_in_logs(logs[0], "gen_ai.user.message", user_content, span_0) function_call_0 = {"name": "get_current_weather"} function_call_1 = {"name": "get_current_weather"} if expect_content: function_call_0["arguments"] = {"location": "Seattle"} function_call_1["arguments"] = {"location": "San Francisco"} choice_body = { "index": 0, "finish_reason": "tool_use", "message": { "role": "assistant", "tool_calls": [ { "id": tool_requests_ids[0], "type": "function", "function": function_call_0, }, { "id": tool_requests_ids[1], "type": "function", "function": function_call_1, }, ], }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_body, span_0) # second span assert_message_in_logs(logs[2], "gen_ai.user.message", user_content, span_1) assistant_body = assistant_message assistant_body["tool_calls"] = choice_body["message"]["tool_calls"] assistant_body.pop("role") if not expect_content: assistant_body.pop("content") assert_message_in_logs( logs[3], "gen_ai.assistant.message", assistant_body, span_1, ) tool_message_0, tool_message_1 = llm_model_config.tool_messages(tool_requests_ids, tool_call_result, expect_content) assert_message_in_logs(logs[4], "gen_ai.tool.message", tool_message_0, span_1) assert_message_in_logs(logs[5], "gen_ai.tool.message", tool_message_1, span_1) user_message_body = tool_call_result user_message_body.pop("role") if not expect_content: user_message_body.pop("content") assert_message_in_logs(logs[6], "gen_ai.user.message", user_message_body, span_1) choice_content = llm_model_config.choice_content(response_1_body) choice_body = { "index": 0, "finish_reason": "end_turn", "message": { "role": "assistant", "content": choice_content, }, } if not expect_content: choice_body["message"].pop("content") assert_message_in_logs(logs[7], "gen_ai.choice", choice_body, span_1) @pytest.mark.parametrize( "model_family", ["amazon.nova", "anthropic.claude"], ) @pytest.mark.vcr() def test_invoke_model_with_content_tool_call( span_exporter, log_exporter, bedrock_runtime_client, instrument_with_content, model_family, ): if model_family == "amazon.nova": llm_model_value = "amazon.nova-micro-v1:0" llm_model_config = AmazonNovaModel elif model_family == "anthropic.claude": llm_model_value = "us.anthropic.claude-3-5-sonnet-20240620-v1:0" llm_model_config = AnthropicClaudeModel else: pytest.xfail("model family not handled: {model_family}") invoke_model_tool_call( span_exporter, log_exporter, bedrock_runtime_client, llm_model_value, llm_model_config, expect_content=True, ) @pytest.mark.parametrize( "model_family", [ "amazon.nova", "amazon.titan", "anthropic.claude", "cohere.command-r", "cohere.command", "meta.llama", "mistral.mistral", ], ) @pytest.mark.vcr() def test_invoke_model_no_content( span_exporter, log_exporter, bedrock_runtime_client, instrument_no_content, model_family, ): # pylint:disable=too-many-locals llm_model_value = get_model_name_from_family(model_family) max_tokens, temperature, top_p, stop_sequences = ( 10, 0.8, 0.99 if model_family == "cohere.command-r" else 1, ["|"], ) body = get_invoke_model_body(llm_model_value, max_tokens, temperature, top_p, stop_sequences) response = bedrock_runtime_client.invoke_model( body=body, modelId=llm_model_value, ) (span,) = span_exporter.get_finished_spans() assert_completion_attributes_from_streaming_body( span, llm_model_value, response, "text_completion" if model_family == "amazon.titan" else "chat", top_p, temperature, max_tokens, None if model_family == "meta.llama" else stop_sequences, ) logs = log_exporter.get_finished_logs() assert len(logs) == 2 assert_message_in_logs(logs[0], "gen_ai.user.message", None, span) if model_family in ("anthropic.claude", "amazon.nova"): choice_message = {"role": "assistant"} finish_reason = "max_tokens" elif model_family == "amazon.titan": choice_message = {} finish_reason = "LENGTH" elif model_family == "cohere.command-r": choice_message = {} finish_reason = "STOP_SEQUENCE" elif model_family == "cohere.command": choice_message = {} finish_reason = "MAX_TOKENS" elif model_family in ("meta.llama", "mistral.mistral"): choice_message = {} finish_reason = "length" else: pytest.xfail("model family not handled: {model_family}") choice_body = { "index": 0, "finish_reason": finish_reason, "message": choice_message, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_body, span) @pytest.mark.parametrize( "model_family", ["amazon.nova", "anthropic.claude"], ) @pytest.mark.vcr() def test_invoke_model_no_content_different_events( span_exporter, log_exporter, bedrock_runtime_client, instrument_no_content, model_family, ): llm_model_value = get_model_name_from_family(model_family) max_tokens = 10 if llm_model_value == "amazon.nova-micro-v1:0": messages = amazon_nova_messages() system = amazon_nova_system() finish_reason = "max_tokens" elif llm_model_value == "anthropic.claude-v2": messages = anthropic_claude_messages() system = anthropic_claude_system() finish_reason = "end_turn" else: pytest.xfail("llm_model_value not handled: {llm_model_value}") body = get_invoke_model_body( llm_model_value, max_tokens=max_tokens, system=system, messages=messages, ) response = bedrock_runtime_client.invoke_model( body=body, modelId=llm_model_value, ) (span,) = span_exporter.get_finished_spans() assert_completion_attributes_from_streaming_body( span, llm_model_value, response, "chat", request_max_tokens=max_tokens, ) logs = log_exporter.get_finished_logs() assert len(logs) == 5 assert_message_in_logs(logs[0], "gen_ai.system.message", None, span) assert_message_in_logs(logs[1], "gen_ai.user.message", None, span) assert_message_in_logs(logs[2], "gen_ai.assistant.message", None, span) assert_message_in_logs(logs[3], "gen_ai.user.message", None, span) choice_body = { "index": 0, "finish_reason": finish_reason, "message": {"role": "assistant"}, } assert_message_in_logs(logs[4], "gen_ai.choice", choice_body, span) @pytest.mark.parametrize( "model_family", ["amazon.nova", "anthropic.claude"], ) @pytest.mark.vcr() def test_invoke_model_no_content_tool_call( span_exporter, log_exporter, bedrock_runtime_client, instrument_no_content, model_family, ): if model_family == "amazon.nova": llm_model_value = "amazon.nova-micro-v1:0" llm_model_config = AmazonNovaModel elif model_family == "anthropic.claude": llm_model_value = "us.anthropic.claude-3-5-sonnet-20240620-v1:0" llm_model_config = AnthropicClaudeModel else: pytest.xfail("model family not handled: {model_family}") invoke_model_tool_call( span_exporter, log_exporter, bedrock_runtime_client, llm_model_value, llm_model_config, expect_content=False, ) @pytest.mark.vcr() def test_invoke_model_with_invalid_model( span_exporter, log_exporter, bedrock_runtime_client, instrument_with_content, ): llm_model_value = "does-not-exist" with pytest.raises(bedrock_runtime_client.exceptions.ClientError): bedrock_runtime_client.invoke_model( body=b"", modelId=llm_model_value, ) (span,) = span_exporter.get_finished_spans() assert_completion_attributes_from_streaming_body( span, llm_model_value, None, "chat", ) assert span.status.status_code == StatusCode.ERROR assert span.attributes[ERROR_TYPE] == "ValidationException" logs = log_exporter.get_finished_logs() assert len(logs) == 0 @pytest.mark.parametrize( "model_family", ["amazon.nova", "amazon.titan", "anthropic.claude"], ) @pytest.mark.vcr() def test_invoke_model_with_response_stream_with_content( span_exporter, log_exporter, bedrock_runtime_client, instrument_with_content, model_family, ): # pylint:disable=too-many-locals,too-many-branches,too-many-statements llm_model_value = get_model_name_from_family(model_family) max_tokens, temperature, top_p, stop_sequences = 10, 0.8, 1, ["|"] body = get_invoke_model_body(llm_model_value, max_tokens, temperature, top_p, stop_sequences) response = bedrock_runtime_client.invoke_model_with_response_stream( body=body, modelId=llm_model_value, ) # consume the stream in order to have it traced finish_reason = None input_tokens, output_tokens = None, None text = "" for event in response["body"]: json_bytes = event["chunk"].get("bytes", b"") decoded = json_bytes.decode("utf-8") chunk = json.loads(decoded) # amazon.titan if (stop_reason := chunk.get("completionReason")) is not None: finish_reason = stop_reason if (output_text := chunk.get("outputText")) is not None: text += output_text # amazon.titan, anthropic.claude if invocation_metrics := chunk.get("amazon-bedrock-invocationMetrics"): input_tokens = invocation_metrics["inputTokenCount"] output_tokens = invocation_metrics["outputTokenCount"] # anthropic.claude if (message_type := chunk.get("type")) is not None: if message_type == "content_block_start": text += chunk["content_block"]["text"] elif message_type == "content_block_delta": text += chunk["delta"]["text"] elif message_type == "message_delta": finish_reason = chunk["delta"]["stop_reason"] # amazon nova if "contentBlockDelta" in chunk: text += chunk["contentBlockDelta"]["delta"]["text"] if "messageStop" in chunk: finish_reason = chunk["messageStop"]["stopReason"] if "metadata" in chunk: usage = chunk["metadata"]["usage"] input_tokens = usage["inputTokens"] output_tokens = usage["outputTokens"] assert text assert finish_reason assert input_tokens assert output_tokens (span,) = span_exporter.get_finished_spans() assert_stream_completion_attributes( span, llm_model_value, input_tokens, output_tokens, (finish_reason,), "text_completion" if model_family == "amazon.titan" else "chat", top_p, temperature, max_tokens, stop_sequences, ) logs = log_exporter.get_finished_logs() assert len(logs) == 2 if model_family == "anthropic.claude": user_content = {"content": [{"text": "Say this is a test", "type": "text"}]} else: user_content = {"content": [{"text": "Say this is a test"}]} assert_message_in_logs(logs[0], "gen_ai.user.message", user_content, span) if model_family == "anthropic.claude": choice_message = { "content": [{"text": "Okay, I will repeat: This is a test", "type": "text"}], "role": "assistant", } elif model_family == "amazon.nova": choice_message = { "content": [{"text": "It sounds like you're initiating a message or"}], "role": "assistant", } elif model_family == "amazon.titan": choice_message = {"content": [{"text": "\nHello! I am a computer program designed to"}]} else: pytest.xfail("model family not handled: {model_family}") choice_body = { "index": 0, "finish_reason": finish_reason, "message": choice_message, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_body, span) @pytest.mark.parametrize( "model_family", ["amazon.nova", "anthropic.claude"], ) @pytest.mark.vcr() def test_invoke_model_with_response_stream_with_content_different_events( span_exporter, log_exporter, bedrock_runtime_client, instrument_with_content, model_family, ): # pylint:disable=too-many-locals llm_model_value = get_model_name_from_family(model_family) if llm_model_value == "amazon.nova-micro-v1:0": messages = amazon_nova_messages() system = amazon_nova_system() finish_reason = "max_tokens" choice_content = [{"text": "This is a test again. If you need any"}] elif llm_model_value == "anthropic.claude-v2": messages = anthropic_claude_messages() system = anthropic_claude_system() finish_reason = "end_turn" choice_content = [{"text": "This is a test", "type": "text"}] else: pytest.xfail("llm_model_value not handled: {llm_model_value}") max_tokens = 10 body = get_invoke_model_body( llm_model_value, system=system, messages=messages, max_tokens=max_tokens, ) response = bedrock_runtime_client.invoke_model_with_response_stream( body=body, modelId=llm_model_value, ) # consume the stream in order to have it traced for _ in response["body"]: pass (span,) = span_exporter.get_finished_spans() assert_stream_completion_attributes( span, llm_model_value, input_tokens=mock.ANY, output_tokens=mock.ANY, request_max_tokens=max_tokens, finish_reason=(finish_reason,), operation_name="chat", ) logs = log_exporter.get_finished_logs() assert len(logs) == 5 assert_message_in_logs( logs[0], "gen_ai.system.message", {"content": [{"text": "You are a friendly model"}]}, span, ) user_message, assistant_message, last_user_message = messages user_content = filter_message_keys(user_message, ["content"]) assert_message_in_logs(logs[1], "gen_ai.user.message", user_content, span) assistant_content = filter_message_keys(assistant_message, ["content"]) assert_message_in_logs(logs[2], "gen_ai.assistant.message", assistant_content, span) last_user_content = filter_message_keys(last_user_message, ["content"]) assert_message_in_logs(logs[3], "gen_ai.user.message", last_user_content, span) choice_body = { "index": 0, "finish_reason": finish_reason, "message": {"content": choice_content, "role": "assistant"}, } assert_message_in_logs(logs[4], "gen_ai.choice", choice_body, span) def invoke_model_with_response_stream_tool_call( span_exporter, log_exporter, bedrock_runtime_client, llm_model_value, llm_model_config, expect_content, ): # pylint:disable=too-many-locals,too-many-statements,too-many-branches user_prompt = "What is the weather in Seattle and San Francisco today? Please give one tool call for Seattle and one for San Francisco" user_msg_content = llm_model_config.user_message(user_prompt) messages = [{"role": "user", "content": [user_msg_content]}] max_tokens = 1000 tool_config = llm_model_config.tool_config() body = get_invoke_model_body( llm_model_value, messages=messages, tools=tool_config, max_tokens=max_tokens, ) response_0 = bedrock_runtime_client.invoke_model_with_response_stream( body=body, modelId=llm_model_value, ) content = llm_model_config.get_stream_body_content(response_0["body"]) assert content tool_requests_ids = llm_model_config.tool_requests_ids_from_stream(content) assert len(tool_requests_ids) == 2 tool_call_result = llm_model_config.tool_calls_results(tool_requests_ids) # remove extra attributes from response messages.append({"role": "assistant", "content": content}) messages.append(tool_call_result) body = get_invoke_model_body( llm_model_value, messages=messages, max_tokens=max_tokens, tools=tool_config, ) response_1 = bedrock_runtime_client.invoke_model_with_response_stream( body=body, modelId=llm_model_value, ) response_1_content = llm_model_config.get_stream_body_content(response_1["body"]) assert response_1_content (span_0, span_1) = span_exporter.get_finished_spans() assert_stream_completion_attributes( span_0, llm_model_value, input_tokens=mock.ANY, output_tokens=mock.ANY, request_max_tokens=max_tokens, finish_reason=("tool_use",), operation_name="chat", ) assert_stream_completion_attributes( span_1, llm_model_value, input_tokens=mock.ANY, output_tokens=mock.ANY, request_max_tokens=max_tokens, finish_reason=("end_turn",), operation_name="chat", ) logs = log_exporter.get_finished_logs() assert len(logs) == 8 # first span if expect_content: user_content = filter_message_keys(messages[0], ["content"]) else: user_content = {} assert_message_in_logs(logs[0], "gen_ai.user.message", user_content, span_0) function_call_0 = {"name": "get_current_weather"} function_call_1 = {"name": "get_current_weather"} if expect_content: function_call_0["arguments"] = {"location": "Seattle"} function_call_1["arguments"] = {"location": "San Francisco"} choice_body = { "index": 0, "finish_reason": "tool_use", "message": { "role": "assistant", "tool_calls": [ { "id": tool_requests_ids[0], "type": "function", "function": function_call_0, }, { "id": tool_requests_ids[1], "type": "function", "function": function_call_1, }, ], }, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_body, span_0) # second span assert_message_in_logs(logs[2], "gen_ai.user.message", user_content, span_1) assistant_body = {"role": "assistant", "content": content} assistant_body["tool_calls"] = choice_body["message"]["tool_calls"] assistant_body.pop("role") if not expect_content: assistant_body.pop("content") assert_message_in_logs( logs[3], "gen_ai.assistant.message", assistant_body, span_1, ) tool_message_0, tool_message_1 = llm_model_config.tool_messages(tool_requests_ids, tool_call_result, expect_content) assert_message_in_logs(logs[4], "gen_ai.tool.message", tool_message_0, span_1) assert_message_in_logs(logs[5], "gen_ai.tool.message", tool_message_1, span_1) user_message_body = tool_call_result user_message_body.pop("role") if not expect_content: user_message_body.pop("content") assert_message_in_logs(logs[6], "gen_ai.user.message", user_message_body, span_1) choice_body = { "index": 0, "finish_reason": "end_turn", "message": { "role": "assistant", "content": response_1_content, }, } if not expect_content: choice_body["message"].pop("content") assert_message_in_logs(logs[7], "gen_ai.choice", choice_body, span_1) @pytest.mark.parametrize( "model_family", ["amazon.nova", "anthropic.claude"], ) @pytest.mark.vcr() def test_invoke_model_with_response_stream_with_content_tool_call( span_exporter, log_exporter, bedrock_runtime_client, instrument_with_content, model_family, ): if model_family == "amazon.nova": llm_model_value = "amazon.nova-micro-v1:0" llm_model_config = AmazonNovaModel elif model_family == "anthropic.claude": llm_model_value = "us.anthropic.claude-3-5-sonnet-20240620-v1:0" llm_model_config = AnthropicClaudeModel else: pytest.xfail("model family not handled: {model_family}") invoke_model_with_response_stream_tool_call( span_exporter, log_exporter, bedrock_runtime_client, llm_model_value, llm_model_config, expect_content=True, ) @pytest.mark.parametrize( "model_family", ["amazon.nova", "amazon.titan", "anthropic.claude"], ) @pytest.mark.vcr() def test_invoke_model_with_response_stream_no_content( span_exporter, log_exporter, bedrock_runtime_client, instrument_no_content, model_family, ): # pylint:disable=too-many-locals,too-many-branches llm_model_value = get_model_name_from_family(model_family) max_tokens, temperature, top_p, stop_sequences = 10, 0.8, 1, ["|"] body = get_invoke_model_body(llm_model_value, max_tokens, temperature, top_p, stop_sequences) response = bedrock_runtime_client.invoke_model_with_response_stream( body=body, modelId=llm_model_value, ) # consume the stream in order to have it traced finish_reason = None input_tokens, output_tokens = None, None text = "" for event in response["body"]: json_bytes = event["chunk"].get("bytes", b"") decoded = json_bytes.decode("utf-8") chunk = json.loads(decoded) # amazon.titan if (stop_reason := chunk.get("completionReason")) is not None: finish_reason = stop_reason if (output_text := chunk.get("outputText")) is not None: text += output_text # amazon.titan, anthropic.claude if invocation_metrics := chunk.get("amazon-bedrock-invocationMetrics"): input_tokens = invocation_metrics["inputTokenCount"] output_tokens = invocation_metrics["outputTokenCount"] # anthropic.claude if (message_type := chunk.get("type")) is not None: if message_type == "content_block_start": text += chunk["content_block"]["text"] elif message_type == "content_block_delta": text += chunk["delta"]["text"] elif message_type == "message_delta": finish_reason = chunk["delta"]["stop_reason"] # amazon nova if "contentBlockDelta" in chunk: text += chunk["contentBlockDelta"]["delta"]["text"] if "messageStop" in chunk: finish_reason = chunk["messageStop"]["stopReason"] if "metadata" in chunk: usage = chunk["metadata"]["usage"] input_tokens = usage["inputTokens"] output_tokens = usage["outputTokens"] assert text assert finish_reason assert input_tokens assert output_tokens (span,) = span_exporter.get_finished_spans() assert_stream_completion_attributes( span, llm_model_value, input_tokens, output_tokens, (finish_reason,), "text_completion" if model_family == "amazon.titan" else "chat", top_p, temperature, max_tokens, stop_sequences, ) logs = log_exporter.get_finished_logs() assert len(logs) == 2 assert_message_in_logs(logs[0], "gen_ai.user.message", None, span) message = {} if model_family == "amazon.titan" else {"role": "assistant"} choice_body = { "index": 0, "finish_reason": finish_reason, "message": message, } assert_message_in_logs(logs[1], "gen_ai.choice", choice_body, span) @pytest.mark.parametrize( "model_family", ["amazon.nova", "anthropic.claude"], ) @pytest.mark.vcr() def test_invoke_model_with_response_stream_no_content_different_events( span_exporter, log_exporter, bedrock_runtime_client, instrument_no_content, model_family, ): llm_model_value = get_model_name_from_family(model_family) if llm_model_value == "amazon.nova-micro-v1:0": messages = amazon_nova_messages() system = amazon_nova_system() finish_reason = "max_tokens" elif llm_model_value == "anthropic.claude-v2": messages = anthropic_claude_messages() system = anthropic_claude_system() finish_reason = "end_turn" else: pytest.xfail("llm_model_value not handled: {llm_model_value}") max_tokens = 10 body = get_invoke_model_body( llm_model_value, system=system, messages=messages, max_tokens=max_tokens, ) response = bedrock_runtime_client.invoke_model_with_response_stream( body=body, modelId=llm_model_value, ) # consume the stream in order to have it traced for _ in response["body"]: pass (span,) = span_exporter.get_finished_spans() assert_stream_completion_attributes( span, llm_model_value, input_tokens=mock.ANY, output_tokens=mock.ANY, request_max_tokens=max_tokens, finish_reason=(finish_reason,), operation_name="chat", ) logs = log_exporter.get_finished_logs() assert len(logs) == 5 assert_message_in_logs(logs[0], "gen_ai.system.message", None, span) assert_message_in_logs(logs[1], "gen_ai.user.message", None, span) assert_message_in_logs(logs[2], "gen_ai.assistant.message", None, span) assert_message_in_logs(logs[3], "gen_ai.user.message", None, span) choice_body = { "index": 0, "finish_reason": finish_reason, "message": {"role": "assistant"}, } assert_message_in_logs(logs[4], "gen_ai.choice", choice_body, span) @pytest.mark.parametrize( "model_family", ["amazon.nova", "anthropic.claude"], ) @pytest.mark.vcr() def test_invoke_model_with_response_stream_no_content_tool_call( span_exporter, log_exporter, bedrock_runtime_client, instrument_no_content, model_family, ): if model_family == "amazon.nova": llm_model_value = "amazon.nova-micro-v1:0" llm_model_config = AmazonNovaModel elif model_family == "anthropic.claude": llm_model_value = "us.anthropic.claude-3-5-sonnet-20240620-v1:0" llm_model_config = AnthropicClaudeModel else: pytest.xfail("model family not handled: {model_family}") invoke_model_with_response_stream_tool_call( span_exporter, log_exporter, bedrock_runtime_client, llm_model_value, llm_model_config, expect_content=False, ) @pytest.mark.parametrize( "model_family", ["amazon.nova", "amazon.titan", "anthropic.claude"], ) @pytest.mark.vcr() def test_invoke_model_with_response_stream_close_before_consumption( span_exporter, log_exporter, bedrock_runtime_client, instrument_with_content, model_family, ): llm_model_value = get_model_name_from_family(model_family) max_tokens, temperature, top_p, stop_sequences = 10, 0.8, 1, ["|"] body = get_invoke_model_body(llm_model_value, max_tokens, temperature, top_p, stop_sequences) response = bedrock_runtime_client.invoke_model_with_response_stream( body=body, modelId=llm_model_value, ) # Close the stream without consuming it response["body"].close() # Verify span is closed regardless of stream consumption (span,) = span_exporter.get_finished_spans() assert_stream_completion_attributes( span, llm_model_value, input_tokens=None, output_tokens=None, finish_reason=None, operation_name="text_completion" if model_family == "amazon.titan" else "chat", request_top_p=top_p, request_temperature=temperature, request_max_tokens=max_tokens, request_stop_sequences=None if model_family == "meta.llama" else stop_sequences, ) assert span.status.status_code == StatusCode.UNSET logs = log_exporter.get_finished_logs() assert len(logs) == 1 # Set appropriate user_content based on model family if model_family == "anthropic.claude": user_content = {"content": [{"text": "Say this is a test", "type": "text"}]} else: user_content = {"content": [{"text": "Say this is a test"}]} assert_message_in_logs(logs[0], "gen_ai.user.message", user_content, span) @pytest.mark.vcr() def test_invoke_model_with_response_stream_handles_stream_error( span_exporter, log_exporter, bedrock_runtime_client, instrument_with_content, ): # pylint:disable=too-many-locals llm_model_value = "amazon.titan-text-lite-v1" max_tokens, temperature, top_p, stop_sequences = 10, 0.8, 1, ["|"] body = get_invoke_model_body(llm_model_value, max_tokens, temperature, top_p, stop_sequences) response = bedrock_runtime_client.invoke_model_with_response_stream( body=body, modelId=llm_model_value, ) # consume the stream in order to have it traced finish_reason = None input_tokens, output_tokens = None, None with mock.patch.object( EventStream, "_parse_event", side_effect=EventStreamError({"modelStreamErrorException": {}}, "InvokeModelWithResponseStream"), ): with pytest.raises(EventStreamError): for _event in response["body"]: pass (span,) = span_exporter.get_finished_spans() assert_stream_completion_attributes( span, llm_model_value, input_tokens, output_tokens, finish_reason, "text_completion", top_p, temperature, max_tokens, stop_sequences, ) logs = log_exporter.get_finished_logs() assert len(logs) == 1 user_content = {"content": [{"text": "Say this is a test"}]} assert_message_in_logs(logs[0], "gen_ai.user.message", user_content, span) @pytest.mark.vcr() def test_invoke_model_with_response_stream_invalid_model( span_exporter, log_exporter, bedrock_runtime_client, instrument_with_content, ): llm_model_value = "does-not-exist" with pytest.raises(bedrock_runtime_client.exceptions.ClientError): bedrock_runtime_client.invoke_model_with_response_stream( body=b"", modelId=llm_model_value, ) (span,) = span_exporter.get_finished_spans() assert_completion_attributes_from_streaming_body( span, llm_model_value, None, "chat", ) assert span.status.status_code == StatusCode.ERROR assert span.attributes[ERROR_TYPE] == "ValidationException" logs = log_exporter.get_finished_logs() assert len(logs) == 0 @pytest.mark.parametrize( "input_value,expected_output", [ ({"location": "Seattle"}, {"location": "Seattle"}), ({}, {}), (None, None), ], ) def test_anthropic_claude_chunk_tool_use_input_handling(input_value, expected_output): """Test that _process_anthropic_claude_chunk handles various tool_use input formats.""" def stream_done_callback(response, ended): pass def stream_error_callback(exc, ended): pass wrapper = InvokeModelWithResponseStreamWrapper( stream=mock.MagicMock(), stream_done_callback=stream_done_callback, stream_error_callback=stream_error_callback, model_id="anthropic.claude-3-5-sonnet-20240620-v1:0", ) # Simulate message_start wrapper._process_anthropic_claude_chunk( { "type": "message_start", "message": { "role": "assistant", "content": [], }, } ) # Simulate content_block_start with specified input content_block = { "type": "tool_use", "id": "test_id", "name": "test_tool", } if input_value is not None: content_block["input"] = input_value wrapper._process_anthropic_claude_chunk( { "type": "content_block_start", "index": 0, "content_block": content_block, } ) # Simulate content_block_stop wrapper._process_anthropic_claude_chunk({"type": "content_block_stop", "index": 0}) # Verify the message content assert len(wrapper._message["content"]) == 1 tool_block = wrapper._message["content"][0] assert tool_block["type"] == "tool_use" assert tool_block["id"] == "test_id" assert tool_block["name"] == "test_tool" if expected_output is not None: assert tool_block["input"] == expected_output assert isinstance(tool_block["input"], dict) else: assert "input" not in tool_block def test_converse_stream_with_missing_output_in_response(): # Test malformed response missing "output" key malformed_response = {"stopReason": "end_turn"} choice = _Choice.from_converse(malformed_response, capture_content=True) assert choice.finish_reason == "end_turn" assert choice.message == {} assert choice.index == 0 def test_converse_stream_accumulates_cache_tokens(): # The ConverseStream metadata event carries prompt cache token usage; # the wrapper should accumulate it alongside input/output tokens. wrapper = ConverseStreamWrapper( stream=mock.MagicMock(), stream_done_callback=lambda *args, **kwargs: None, stream_error_callback=lambda *args, **kwargs: None, ) wrapper._process_event( { "metadata": { "usage": { "inputTokens": 8, "outputTokens": 10, "cacheReadInputTokens": 1500, "cacheWriteInputTokens": 25, } } } ) usage = wrapper._response["usage"] assert usage["inputTokens"] == 8 assert usage["outputTokens"] == 10 assert usage["cacheReadInputTokens"] == 1500 assert usage["cacheWriteInputTokens"] == 25 def amazon_nova_messages(): return [ {"role": "user", "content": [{"text": "Say this is a test"}]}, {"role": "assistant", "content": [{"text": "This is a test"}]}, { "role": "user", "content": [{"text": "Say again this is a test"}], }, ] def amazon_nova_system(): return [ {"text": "You are a friendly model"}, ] def anthropic_claude_converse_messages(): return amazon_nova_messages() def anthropic_claude_converse_system(): return amazon_nova_system() def anthropic_claude_messages(): return [ { "role": "user", "content": [{"text": "Say this is a test", "type": "text"}], }, { "role": "assistant", "content": [{"text": "This is a test", "type": "text"}], }, { "role": "user", "content": [{"text": "Say again this is a test", "type": "text"}], }, ] def anthropic_claude_system(): return "You are a friendly model" def get_tool_config(): return { "tools": [ { "toolSpec": { "name": "get_current_weather", "description": "Get the current weather in a given location.", "inputSchema": { "json": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city", } }, "required": ["location"], } }, } } ] } def get_anthropic_tool_config(): return [ { "name": "get_current_weather", "description": "Get the current weather in a given location.", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The name of the city", } }, "required": ["location"], }, } ] test_botocore_dynamodb.py000066400000000000000000000426541527070072300411570ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json from unittest import mock import botocore.session from moto import mock_aws # pylint: disable=import-error from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.instrumentation.botocore.extensions.dynamodb import ( _BotocoreInstrumentorContext, _DynamoDbExtension, ) from opentelemetry.semconv._incubating.attributes import ( aws_attributes, db_attributes, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace.span import Span # pylint: disable=too-many-public-methods class TestDynamoDbExtension(TestBase): def setUp(self): super().setUp() BotocoreInstrumentor().instrument() session = botocore.session.get_session() session.set_credentials(access_key="access-key", secret_key="secret-key") self.client = session.create_client("dynamodb", region_name="us-west-2") self.default_table_name = "test_table" def tearDown(self): super().tearDown() BotocoreInstrumentor().uninstrument() def _create_table(self, **kwargs): create_args = { "TableName": self.default_table_name, "AttributeDefinitions": [ {"AttributeName": "id", "AttributeType": "S"}, {"AttributeName": "idl", "AttributeType": "S"}, {"AttributeName": "idg", "AttributeType": "S"}, ], "KeySchema": [{"AttributeName": "id", "KeyType": "HASH"}], "ProvisionedThroughput": { "ReadCapacityUnits": 5, "WriteCapacityUnits": 5, }, "LocalSecondaryIndexes": [ { "IndexName": "lsi", "KeySchema": [{"AttributeName": "idl", "KeyType": "HASH"}], "Projection": {"ProjectionType": "KEYS_ONLY"}, } ], "GlobalSecondaryIndexes": [ { "IndexName": "gsi", "KeySchema": [{"AttributeName": "idg", "KeyType": "HASH"}], "Projection": {"ProjectionType": "KEYS_ONLY"}, } ], } create_args.update(kwargs) self.client.create_table(**create_args) def _create_prepared_table(self, **kwargs): self._create_table(**kwargs) table = kwargs.get("TableName", self.default_table_name) self.client.put_item( TableName=table, Item={"id": {"S": "1"}, "idl": {"S": "2"}, "idg": {"S": "3"}}, ) self.memory_exporter.clear() @staticmethod def _create_extension(operation: str) -> _DynamoDbExtension: call_context = mock.MagicMock(operation=operation) return _DynamoDbExtension(call_context) def assert_span(self, operation: str) -> Span: spans = self.memory_exporter.get_finished_spans() self.assertEqual(1, len(spans)) span = spans[0] self.assertEqual("dynamodb", span.attributes[db_attributes.DB_SYSTEM]) self.assertEqual(operation, span.attributes[db_attributes.DB_OPERATION]) self.assertEqual( "dynamodb.us-west-2.amazonaws.com", span.attributes[NET_PEER_NAME], ) return span def assert_table_names(self, span: Span, *table_names): self.assertEqual( tuple(table_names), span.attributes[aws_attributes.AWS_DYNAMODB_TABLE_NAMES], ) def assert_consumed_capacity(self, span: Span, *table_names): cap = span.attributes[aws_attributes.AWS_DYNAMODB_CONSUMED_CAPACITY] self.assertEqual(len(cap), len(table_names)) cap_tables = set() for item in cap: # should be like {"TableName": name, "CapacityUnits": number, ...} deserialized = json.loads(item) cap_tables.add(deserialized["TableName"]) for table_name in table_names: self.assertIn(table_name, cap_tables) def assert_item_col_metrics(self, span: Span): actual = span.attributes[aws_attributes.AWS_DYNAMODB_ITEM_COLLECTION_METRICS] self.assertIsNotNone(actual) json.loads(actual) def assert_provisioned_read_cap(self, span: Span, expected: int): actual = span.attributes[aws_attributes.AWS_DYNAMODB_PROVISIONED_READ_CAPACITY] self.assertEqual(expected, actual) def assert_provisioned_write_cap(self, span: Span, expected: int): actual = span.attributes[aws_attributes.AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY] self.assertEqual(expected, actual) def assert_consistent_read(self, span: Span, expected: bool): actual = span.attributes[aws_attributes.AWS_DYNAMODB_CONSISTENT_READ] self.assertEqual(expected, actual) def assert_projection(self, span: Span, expected: str): actual = span.attributes[aws_attributes.AWS_DYNAMODB_PROJECTION] self.assertEqual(expected, actual) def assert_attributes_to_get(self, span: Span, *attrs): self.assertEqual( tuple(attrs), span.attributes[aws_attributes.AWS_DYNAMODB_ATTRIBUTES_TO_GET], ) def assert_index_name(self, span: Span, expected: str): self.assertEqual(expected, span.attributes[aws_attributes.AWS_DYNAMODB_INDEX_NAME]) def assert_limit(self, span: Span, expected: int): self.assertEqual(expected, span.attributes[aws_attributes.AWS_DYNAMODB_LIMIT]) def assert_select(self, span: Span, expected: str): self.assertEqual(expected, span.attributes[aws_attributes.AWS_DYNAMODB_SELECT]) def assert_extension_item_col_metrics(self, operation: str): span = self.tracer_provider.get_tracer("test").start_span("test") extension = self._create_extension(operation) extension.on_success( span, {"ItemCollectionMetrics": {"ItemCollectionKey": {"id": "1"}}}, _BotocoreInstrumentorContext(logger=mock.Mock()), ) self.assert_item_col_metrics(span) @mock_aws def test_batch_get_item(self): table_name1 = "test_table1" table_name2 = "test_table2" self._create_prepared_table(TableName=table_name1) self._create_prepared_table(TableName=table_name2) self.client.batch_get_item( RequestItems={ table_name1: {"Keys": [{"id": {"S": "test_key"}}]}, table_name2: {"Keys": [{"id": {"S": "test_key2"}}]}, }, ReturnConsumedCapacity="TOTAL", ) span = self.assert_span("BatchGetItem") self.assert_table_names(span, table_name1, table_name2) self.assert_consumed_capacity(span, table_name1, table_name2) @mock_aws def test_batch_write_item(self): table_name1 = "test_table1" table_name2 = "test_table2" self._create_prepared_table(TableName=table_name1) self._create_prepared_table(TableName=table_name2) self.client.batch_write_item( RequestItems={ table_name1: [{"PutRequest": {"Item": {"id": {"S": "123"}}}}], table_name2: [{"PutRequest": {"Item": {"id": {"S": "456"}}}}], }, ReturnConsumedCapacity="TOTAL", ReturnItemCollectionMetrics="SIZE", ) span = self.assert_span("BatchWriteItem") self.assert_table_names(span, table_name1, table_name2) self.assert_consumed_capacity(span, table_name1, table_name2) self.assert_item_col_metrics(span) @mock_aws def test_create_table(self): local_sec_idx = { "IndexName": "local_sec_idx", "KeySchema": [{"AttributeName": "value", "KeyType": "HASH"}], "Projection": {"ProjectionType": "KEYS_ONLY"}, } global_sec_idx = { "IndexName": "global_sec_idx", "KeySchema": [{"AttributeName": "value", "KeyType": "HASH"}], "Projection": {"ProjectionType": "KEYS_ONLY"}, } self.client.create_table( AttributeDefinitions=[ {"AttributeName": "id", "AttributeType": "S"}, {"AttributeName": "value", "AttributeType": "S"}, ], KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], LocalSecondaryIndexes=[local_sec_idx], GlobalSecondaryIndexes=[global_sec_idx], ProvisionedThroughput={ "ReadCapacityUnits": 42, "WriteCapacityUnits": 17, }, TableName=self.default_table_name, ) span = self.assert_span("CreateTable") self.assert_table_names(span, self.default_table_name) self.assertEqual( (json.dumps(global_sec_idx),), span.attributes[aws_attributes.AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES], ) self.assertEqual( (json.dumps(local_sec_idx),), span.attributes[aws_attributes.AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES], ) self.assert_provisioned_read_cap(span, 42) @mock_aws def test_delete_item(self): self._create_prepared_table() self.client.delete_item( TableName=self.default_table_name, Key={"id": {"S": "1"}}, ReturnConsumedCapacity="TOTAL", ReturnItemCollectionMetrics="SIZE", ) span = self.assert_span("DeleteItem") self.assert_table_names(span, self.default_table_name) # moto does not seem to return these: # self.assert_consumed_capacity(span, self.default_table_name) # self.assert_item_coll_metrics(span) def test_delete_item_consumed_capacity(self): span = self.tracer_provider.get_tracer("test").start_span("test") extension = self._create_extension("DeleteItem") extension.on_success( span, {"ConsumedCapacity": {"TableName": "table"}}, _BotocoreInstrumentorContext(logger=mock.Mock()), ) self.assert_consumed_capacity(span, "table") def test_delete_item_item_collection_metrics(self): self.assert_extension_item_col_metrics("DeleteItem") @mock_aws def test_delete_table(self): self._create_prepared_table() self.client.delete_table(TableName=self.default_table_name) span = self.assert_span("DeleteTable") self.assert_table_names(span, self.default_table_name) @mock_aws def test_describe_table(self): self._create_prepared_table() self.client.describe_table(TableName=self.default_table_name) span = self.assert_span("DescribeTable") self.assert_table_names(span, self.default_table_name) @mock_aws def test_get_item_expression(self): self._create_prepared_table() self.client.get_item( TableName=self.default_table_name, Key={"id": {"S": "1"}}, ConsistentRead=True, ProjectionExpression="PE", ReturnConsumedCapacity="TOTAL", ) span = self.assert_span("GetItem") self.assert_table_names(span, self.default_table_name) self.assert_consistent_read(span, True) self.assert_consumed_capacity(span, self.default_table_name) @mock_aws def test_get_item_non_expression(self): self._create_prepared_table() self.client.get_item( TableName=self.default_table_name, Key={"id": {"S": "1"}}, ConsistentRead=True, ProjectionExpression="PE", ReturnConsumedCapacity="TOTAL", ) span = self.assert_span("GetItem") self.assert_table_names(span, self.default_table_name) self.assert_consistent_read(span, True) self.assert_projection(span, "PE") self.assert_consumed_capacity(span, self.default_table_name) @mock_aws def test_list_tables(self): self._create_table(TableName="my_table") self._create_prepared_table() self.client.list_tables(ExclusiveStartTableName="my_table", Limit=5) span = self.assert_span("ListTables") self.assertEqual( "my_table", span.attributes[aws_attributes.AWS_DYNAMODB_EXCLUSIVE_START_TABLE], ) self.assertEqual(1, span.attributes[aws_attributes.AWS_DYNAMODB_TABLE_COUNT]) self.assertEqual(5, span.attributes[aws_attributes.AWS_DYNAMODB_LIMIT]) @mock_aws def test_put_item(self): table = "test_table" self._create_prepared_table(TableName=table) self.client.put_item( TableName=table, Item={"id": {"S": "1"}, "idl": {"S": "2"}, "idg": {"S": "3"}}, ReturnConsumedCapacity="TOTAL", ReturnItemCollectionMetrics="SIZE", ) span = self.assert_span("PutItem") self.assert_table_names(span, table) self.assert_consumed_capacity(span, table) # moto does not seem to return these: # self.assert_item_coll_metrics(span) def test_put_item_item_collection_metrics(self): self.assert_extension_item_col_metrics("PutItem") @mock_aws def test_query(self): self._create_prepared_table() self.client.query( TableName=self.default_table_name, IndexName="lsi", Select="ALL_ATTRIBUTES", AttributesToGet=["id"], Limit=42, ConsistentRead=True, KeyConditions={ "id": { "AttributeValueList": [{"S": "123"}], "ComparisonOperator": "EQ", } }, ScanIndexForward=True, ProjectionExpression="PE", ReturnConsumedCapacity="TOTAL", ) span = self.assert_span("Query") self.assert_table_names(span, self.default_table_name) self.assertTrue(span.attributes[aws_attributes.AWS_DYNAMODB_SCAN_FORWARD]) self.assert_attributes_to_get(span, "id") self.assert_consistent_read(span, True) self.assert_index_name(span, "lsi") self.assert_limit(span, 42) self.assert_projection(span, "PE") self.assert_select(span, "ALL_ATTRIBUTES") self.assert_consumed_capacity(span, self.default_table_name) @mock_aws def test_scan(self): self._create_prepared_table() self.client.scan( TableName=self.default_table_name, IndexName="lsi", AttributesToGet=["id", "idl"], Limit=42, Select="ALL_ATTRIBUTES", TotalSegments=17, Segment=16, ProjectionExpression="PE", ConsistentRead=True, ReturnConsumedCapacity="TOTAL", ) span = self.assert_span("Scan") self.assert_table_names(span, self.default_table_name) self.assertEqual(16, span.attributes[aws_attributes.AWS_DYNAMODB_SEGMENT]) self.assertEqual(17, span.attributes[aws_attributes.AWS_DYNAMODB_TOTAL_SEGMENTS]) self.assertEqual(0, span.attributes[aws_attributes.AWS_DYNAMODB_COUNT]) self.assertEqual(0, span.attributes[aws_attributes.AWS_DYNAMODB_SCANNED_COUNT]) self.assert_attributes_to_get(span, "id", "idl") self.assert_consistent_read(span, True) self.assert_index_name(span, "lsi") self.assert_limit(span, 42) self.assert_projection(span, "PE") self.assert_select(span, "ALL_ATTRIBUTES") self.assert_consumed_capacity(span, self.default_table_name) @mock_aws def test_update_item(self): self._create_prepared_table() self.client.update_item( TableName=self.default_table_name, Key={"id": {"S": "123"}}, AttributeUpdates={"id": {"Value": {"S": "456"}, "Action": "PUT"}}, ReturnConsumedCapacity="TOTAL", ReturnItemCollectionMetrics="SIZE", ) span = self.assert_span("UpdateItem") self.assert_table_names(span, self.default_table_name) self.assert_consumed_capacity(span, self.default_table_name) # moto does not seem to return these: # self.assert_item_coll_metrics(span) def test_update_item_item_collection_metrics(self): self.assert_extension_item_col_metrics("UpdateItem") @mock_aws def test_update_table(self): self._create_prepared_table() global_sec_idx_updates = { "Update": { "IndexName": "gsi", "ProvisionedThroughput": { "ReadCapacityUnits": 777, "WriteCapacityUnits": 666, }, } } attr_definition = {"AttributeName": "id", "AttributeType": "N"} self.client.update_table( TableName=self.default_table_name, AttributeDefinitions=[attr_definition], ProvisionedThroughput={ "ReadCapacityUnits": 23, "WriteCapacityUnits": 19, }, GlobalSecondaryIndexUpdates=[global_sec_idx_updates], ) span = self.assert_span("UpdateTable") self.assert_table_names(span, self.default_table_name) self.assert_provisioned_read_cap(span, 23) self.assert_provisioned_write_cap(span, 19) self.assertEqual( (json.dumps(attr_definition),), span.attributes[aws_attributes.AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS], ) self.assertEqual( (json.dumps(global_sec_idx_updates),), span.attributes[aws_attributes.AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES], ) test_botocore_instrumentation.py000066400000000000000000000507471527070072300426270ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import os from importlib.metadata import EntryPoint from unittest.mock import ANY, Mock, call, patch from urllib.parse import urlparse import botocore.session from botocore.exceptions import ClientError, ParamValidationError from moto import mock_aws # pylint: disable=import-error from opentelemetry import trace as trace_api from opentelemetry.instrumentation.auto_instrumentation import ( _load_instrumentors, ) from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.instrumentation.utils import ( suppress_http_instrumentation, suppress_instrumentation, ) from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.propagators.aws.aws_xray_propagator import TRACE_HEADER_KEY from opentelemetry.semconv._incubating.attributes import rpc_attributes from opentelemetry.semconv._incubating.attributes.cloud_attributes import ( CLOUD_REGION, ) from opentelemetry.semconv._incubating.attributes.exception_attributes import ( EXCEPTION_MESSAGE, EXCEPTION_STACKTRACE, EXCEPTION_TYPE, ) from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_STATUS_CODE, ) from opentelemetry.semconv._incubating.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.test.mock_textmap import MockTextMapPropagator from opentelemetry.test.test_base import TestBase from opentelemetry.trace.span import format_span_id, format_trace_id _REQUEST_ID_REGEX_MATCH = r"[A-Za-z0-9]{52}" # pylint:disable=too-many-public-methods class TestBotocoreInstrumentor(TestBase): """Botocore integration testsuite""" def setUp(self): super().setUp() BotocoreInstrumentor().instrument() self.session = botocore.session.get_session() self.session.set_credentials(access_key="access-key", secret_key="secret-key") self.region = "us-west-2" def tearDown(self): super().tearDown() BotocoreInstrumentor().uninstrument() def _make_client(self, service: str): return self.session.create_client(service, region_name=self.region) def _default_span_attributes(self, service: str, operation: str): return { rpc_attributes.RPC_SYSTEM: "aws-api", rpc_attributes.RPC_SERVICE: service, rpc_attributes.RPC_METHOD: operation, CLOUD_REGION: self.region, "retry_attempts": 0, HTTP_STATUS_CODE: 200, # Some services like IAM or STS have a global endpoint and exclude specified region. SERVER_ADDRESS: f"{service.lower()}.{'' if self.region == 'aws-global' else self.region + '.'}amazonaws.com", SERVER_PORT: 443, } def assert_only_span(self): spans = self.memory_exporter.get_finished_spans() self.assertEqual(1, len(spans)) return spans[0] def assert_span( self, service: str, operation: str, request_id=None, attributes=None, ): span = self.assert_only_span() expected = self._default_span_attributes(service, operation) if attributes: expected.update(attributes) span_attributes_request_id = "aws.request_id" if request_id is _REQUEST_ID_REGEX_MATCH: actual_request_id = span.attributes[span_attributes_request_id] self.assertRegex(actual_request_id, _REQUEST_ID_REGEX_MATCH) expected[span_attributes_request_id] = actual_request_id elif request_id is not None: expected[span_attributes_request_id] = request_id self.assertSpanHasAttributes(span, expected) self.assertEqual(f"{service}.{operation}", span.name) return span @mock_aws def test_traced_client(self): ec2 = self._make_client("ec2") ec2.describe_instances() request_id = "fdcdcab1-ae5c-489e-9c33-4637c5dda355" self.assert_span("EC2", "DescribeInstances", request_id=request_id) @mock_aws def test_no_op_tracer_provider_ec2(self): BotocoreInstrumentor().uninstrument() BotocoreInstrumentor().instrument(tracer_provider=trace_api.NoOpTracerProvider()) ec2 = self._make_client("ec2") ec2.describe_instances() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) @mock_aws def test_not_recording(self): mock_tracer = Mock() mock_span = Mock() mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span with patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer ec2 = self._make_client("ec2") ec2.describe_instances() self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) @mock_aws def test_exception(self): s3 = self._make_client("s3") with self.assertRaises(ParamValidationError): s3.list_objects(bucket="mybucket") spans = self.memory_exporter.get_finished_spans() self.assertEqual(1, len(spans)) span = spans[0] expected = self._default_span_attributes("S3", "ListObjects") expected.pop(HTTP_STATUS_CODE) expected.pop("retry_attempts") self.assertEqual(expected, span.attributes) self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) self.assertEqual(1, len(span.events)) event = span.events[0] self.assertIn(EXCEPTION_STACKTRACE, event.attributes) self.assertIn(EXCEPTION_TYPE, event.attributes) self.assertIn(EXCEPTION_MESSAGE, event.attributes) @mock_aws def test_client_error_with_http_error_status(self): """ClientError with HTTP 4xx sets span status to ERROR.""" s3 = self._make_client("s3") with self.assertRaises(ClientError): s3.get_object(Bucket="non-existent-bucket", Key="non-existent-key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(1, len(spans)) span = spans[0] self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) @mock_aws def test_client_error_304_not_modified(self): """S3 IfNoneMatch returning 304 leaves span status as UNSET.""" s3 = self._make_client("s3") s3.create_bucket( Bucket="test-bucket", CreateBucketConfiguration={"LocationConstraint": self.region}, ) etag = s3.put_object(Bucket="test-bucket", Key="key", Body=b"data")["ETag"] with self.assertRaises(ClientError): s3.get_object(Bucket="test-bucket", Key="key", IfNoneMatch=etag) spans = self.memory_exporter.get_finished_spans() # Filter to the GetObject span get_spans = [s for s in spans if s.name == "S3.GetObject"] self.assertEqual(1, len(get_spans)) self.assertIs(get_spans[0].status.status_code, trace_api.StatusCode.UNSET) @mock_aws def test_s3_client(self): s3 = self._make_client("s3") s3.list_buckets() self.assert_span("S3", "ListBuckets") @mock_aws def test_no_op_tracer_provider_s3(self): BotocoreInstrumentor().uninstrument() BotocoreInstrumentor().instrument(tracer_provider=trace_api.NoOpTracerProvider()) s3 = self._make_client("s3") s3.list_buckets() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) @mock_aws def test_s3_put(self): s3 = self._make_client("s3") location = {"LocationConstraint": "us-west-2"} s3.create_bucket(Bucket="mybucket", CreateBucketConfiguration=location) self.assert_span("S3", "CreateBucket", request_id=_REQUEST_ID_REGEX_MATCH) self.memory_exporter.clear() s3.put_object(Key="foo", Bucket="mybucket", Body=b"bar") self.assert_span("S3", "PutObject", request_id=_REQUEST_ID_REGEX_MATCH) self.memory_exporter.clear() s3.get_object(Bucket="mybucket", Key="foo") self.assert_span("S3", "GetObject", request_id=_REQUEST_ID_REGEX_MATCH) @mock_aws def test_sqs_client(self): sqs = self._make_client("sqs") sqs.list_queues() self.assert_span("SQS", "ListQueues", request_id=_REQUEST_ID_REGEX_MATCH) @mock_aws def test_no_op_tracer_provider_sqs(self): BotocoreInstrumentor().uninstrument() BotocoreInstrumentor().instrument(tracer_provider=trace_api.NoOpTracerProvider()) sqs = self._make_client("sqs") sqs.list_queues() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) @mock_aws def test_sqs_send_message(self): sqs = self._make_client("sqs") test_queue_name = "test_queue_name" response = sqs.create_queue(QueueName=test_queue_name) self.assert_span("SQS", "CreateQueue", request_id=_REQUEST_ID_REGEX_MATCH) self.memory_exporter.clear() queue_url = response["QueueUrl"] sqs.send_message(QueueUrl=queue_url, MessageBody="Test SQS MESSAGE!") self.assert_span( "SQS", "SendMessage", request_id=_REQUEST_ID_REGEX_MATCH, attributes={"aws.queue_url": queue_url}, ) @mock_aws def test_kinesis_client(self): kinesis = self._make_client("kinesis") kinesis.list_streams() self.assert_span("Kinesis", "ListStreams") @mock_aws def test_no_op_tracer_provider_kinesis(self): BotocoreInstrumentor().uninstrument() BotocoreInstrumentor().instrument(tracer_provider=trace_api.NoOpTracerProvider()) kinesis = self._make_client("kinesis") kinesis.list_streams() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) @mock_aws def test_unpatch(self): kinesis = self._make_client("kinesis") BotocoreInstrumentor().uninstrument() kinesis.list_streams() self.assertEqual(0, len(self.memory_exporter.get_finished_spans())) @mock_aws def test_no_op_tracer_provider_kms(self): BotocoreInstrumentor().uninstrument() BotocoreInstrumentor().instrument(tracer_provider=trace_api.NoOpTracerProvider()) kms = self._make_client("kms") kms.list_keys(Limit=21) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) @mock_aws def test_uninstrument_does_not_inject_headers(self): headers = {} def intercept_headers(**kwargs): headers.update(kwargs["request"].headers) ec2 = self._make_client("ec2") BotocoreInstrumentor().uninstrument() ec2.meta.events.register_first("before-send.ec2.DescribeInstances", intercept_headers) with self.tracer_provider.get_tracer("test").start_span("parent"): ec2.describe_instances() self.assertNotIn(TRACE_HEADER_KEY, headers) @mock_aws def test_double_patch(self): sqs = self._make_client("sqs") BotocoreInstrumentor().instrument() BotocoreInstrumentor().instrument() sqs.list_queues() self.assert_span("SQS", "ListQueues", request_id=_REQUEST_ID_REGEX_MATCH) @mock_aws def test_kms_client(self): kms = self._make_client("kms") kms.list_keys(Limit=21) span = self.assert_only_span() expected = self._default_span_attributes("KMS", "ListKeys") expected["aws.request_id"] = ANY # check for exact attribute set to make sure not to leak any kms secrets self.assertEqual(expected, dict(span.attributes)) @mock_aws def test_sts_client(self): sts = self._make_client("sts") sts.get_caller_identity() span = self.assert_only_span() expected = self._default_span_attributes("STS", "GetCallerIdentity") expected["aws.request_id"] = ANY expected[SERVER_ADDRESS] = urlparse(sts.meta.endpoint_url).hostname # check for exact attribute set to make sure not to leak any sts secrets self.assertEqual(expected, dict(span.attributes)) @mock_aws def test_no_op_tracer_provider_sts(self): BotocoreInstrumentor().uninstrument() BotocoreInstrumentor().instrument(tracer_provider=trace_api.NoOpTracerProvider()) sts = self._make_client("sts") sts.get_caller_identity() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) @mock_aws def test_propagator_injects_into_request(self): headers = {} previous_propagator = get_global_textmap() def check_headers(**kwargs): nonlocal headers headers = kwargs["request"].headers try: set_global_textmap(MockTextMapPropagator()) ec2 = self._make_client("ec2") ec2.meta.events.register_first("before-send.ec2.DescribeInstances", check_headers) ec2.describe_instances() request_id = "fdcdcab1-ae5c-489e-9c33-4637c5dda355" span = self.assert_span("EC2", "DescribeInstances", request_id=request_id) # only x-ray propagation is used in HTTP requests self.assertIn(TRACE_HEADER_KEY, headers) xray_context = headers[TRACE_HEADER_KEY] formated_trace_id = format_trace_id(span.get_span_context().trace_id) formated_trace_id = formated_trace_id[:8] + "-" + formated_trace_id[8:] self.assertEqual( xray_context.lower(), f"root=1-{formated_trace_id};parent={format_span_id(span.get_span_context().span_id)};sampled=1".lower(), ) finally: set_global_textmap(previous_propagator) @mock_aws def test_no_op_tracer_provider_xray(self): BotocoreInstrumentor().uninstrument() BotocoreInstrumentor().instrument(tracer_provider=trace_api.NoOpTracerProvider()) xray_client = self._make_client("xray") xray_client.put_trace_segments(TraceSegmentDocuments=["str1"]) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) @mock_aws def test_override_xray_propagator_injects_into_request(self): headers = {} def check_headers(**kwargs): nonlocal headers headers = kwargs["request"].headers BotocoreInstrumentor().instrument() ec2 = self._make_client("ec2") ec2.meta.events.register_first("before-send.ec2.DescribeInstances", check_headers) ec2.describe_instances() self.assertNotIn(MockTextMapPropagator.TRACE_ID_KEY, headers) self.assertNotIn(MockTextMapPropagator.SPAN_ID_KEY, headers) @mock_aws def test_suppress_instrumentation_xray_client(self): xray_client = self._make_client("xray") with suppress_instrumentation(): xray_client.put_trace_segments(TraceSegmentDocuments=["str1"]) xray_client.put_trace_segments(TraceSegmentDocuments=["str2"]) self.assertEqual(0, len(self.get_finished_spans())) @mock_aws def test_suppress_http_instrumentation_xray_client(self): xray_client = self._make_client("xray") with suppress_http_instrumentation(): xray_client.put_trace_segments(TraceSegmentDocuments=["str1"]) xray_client.put_trace_segments(TraceSegmentDocuments=["str2"]) self.assertEqual(2, len(self.get_finished_spans())) @mock_aws def test_request_hook(self): request_hook_service_attribute_name = "request_hook.service_name" request_hook_operation_attribute_name = "request_hook.operation_name" request_hook_api_params_attribute_name = "request_hook.api_params" def request_hook(span, service_name, operation_name, api_params): hook_attributes = { request_hook_service_attribute_name: service_name, request_hook_operation_attribute_name: operation_name, request_hook_api_params_attribute_name: json.dumps(api_params), } span.set_attributes(hook_attributes) BotocoreInstrumentor().uninstrument() BotocoreInstrumentor().instrument(request_hook=request_hook) s3 = self._make_client("s3") params = { "Bucket": "mybucket", "CreateBucketConfiguration": {"LocationConstraint": "us-west-2"}, } s3.create_bucket(**params) self.assert_span( "S3", "CreateBucket", attributes={ request_hook_service_attribute_name: "s3", request_hook_operation_attribute_name: "CreateBucket", request_hook_api_params_attribute_name: json.dumps(params), }, ) @mock_aws def test_response_hook(self): response_hook_service_attribute_name = "request_hook.service_name" response_hook_operation_attribute_name = "response_hook.operation_name" response_hook_result_attribute_name = "response_hook.result" def response_hook(span, service_name, operation_name, result): hook_attributes = { response_hook_service_attribute_name: service_name, response_hook_operation_attribute_name: operation_name, response_hook_result_attribute_name: len(result["Buckets"]), } span.set_attributes(hook_attributes) BotocoreInstrumentor().uninstrument() BotocoreInstrumentor().instrument(response_hook=response_hook) s3 = self._make_client("s3") s3.list_buckets() self.assert_span( "S3", "ListBuckets", attributes={ response_hook_service_attribute_name: "s3", response_hook_operation_attribute_name: "ListBuckets", response_hook_result_attribute_name: 0, }, ) @mock_aws def test_server_attributes(self): # Test regional endpoint ec2 = self._make_client("ec2") ec2.describe_instances() self.assert_span( "EC2", "DescribeInstances", attributes={ SERVER_ADDRESS: f"ec2.{self.region}.amazonaws.com", SERVER_PORT: 443, }, ) self.memory_exporter.clear() # Test global endpoint iam_global = self._make_client("iam") iam_global.list_users() self.assert_span( "IAM", "ListUsers", attributes={ SERVER_ADDRESS: "iam.amazonaws.com", SERVER_PORT: 443, CLOUD_REGION: "aws-global", }, ) @mock_aws def test_server_attributes_with_custom_endpoint(self): with patch.dict( os.environ, {"MOTO_S3_CUSTOM_ENDPOINTS": "https://proxy.amazon.org:2025"}, ): s3 = self.session.create_client( "s3", region_name=self.region, endpoint_url="https://proxy.amazon.org:2025", ) s3.list_buckets() self.assert_span( "S3", "ListBuckets", attributes={ SERVER_ADDRESS: "proxy.amazon.org", SERVER_PORT: 2025, }, ) @patch("opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts") @patch("opentelemetry.instrumentation.auto_instrumentation._load._logger") def test_instruments_with_botocore_installed(self, mock_logger, mock_dep): def _load_instrumentor(ep: EntryPoint, **kwargs): # simulate aiobotocore not being present if ep.name == "aiobotocore": raise ModuleNotFoundError("aiobotocore") mock_distro = Mock() mock_dep.return_value = None mock_distro.load_instrumentor.side_effect = _load_instrumentor _load_instrumentors(mock_distro) eps = [c[0][0].name for c in mock_distro.load_instrumentor.call_args_list] self.assertIn("botocore", eps) mock_logger.debug.assert_has_calls( [ call("Instrumented %s", "botocore"), call( "Skipping instrumentation %s: %s", "aiobotocore", "aiobotocore", ), ], any_order=True, ) test_botocore_lambda.py000066400000000000000000000135611527070072300405750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import io import json import sys import zipfile from unittest import mock import botocore.session from moto import mock_aws # pylint: disable=import-error from pytest import mark from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.instrumentation.botocore.extensions.lmbd import ( _LambdaExtension, ) from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.semconv._incubating.attributes import rpc_attributes from opentelemetry.semconv._incubating.attributes.faas_attributes import ( FAAS_INVOKED_NAME, FAAS_INVOKED_PROVIDER, FAAS_INVOKED_REGION, ) from opentelemetry.test.mock_textmap import MockTextMapPropagator from opentelemetry.test.test_base import TestBase from opentelemetry.trace.span import Span def get_as_zip_file(file_name, content): zip_output = io.BytesIO() with zipfile.ZipFile(zip_output, "w", zipfile.ZIP_DEFLATED) as zip_file: zip_file.writestr(file_name, content) zip_output.seek(0) return zip_output.read() def return_headers_lambda_str(): pfunc = """ def lambda_handler(event, context): print("custom log event") headers = event.get('headers', event.get('attributes', {})) return headers """ return pfunc class TestLambdaExtension(TestBase): def setUp(self): super().setUp() BotocoreInstrumentor().instrument() session = botocore.session.get_session() session.set_credentials(access_key="access-key", secret_key="secret-key") self.region = "us-west-2" self.client = session.create_client("lambda", region_name=self.region) self.iam_client = session.create_client("iam", region_name=self.region) def tearDown(self): super().tearDown() BotocoreInstrumentor().uninstrument() def assert_span(self, operation: str) -> Span: spans = self.memory_exporter.get_finished_spans() self.assertEqual(1, len(spans)) span = spans[0] self.assertEqual(operation, span.attributes[rpc_attributes.RPC_METHOD]) self.assertEqual("Lambda", span.attributes[rpc_attributes.RPC_SERVICE]) self.assertEqual("aws-api", span.attributes[rpc_attributes.RPC_SYSTEM]) return span def assert_invoke_span(self, function_name: str) -> Span: span = self.assert_span("Invoke") self.assertEqual("aws", span.attributes[FAAS_INVOKED_PROVIDER]) self.assertEqual(self.region, span.attributes[FAAS_INVOKED_REGION]) self.assertEqual(function_name, span.attributes[FAAS_INVOKED_NAME]) return span @staticmethod def _create_extension(operation: str) -> _LambdaExtension: mock_call_context = mock.MagicMock(operation=operation, params={}) return _LambdaExtension(mock_call_context) @mock_aws def test_list_functions(self): self.client.list_functions() self.assert_span("ListFunctions") @mock_aws def _create_role_and_get_arn(self) -> str: return self.iam_client.create_role( RoleName="my-role", AssumeRolePolicyDocument="some policy", Path="/my-path/", )["Role"]["Arn"] def _create_lambda_function(self, function_name: str, function_code: str): role_arn = self._create_role_and_get_arn() self.client.create_function( FunctionName=function_name, Runtime="python3.10", Role=role_arn, Handler="lambda_function.lambda_handler", Code={"ZipFile": get_as_zip_file("lambda_function.py", function_code)}, Description="test lambda function", Timeout=3, MemorySize=128, Publish=True, ) @mark.skip(reason="Docker error, unblocking builds for now.") @mark.skipif( sys.platform == "win32", reason="requires docker and Github CI Windows does not have docker installed by default", ) @mock_aws def test_invoke(self): previous_propagator = get_global_textmap() try: set_global_textmap(MockTextMapPropagator()) function_name = "testFunction" self._create_lambda_function(function_name, return_headers_lambda_str()) # 2 spans for create IAM + create lambda self.assertEqual(2, len(self.memory_exporter.get_finished_spans())) self.memory_exporter.clear() response = self.client.invoke( Payload=json.dumps({}), FunctionName=function_name, InvocationType="RequestResponse", ) span = self.assert_invoke_span(function_name) span_context = span.get_span_context() # # assert injected span headers = json.loads(response["Payload"].read().decode("utf-8")) self.assertEqual( str(span_context.trace_id), headers[MockTextMapPropagator.TRACE_ID_KEY], ) self.assertEqual( str(span_context.span_id), headers[MockTextMapPropagator.SPAN_ID_KEY], ) finally: set_global_textmap(previous_propagator) def test_invoke_parse_arn(self): function_name = "my_func" arns = ( f"arn:aws:lambda:{self.region}:000000000000:function:{function_name}", # full arn f"000000000000:{function_name}", # partial arn f"arn:aws:lambda:{self.region}:000000000000:function:{function_name}:alias", # aliased arn ) for arn in arns: with self.subTest(arn=arn): extension = self._create_extension("Invoke") extension._call_context.params["FunctionName"] = arn attributes = {} extension.extract_attributes(attributes) self.assertEqual(function_name, attributes[FAAS_INVOKED_NAME]) test_botocore_messaging.py000066400000000000000000000024711527070072300413300ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.instrumentation.botocore.extensions._messaging import ( inject_propagation_context, message_attributes_setter, ) from opentelemetry.test.test_base import TestBase class TestMessageAttributes(TestBase): def test_message_attributes_setter(self): carrier = {} message_attributes_setter.set(carrier, "key", "value") self.assertEqual({"key": {"DataType": "String", "StringValue": "value"}}, carrier) def test_inject_propagation_context(self): carrier = { "key1": {"DataType": "String", "StringValue": "value1"}, "key2": {"DataType": "String", "StringValue": "value2"}, } tracer = self.tracer_provider.get_tracer("test-tracer") with tracer.start_as_current_span("span"): inject_propagation_context(carrier) self.assertGreater(len(carrier), 2) def test_inject_propagation_context_too_many_attributes(self): carrier = {f"key{idx}": {"DataType": "String", "StringValue": f"value{idx}"} for idx in range(10)} tracer = self.tracer_provider.get_tracer("test-tracer") with tracer.start_as_current_span("span"): inject_propagation_context(carrier) self.assertEqual(10, len(carrier)) test_botocore_secretsmanager.py000066400000000000000000000043551527070072300423610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import botocore.session from moto import mock_aws from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.semconv._incubating.attributes.aws_attributes import ( AWS_SECRETSMANAGER_SECRET_ARN, ) from opentelemetry.test.test_base import TestBase class TestSecretsManagerExtension(TestBase): def setUp(self): super().setUp() BotocoreInstrumentor().instrument() session = botocore.session.get_session() session.set_credentials(access_key="access-key", secret_key="secret-key") self.region = "us-west-2" self.client = session.create_client("secretsmanager", region_name=self.region) def tearDown(self): super().tearDown() BotocoreInstrumentor().uninstrument() def create_secret_and_get_arn(self, name: str = "test-secret") -> str: """ Create a secret in mocked Secrets Manager and return its ARN. """ # Clear spans before creating secret for helper method self.memory_exporter.clear() response = self.client.create_secret(Name=name, SecretString="test-secret-value") return response["ARN"] @mock_aws def test_tag_resource_with_arn(self): secret_arn = self.create_secret_and_get_arn() self.client.tag_resource(SecretId=secret_arn, Tags=[{"Key": "Environment", "Value": "Test"}]) spans = self.memory_exporter.get_finished_spans() assert spans self.assertEqual(len(spans), 2) span = spans[1] # tag_resource span self.assertEqual( span.attributes[AWS_SECRETSMANAGER_SECRET_ARN], secret_arn, ) @mock_aws def test_create_secret(self): secret_name = "test-secret" response = self.client.create_secret(Name=secret_name, SecretString="test-secret-value") secret_arn = response["ARN"] spans = self.memory_exporter.get_finished_spans() assert spans self.assertEqual(len(spans), 1) span = spans[0] # create_secret span # Should capture ARN from response self.assertEqual( span.attributes[AWS_SECRETSMANAGER_SECRET_ARN], secret_arn, ) test_botocore_sfns.py000066400000000000000000000072171527070072300403270ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import botocore.session from moto import mock_aws from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.semconv._incubating.attributes.aws_attributes import ( AWS_STEP_FUNCTIONS_ACTIVITY_ARN, AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN, ) from opentelemetry.test.test_base import TestBase class TestSfnsExtension(TestBase): def setUp(self): super().setUp() BotocoreInstrumentor().instrument() session = botocore.session.get_session() session.set_credentials(access_key="access-key", secret_key="secret-key") self.region = "us-west-2" self.client = session.create_client("stepfunctions", region_name=self.region) def tearDown(self): super().tearDown() BotocoreInstrumentor().uninstrument() SIMPLE_STATE_MACHINE_DEF = { "Comment": "A simple Hello World example", "StartAt": "HelloState", "States": { "HelloState": { "Type": "Pass", "Result": "Hello, Moto!", "End": True, } }, } def create_state_machine_and_get_arn(self, name: str = "TestStateMachine") -> str: """ Create a state machine in mocked Step Functions and return its ARN. """ definition_json = json.dumps(self.SIMPLE_STATE_MACHINE_DEF) role_arn = "arn:aws:iam::123456789012:role/DummyRole" response = self.client.create_state_machine( name=name, definition=definition_json, roleArn=role_arn, ) return response["stateMachineArn"] def create_activity_and_get_arn(self, name: str = "TestActivity") -> str: """ Create an activity in mocked Step Functions and return its ARN. """ response = self.client.create_activity( name=name, ) return response["activityArn"] @mock_aws def test_sfns_create_state_machine(self): state_machine_arn = self.create_state_machine_and_get_arn() spans = self.memory_exporter.get_finished_spans() assert spans self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual( span.attributes[AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN], state_machine_arn, ) @mock_aws def test_sfns_describe_state_machine(self): state_machine_arn = self.create_state_machine_and_get_arn() self.client.describe_state_machine(stateMachineArn=state_machine_arn) spans = self.memory_exporter.get_finished_spans() assert spans self.assertEqual(len(spans), 2) span = spans[1] self.assertEqual( span.attributes[AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN], state_machine_arn, ) @mock_aws def test_sfns_create_activity(self): activity_arn = self.create_activity_and_get_arn() spans = self.memory_exporter.get_finished_spans() assert spans self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual( span.attributes[AWS_STEP_FUNCTIONS_ACTIVITY_ARN], activity_arn, ) @mock_aws def test_sfns_describe_activity(self): activity_arn = self.create_activity_and_get_arn() self.client.describe_activity(activityArn=activity_arn) spans = self.memory_exporter.get_finished_spans() assert spans self.assertEqual(len(spans), 2) span = spans[1] self.assertEqual( span.attributes[AWS_STEP_FUNCTIONS_ACTIVITY_ARN], activity_arn, ) test_botocore_sns.py000066400000000000000000000153171527070072300401610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import contextlib from typing import Any from unittest import mock import botocore.session from botocore.awsrequest import AWSResponse from moto import mock_aws from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.semconv._incubating.attributes import messaging_attributes from opentelemetry.semconv._incubating.attributes.aws_attributes import ( AWS_SNS_TOPIC_ARN, ) from opentelemetry.semconv.trace import ( MessagingDestinationKindValues, SpanAttributes, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind from opentelemetry.trace.span import Span class TestSnsExtension(TestBase): def setUp(self): super().setUp() BotocoreInstrumentor().instrument() session = botocore.session.get_session() session.set_credentials(access_key="access-key", secret_key="secret-key") self.client = session.create_client("sns", region_name="us-west-2") self.topic_name = "my-topic" self.topic_arn = f"arn:aws:sns:us-west-2:123456789012:{self.topic_name}" def tearDown(self): super().tearDown() BotocoreInstrumentor().uninstrument() def _create_topic(self, name: str = None) -> str: if name is None: name = self.topic_name response = self.client.create_topic(Name=name) self.memory_exporter.clear() return response["TopicArn"] @contextlib.contextmanager def _mocked_aws_endpoint(self, response): response_func = self._make_aws_response_func(response) with mock.patch("botocore.endpoint.Endpoint.make_request", new=response_func): yield @staticmethod def _make_aws_response_func(response): def _response_func(*args, **kwargs): return AWSResponse("http://127.0.0.1", 200, {}, "{}"), response return _response_func def assert_span(self, name: str) -> Span: spans = self.memory_exporter.get_finished_spans() self.assertEqual(1, len(spans)) span = spans[0] self.assertEqual(SpanKind.PRODUCER, span.kind) self.assertEqual(name, span.name) self.assertEqual("aws.sns", span.attributes[messaging_attributes.MESSAGING_SYSTEM]) return span def assert_injected_span(self, message_attrs: dict[str, Any], span: Span): # traceparent: --- trace_parent = message_attrs["traceparent"]["StringValue"].split("-") span_context = span.get_span_context() self.assertEqual(span_context.trace_id, int(trace_parent[1], 16)) self.assertEqual(span_context.span_id, int(trace_parent[2], 16)) @mock_aws def test_publish_to_topic_arn(self): self._test_publish_to_arn("TopicArn") @mock_aws def test_publish_to_target_arn(self): self._test_publish_to_arn("TargetArn") def _test_publish_to_arn(self, arg_name: str): target_arn = self._create_topic(self.topic_name) self.client.publish( **{ arg_name: target_arn, "Message": "Hello message", } ) span = self.assert_span(f"{self.topic_name} send") self.assertEqual( MessagingDestinationKindValues.TOPIC.value, span.attributes[SpanAttributes.MESSAGING_DESTINATION_KIND], ) self.assertEqual( self.topic_arn, span.attributes[SpanAttributes.MESSAGING_DESTINATION], ) self.assertEqual( target_arn, span.attributes[SpanAttributes.MESSAGING_DESTINATION_NAME], ) @mock_aws def test_publish_to_phone_number(self): phone_number = "+10000000000" self.client.publish( PhoneNumber=phone_number, Message="Hello SNS", ) span = self.assert_span("phone_number send") self.assertEqual( "phone_number:**", span.attributes[SpanAttributes.MESSAGING_DESTINATION], ) @mock_aws def test_publish_injects_span(self): message_attrs = {} topic_arn = self._create_topic() self.client.publish( TopicArn=topic_arn, Message="Hello Message", MessageAttributes=message_attrs, ) span = self.assert_span(f"{self.topic_name} send") self.assertEqual( topic_arn, span.attributes[AWS_SNS_TOPIC_ARN], ) self.assert_injected_span(message_attrs, span) def test_publish_batch_to_topic(self): topic_arn = f"arn:aws:sns:region:000000000:{self.topic_name}" message1_attrs = {} message2_attrs = {} mock_response = { "Successful": [ {"Id": "1", "MessageId": "11", "SequenceNumber": "1"}, {"Id": "2", "MessageId": "22", "SequenceNumber": "2"}, ], "Failed": [], } # publish_batch not implemented by moto so mock the endpoint instead with self._mocked_aws_endpoint(mock_response): self.client.publish_batch( TopicArn=topic_arn, PublishBatchRequestEntries=[ { "Id": "1", "Message": "Hello message 1", "MessageAttributes": message1_attrs, }, { "Id": "2", "Message": "Hello message 2", "MessageAttributes": message2_attrs, }, ], ) span = self.assert_span(f"{self.topic_name} send") self.assertEqual( MessagingDestinationKindValues.TOPIC.value, span.attributes[SpanAttributes.MESSAGING_DESTINATION_KIND], ) self.assertEqual( topic_arn, span.attributes[AWS_SNS_TOPIC_ARN], ) self.assertEqual( topic_arn, span.attributes[SpanAttributes.MESSAGING_DESTINATION], ) self.assertEqual( topic_arn, span.attributes[SpanAttributes.MESSAGING_DESTINATION_NAME], ) self.assert_injected_span(message1_attrs, span) self.assert_injected_span(message2_attrs, span) @mock_aws def test_create_topic_span(self): _ = self.client.create_topic(Name=self.topic_name) spans = self.memory_exporter.get_finished_spans() self.assertEqual(1, len(spans)) span = spans[0] self.assertEqual(SpanKind.CLIENT, span.kind) self.assertEqual("SNS.CreateTopic", span.name) self.assertEqual( self.topic_arn, span.attributes[AWS_SNS_TOPIC_ARN], ) test_botocore_sqs.py000066400000000000000000000106441527070072300401620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-botocore/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import botocore.session from moto import mock_aws from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.semconv._incubating.attributes import messaging_attributes from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase class TestSqsExtension(TestBase): def setUp(self): super().setUp() BotocoreInstrumentor().instrument() session = botocore.session.get_session() session.set_credentials(access_key="access-key", secret_key="secret-key") self.region = "us-west-2" self.client = session.create_client("sqs", region_name=self.region) def tearDown(self): super().tearDown() BotocoreInstrumentor().uninstrument() @mock_aws def test_sqs_messaging_send_message(self): create_queue_result = self.client.create_queue(QueueName="test_queue_name") queue_url = create_queue_result["QueueUrl"] response = self.client.send_message(QueueUrl=queue_url, MessageBody="content") spans = self.memory_exporter.get_finished_spans() assert spans self.assertEqual(len(spans), 2) span = spans[1] self.assertEqual(span.attributes[messaging_attributes.MESSAGING_SYSTEM], "aws.sqs") self.assertEqual(span.attributes[SpanAttributes.MESSAGING_URL], queue_url) self.assertEqual( span.attributes[SpanAttributes.MESSAGING_DESTINATION], "test_queue_name", ) self.assertEqual( span.attributes[messaging_attributes.MESSAGING_MESSAGE_ID], response["MessageId"], ) @mock_aws def test_sqs_messaging_send_message_batch(self): create_queue_result = self.client.create_queue(QueueName="test_queue_name") queue_url = create_queue_result["QueueUrl"] response = self.client.send_message_batch( QueueUrl=queue_url, Entries=[ {"Id": "1", "MessageBody": "content"}, {"Id": "2", "MessageBody": "content2"}, ], ) spans = self.memory_exporter.get_finished_spans() assert spans self.assertEqual(len(spans), 2) span = spans[1] self.assertEqual(span.attributes["rpc.method"], "SendMessageBatch") self.assertEqual(span.attributes[messaging_attributes.MESSAGING_SYSTEM], "aws.sqs") self.assertEqual(span.attributes[SpanAttributes.MESSAGING_URL], queue_url) self.assertEqual( span.attributes[SpanAttributes.MESSAGING_DESTINATION], "test_queue_name", ) self.assertEqual( span.attributes[messaging_attributes.MESSAGING_MESSAGE_ID], response["Successful"][0]["MessageId"], ) @mock_aws def test_sqs_messaging_receive_message(self): create_queue_result = self.client.create_queue(QueueName="test_queue_name") queue_url = create_queue_result["QueueUrl"] self.client.send_message(QueueUrl=queue_url, MessageBody="content") message_result = self.client.receive_message(QueueUrl=create_queue_result["QueueUrl"]) spans = self.memory_exporter.get_finished_spans() assert spans self.assertEqual(len(spans), 3) span = spans[-1] self.assertEqual(span.attributes["rpc.method"], "ReceiveMessage") self.assertEqual(span.attributes[messaging_attributes.MESSAGING_SYSTEM], "aws.sqs") self.assertEqual(span.attributes[SpanAttributes.MESSAGING_URL], queue_url) self.assertEqual( span.attributes[SpanAttributes.MESSAGING_DESTINATION], "test_queue_name", ) self.assertEqual( span.attributes[messaging_attributes.MESSAGING_MESSAGE_ID], message_result["Messages"][0]["MessageId"], ) @mock_aws def test_sqs_messaging_failed_operation(self): with self.assertRaises(Exception): self.client.send_message(QueueUrl="non-existing", MessageBody="content") spans = self.memory_exporter.get_finished_spans() assert spans self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes["rpc.method"], "SendMessage") self.assertEqual(span.attributes[messaging_attributes.MESSAGING_SYSTEM], "aws.sqs") self.assertEqual(span.attributes[SpanAttributes.MESSAGING_URL], "non-existing") opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-cassandra/000077500000000000000000000000001527070072300331025ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-cassandra/LICENSE000066400000000000000000000261351527070072300341160ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000014001527070072300345050ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-cassandraOpenTelemetry Cassandra Instrumentation ======================================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-cassandra.svg :target: https://pypi.org/project/opentelemetry-instrumentation-cassandra/ Instrumentation for Cassandra (cassandra-driver and scylla-driver libraries). Installation ------------ :: pip install opentelemetry-instrumentation-cassandra References ---------- * `OpenTelemetry Cassandra Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000032761527070072300357470ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-cassandra[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-cassandra" dynamic = ["version"] description = "OpenTelemetry Cassandra instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [] instruments-any = [ "cassandra-driver ~= 3.25", "scylla-driver ~= 3.25", ] [project.entry-points.opentelemetry_instrumentor] cassandra = "opentelemetry.instrumentation.cassandra:CassandraInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-cassandra" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/cassandra/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-cassandra/src/000077500000000000000000000000001527070072300336715ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300365065ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-cassandra/srcinstrumentation/000077500000000000000000000000001527070072300417515ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-cassandra/src/opentelemetrycassandra/000077500000000000000000000000001527070072300437105ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-cassandra/src/opentelemetry/instrumentation__init__.py000066400000000000000000000100251527070072300460170ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-cassandra/src/opentelemetry/instrumentation/cassandra# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Cassandra instrumentation supporting `cassandra-driver`_ and `scylla-driver`_, it can be enabled by using ``CassandraInstrumentor``. .. _cassandra-driver: https://pypi.org/project/cassandra-driver/ .. _scylla-driver: https://pypi.org/project/scylla-driver/ Usage ----- .. code:: python import cassandra.cluster from opentelemetry.instrumentation.cassandra import CassandraInstrumentor CassandraInstrumentor().instrument() cluster = cassandra.cluster.Cluster() session = cluster.connect() rows = session.execute("SELECT * FROM test") API --- """ from collections.abc import Collection from importlib.metadata import PackageNotFoundError, distribution import cassandra.cluster from wrapt import wrap_function_wrapper from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( _get_schema_url_for_signal_types, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _set_db_name, _set_db_statement, _set_db_system, _set_http_net_peer_name_client, ) from opentelemetry.instrumentation.cassandra.package import ( _instruments_any, _instruments_cassandra_driver, _instruments_scylla_driver, ) from opentelemetry.instrumentation.cassandra.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap def _instrument(tracer_provider, include_db_statement=False, sem_conv_opt_in_mode=None): """Instruments the cassandra-driver/scylla-driver module Wraps cassandra.cluster.Session.execute_async(). """ tracer = trace.get_tracer( __name__, __version__, tracer_provider, schema_url=_get_schema_url_for_signal_types([_OpenTelemetryStabilitySignalType.DATABASE]), ) name = "Cassandra" def _traced_execute_async(func, instance, args, kwargs): with tracer.start_as_current_span(name, kind=trace.SpanKind.CLIENT) as span: if span.is_recording(): attrs = {} _set_db_system(attrs, "cassandra", sem_conv_opt_in_mode) _set_db_name(attrs, instance.keyspace, sem_conv_opt_in_mode) _set_http_net_peer_name_client( attrs, instance.cluster.contact_points, sem_conv_opt_in_mode, ) if include_db_statement: query = args[0] _set_db_statement(attrs, str(query), sem_conv_opt_in_mode) span.set_attributes(attrs) response = func(*args, **kwargs) return response wrap_function_wrapper("cassandra.cluster", "Session.execute_async", _traced_execute_async) class CassandraInstrumentor(BaseInstrumentor): def instrumentation_dependencies(self) -> Collection[str]: # Determine which package of cassandra is installed. # Right now there are two packages, cassandra-driver and scylla-driver. # The latter is a fork with additional support for ScyllaDB. try: distribution("cassandra-driver") return (_instruments_cassandra_driver,) except PackageNotFoundError: pass try: distribution("scylla-driver") return (_instruments_scylla_driver,) except PackageNotFoundError: pass return _instruments_any def _instrument(self, **kwargs): _OpenTelemetrySemanticConventionStability._initialize() sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.DATABASE, ) _instrument( tracer_provider=kwargs.get("tracer_provider"), include_db_statement=kwargs.get("include_db_statement"), sem_conv_opt_in_mode=sem_conv_opt_in_mode, ) def _uninstrument(self, **kwargs): unwrap(cassandra.cluster.Session, "execute_async") package.py000066400000000000000000000004761527070072300456640ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-cassandra/src/opentelemetry/instrumentation/cassandra# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments_cassandra_driver = "cassandra-driver ~= 3.25" _instruments_scylla_driver = "scylla-driver ~= 3.25" _instruments = () _instruments_any = (_instruments_cassandra_driver, _instruments_scylla_driver) _semconv_status = "migration" version.py000066400000000000000000000001441527070072300457460ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-cassandra/src/opentelemetry/instrumentation/cassandra# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements-scylla.txt000066400000000000000000000007541527070072300405570ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-cassandraasgiref==3.8.1 pyasyncore==1.0.4 # for python 3.13 (should removed when cassandra-driver replaces asyncore with recommended asyncio) click==8.1.7 Deprecated==1.2.14 geomet==0.2.1.post1 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 python-snappy==0.7.3 PyYAML==6.0.1 scylla-driver==3.26.6 six==1.16.0 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-cassandra test-requirements.txt000066400000000000000000000007321527070072300372660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-cassandraasgiref==3.8.1 cassandra-driver==3.29.0 pyasyncore==1.0.4 # for python 3.13 (should removed when cassandra-driver replaces asyncore with recommended asyncio) click==8.1.7 Deprecated==1.2.14 geomet==0.2.1.post1 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 PyYAML==6.0.1 six==1.16.0 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-cassandra opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-cassandra/tests/000077500000000000000000000000001527070072300342445ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300362640ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-cassandra/teststest_cassandra_integration.py000066400000000000000000000326641527070072300421530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-cassandra/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from importlib.metadata import PackageNotFoundError from unittest import TestCase, mock from unittest.mock import call, patch import cassandra.cluster from wrapt import BoundFunctionWrapper import opentelemetry.instrumentation.cassandra from opentelemetry import trace as trace_api from opentelemetry.instrumentation._semconv import ( _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.cassandra import CassandraInstrumentor from opentelemetry.instrumentation.cassandra.package import ( _instruments_cassandra_driver, _instruments_scylla_driver, ) from opentelemetry.sdk import resources from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_STATEMENT, DB_SYSTEM, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_NAMESPACE, DB_QUERY_TEXT, DB_SYSTEM_NAME, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind def connect_and_execute_query(): cluster = cassandra.cluster.Cluster() cluster._is_setup = True session = cluster.connect() session.cluster = cluster session.keyspace = "test" session._request_init_callbacks = [] query = "SELECT * FROM test" session.execute(query) return cluster, session, query class TestCassandraIntegration(TestBase): def tearDown(self): super().tearDown() with self.disable_logging(): CassandraInstrumentor().uninstrument() @property def _mocked_session(self): return cassandra.cluster.Session(cluster=mock.Mock(), hosts=[]) def test_instrument_uninstrument(self): instrumentation = CassandraInstrumentor() instrumentation.instrument() self.assertTrue(isinstance(cassandra.cluster.Session.execute_async, BoundFunctionWrapper)) instrumentation.uninstrument() self.assertFalse(isinstance(cassandra.cluster.Session.execute_async, BoundFunctionWrapper)) @mock.patch("cassandra.cluster.Cluster.connect") @mock.patch("cassandra.cluster.Session.__init__") @mock.patch("cassandra.cluster.Session._create_response_future") def test_instrumentor(self, mock_create_response_future, mock_session_init, mock_connect): mock_create_response_future.return_value = mock.Mock() mock_session_init.return_value = None mock_connect.return_value = self._mocked_session CassandraInstrumentor().instrument() connect_and_execute_query() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.cassandra) self.assertEqual(span.name, "Cassandra") self.assertEqual(span.kind, SpanKind.CLIENT) # check that no spans are generated after uninstrument CassandraInstrumentor().uninstrument() connect_and_execute_query() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) @mock.patch("cassandra.cluster.Cluster.connect") @mock.patch("cassandra.cluster.Session.__init__") @mock.patch("cassandra.cluster.Session._create_response_future") def test_custom_tracer_provider(self, mock_create_response_future, mock_session_init, mock_connect): mock_create_response_future.return_value = mock.Mock() mock_session_init.return_value = None mock_connect.return_value = self._mocked_session resource = resources.Resource.create({}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result CassandraInstrumentor().instrument(tracer_provider=tracer_provider) connect_and_execute_query() span_list = exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertIs(span.resource, resource) @mock.patch("cassandra.cluster.Cluster.connect") @mock.patch("cassandra.cluster.Session.__init__") @mock.patch("cassandra.cluster.Session._create_response_future") def test_instrument_connection_no_op_tracer_provider( self, mock_create_response_future, mock_session_init, mock_connect ): mock_create_response_future.return_value = mock.Mock() mock_session_init.return_value = None mock_connect.return_value = self._mocked_session tracer_provider = trace_api.NoOpTracerProvider() CassandraInstrumentor().instrument(tracer_provider=tracer_provider) connect_and_execute_query() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) class TestCassandraInstrumentationDependencies(TestCase): """Tests to verify that the correct package is returned by instrumentation_dependencies() depending on which driver is installed. This covers the cases where only cassandra-driver is installed, only scylla-driver is installed, both are installed, and neither is installed. """ @patch("opentelemetry.instrumentation.cassandra.distribution") def test_instrumentation_dependencies_cassandra_driver_installed(self, mock_distribution) -> None: instrumentation = CassandraInstrumentor() def _distribution(name): if name == "cassandra-driver": return raise PackageNotFoundError mock_distribution.side_effect = _distribution package_to_instrument = instrumentation.instrumentation_dependencies() self.assertEqual(mock_distribution.call_count, 1) self.assertEqual( mock_distribution.mock_calls, [ call("cassandra-driver"), ], ) self.assertEqual(package_to_instrument, (_instruments_cassandra_driver,)) @patch("opentelemetry.instrumentation.cassandra.distribution") def test_instrumentation_dependencies_scylla_driver_installed(self, mock_distribution) -> None: instrumentation = CassandraInstrumentor() def _distribution(name): if name == "scylla-driver": return raise PackageNotFoundError mock_distribution.side_effect = _distribution package_to_instrument = instrumentation.instrumentation_dependencies() self.assertEqual(mock_distribution.call_count, 2) self.assertEqual( mock_distribution.mock_calls, [ call("cassandra-driver"), call("scylla-driver"), ], ) self.assertEqual(package_to_instrument, (_instruments_scylla_driver,)) @patch("opentelemetry.instrumentation.cassandra.distribution") def test_instrumentation_dependencies_both_installed(self, mock_distribution) -> None: instrumentation = CassandraInstrumentor() def _distribution(name): # The function returns None here for all names # to simulate both packages being installed return None mock_distribution.side_effect = _distribution package_to_instrument = instrumentation.instrumentation_dependencies() self.assertEqual(mock_distribution.call_count, 1) self.assertEqual(mock_distribution.mock_calls, [call("cassandra-driver")]) self.assertEqual(package_to_instrument, (_instruments_cassandra_driver,)) @patch("opentelemetry.instrumentation.cassandra.distribution") def test_instrumentation_dependencies_none_installed(self, mock_distribution) -> None: instrumentation = CassandraInstrumentor() def _distribution(name): # Function raises PackageNotFoundError # if name is not in the list. We will # raise it for both names to simulate # neither being installed raise PackageNotFoundError mock_distribution.side_effect = _distribution package_to_instrument = instrumentation.instrumentation_dependencies() self.assertEqual(mock_distribution.call_count, 2) self.assertEqual( mock_distribution.mock_calls, [ call("cassandra-driver"), call("scylla-driver"), ], ) self.assertEqual( package_to_instrument, (_instruments_cassandra_driver, _instruments_scylla_driver), ) class TestCassandraSemconvStability(TestBase): def tearDown(self): super().tearDown() with self.disable_logging(): CassandraInstrumentor().uninstrument() _OpenTelemetrySemanticConventionStability._initialized = False @property def _mocked_session(self): return cassandra.cluster.Session(cluster=mock.Mock(), hosts=[]) @mock.patch("cassandra.cluster.Cluster.connect") @mock.patch("cassandra.cluster.Session.__init__") @mock.patch("cassandra.cluster.Session._create_response_future") def test_default_semconv(self, mock_create_response_future, mock_session_init, mock_connect): mock_create_response_future.return_value = mock.Mock() mock_session_init.return_value = None mock_connect.return_value = self._mocked_session CassandraInstrumentor().instrument(include_db_statement=True) connect_and_execute_query() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes[DB_NAME], "test") self.assertEqual(span.attributes[DB_SYSTEM], "cassandra") self.assertEqual(span.attributes[DB_STATEMENT], "SELECT * FROM test") self.assertIn(NET_PEER_NAME, span.attributes) self.assertNotIn(DB_NAMESPACE, span.attributes) self.assertNotIn(DB_SYSTEM_NAME, span.attributes) self.assertNotIn(DB_QUERY_TEXT, span.attributes) self.assertNotIn(SERVER_ADDRESS, span.attributes) self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.11.0", ) @mock.patch("cassandra.cluster.Cluster.connect") @mock.patch("cassandra.cluster.Session.__init__") @mock.patch("cassandra.cluster.Session._create_response_future") def test_new_semconv(self, mock_create_response_future, mock_session_init, mock_connect): mock_create_response_future.return_value = mock.Mock() mock_session_init.return_value = None mock_connect.return_value = self._mocked_session with mock.patch.dict( "os.environ", {"OTEL_SEMCONV_STABILITY_OPT_IN": "database"}, ): _OpenTelemetrySemanticConventionStability._initialized = False CassandraInstrumentor().instrument(include_db_statement=True) connect_and_execute_query() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes[DB_NAMESPACE], "test") self.assertEqual(span.attributes[DB_SYSTEM_NAME], "cassandra") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT * FROM test") self.assertIn(SERVER_ADDRESS, span.attributes) self.assertNotIn(DB_NAME, span.attributes) self.assertNotIn(DB_SYSTEM, span.attributes) self.assertNotIn(DB_STATEMENT, span.attributes) self.assertNotIn(NET_PEER_NAME, span.attributes) self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.25.0", ) _OpenTelemetrySemanticConventionStability._initialized = False @mock.patch("cassandra.cluster.Cluster.connect") @mock.patch("cassandra.cluster.Session.__init__") @mock.patch("cassandra.cluster.Session._create_response_future") def test_dup_semconv(self, mock_create_response_future, mock_session_init, mock_connect): mock_create_response_future.return_value = mock.Mock() mock_session_init.return_value = None mock_connect.return_value = self._mocked_session with mock.patch.dict( "os.environ", {"OTEL_SEMCONV_STABILITY_OPT_IN": "database/dup"}, ): _OpenTelemetrySemanticConventionStability._initialized = False CassandraInstrumentor().instrument(include_db_statement=True) connect_and_execute_query() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes[DB_NAME], "test") self.assertEqual(span.attributes[DB_SYSTEM], "cassandra") self.assertEqual(span.attributes[DB_STATEMENT], "SELECT * FROM test") self.assertIn(NET_PEER_NAME, span.attributes) self.assertEqual(span.attributes[DB_NAMESPACE], "test") self.assertEqual(span.attributes[DB_SYSTEM_NAME], "cassandra") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT * FROM test") self.assertIn(SERVER_ADDRESS, span.attributes) self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.25.0", ) _OpenTelemetrySemanticConventionStability._initialized = False opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/000077500000000000000000000000001527070072300324265ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/LICENSE000066400000000000000000000261351527070072300334420ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/README.rst000066400000000000000000000012651527070072300341210ustar00rootroot00000000000000OpenTelemetry Celery Instrumentation ==================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-celery.svg :target: https://pypi.org/project/opentelemetry-instrumentation-celery/ Instrumentation for Celery. Installation ------------ :: pip install opentelemetry-instrumentation-celery References ---------- * `OpenTelemetry Celery Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031301527070072300352600ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-celery" dynamic = ["version"] description = "OpenTelemetry Celery Instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", ] [project.optional-dependencies] instruments = [ "celery >= 4.0, < 6.0", ] [project.entry-points.opentelemetry_instrumentor] celery = "opentelemetry.instrumentation.celery:CeleryInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-celery" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/celery/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/src/000077500000000000000000000000001527070072300332155ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300360325ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/srcinstrumentation/000077500000000000000000000000001527070072300412755ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetrycelery/000077500000000000000000000000001527070072300425605ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation__init__.py000066400000000000000000000320311527070072300446700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Instrument `celery`_ to trace Celery applications. .. _celery: https://pypi.org/project/celery/ Usage ----- * Start broker backend .. code:: docker run -p 5672:5672 rabbitmq * Run instrumented task .. code:: python from opentelemetry.instrumentation.celery import CeleryInstrumentor from celery import Celery from celery.signals import worker_process_init @worker_process_init.connect(weak=False) def init_celery_tracing(*args, **kwargs): CeleryInstrumentor().instrument() app = Celery("tasks", broker="amqp://localhost") @app.task def add(x, y): return x + y add.delay(42, 50) Configuration ------------- The ``CeleryInstrumentor().instrument()`` method accepts the following arguments: * ``use_span_links`` (bool): When ``True``, Celery task execution spans will be linked to the task creation spans instead of being created as child spans. This provides a looser coupling between spans in distributed systems. Defaults to ``False`` to maintain backward compatibility. Setting up tracing ------------------ When tracing a celery worker process, tracing and instrumentation both must be initialized after the celery worker process is initialized. This is required for any tracing components that might use threading to work correctly such as the BatchSpanProcessor. Celery provides a signal called ``worker_process_init`` that can be used to accomplish this as shown in the example above. API --- """ from __future__ import annotations import logging from collections.abc import Collection, Iterable from timeit import default_timer from billiard import VERSION from billiard.einfo import ExceptionInfo from celery import signals # pylint: disable=no-name-in-module from celery.worker.request import Request # pylint: disable=no-name-in-module from opentelemetry import context as context_api from opentelemetry import trace from opentelemetry.instrumentation.celery import utils from opentelemetry.instrumentation.celery.package import _instruments from opentelemetry.instrumentation.celery.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.metrics import get_meter from opentelemetry.propagate import extract, inject from opentelemetry.propagators.textmap import Getter from opentelemetry.semconv._incubating.attributes.messaging_attributes import ( MESSAGING_MESSAGE_ID, ) from opentelemetry.trace.status import Status, StatusCode if VERSION >= (4, 0, 1): from billiard.einfo import ExceptionWithTraceback else: ExceptionWithTraceback = None logger = logging.getLogger(__name__) # Task operations _TASK_TAG_KEY = "celery.action" _TASK_APPLY_ASYNC = "apply_async" _TASK_RUN = "run" _TASK_RETRY_REASON_KEY = "celery.retry.reason" _TASK_REVOKED_REASON_KEY = "celery.revoked.reason" _TASK_REVOKED_TERMINATED_SIGNAL_KEY = "celery.terminated.signal" _TASK_NAME_KEY = "celery.task_name" class CeleryGetter(Getter[Request]): def get(self, carrier: Request, key: str) -> list[str] | None: value = getattr(carrier, key, None) if value is None: return None # Celery's Context copies all message properties as instance # attributes, including non-string values like timelimit (tuple # of ints). The TextMapPropagator contract requires string # values, so coerce anything that isn't already a string. if isinstance(value, str): return [value] if isinstance(value, Iterable): return [str(v) if not isinstance(v, str) else v for v in value] return [str(value)] def keys(self, carrier: Request) -> list[str]: return [] celery_getter = CeleryGetter() class CeleryInstrumentor(BaseInstrumentor): def __init__(self): super().__init__() if not hasattr(self, "metrics"): self.metrics = None if not hasattr(self, "task_id_to_start_time"): self.task_id_to_start_time = {} def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): # TODO: deprecate this when support for stable semconv is available tracer_provider = kwargs.get("tracer_provider") use_span_links = kwargs.get("use_span_links", False) self._tracer = trace.get_tracer( __name__, __version__, tracer_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) self._use_span_links = use_span_links meter_provider = kwargs.get("meter_provider") meter = get_meter( __name__, __version__, meter_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) self.task_id_to_start_time = {} self.create_celery_metrics(meter) signals.task_prerun.connect(self._trace_prerun, weak=False) signals.task_postrun.connect(self._trace_postrun, weak=False) signals.before_task_publish.connect(self._trace_before_publish, weak=False) signals.after_task_publish.connect(self._trace_after_publish, weak=False) signals.task_failure.connect(self._trace_failure, weak=False) signals.task_retry.connect(self._trace_retry, weak=False) def _uninstrument(self, **kwargs): signals.task_prerun.disconnect(self._trace_prerun) signals.task_postrun.disconnect(self._trace_postrun) signals.before_task_publish.disconnect(self._trace_before_publish) signals.after_task_publish.disconnect(self._trace_after_publish) signals.task_failure.disconnect(self._trace_failure) signals.task_retry.disconnect(self._trace_retry) self.task_id_to_start_time = {} def _trace_prerun(self, *args, **kwargs): task = utils.retrieve_task(kwargs) task_id = utils.retrieve_task_id(kwargs) if task is None or task_id is None: return self.update_task_duration_time(task_id) request = task.request tracectx = extract(request, getter=celery_getter) or None logger.debug("prerun signal start task_id=%s", task_id) operation_name = f"{_TASK_RUN}/{task.name}" if self._use_span_links and tracectx is not None: parent_span_context = trace.get_current_span(tracectx).get_span_context() links = [trace.Link(parent_span_context)] if parent_span_context.is_valid else None span = self._tracer.start_span(operation_name, links=links, kind=trace.SpanKind.CONSUMER) # Don't attach the context when using links to avoid parent-child relationship token = None else: token = context_api.attach(tracectx) if tracectx is not None else None span = self._tracer.start_span(operation_name, context=tracectx, kind=trace.SpanKind.CONSUMER) activation = trace.use_span(span, end_on_exit=True) activation.__enter__() # pylint: disable=unnecessary-dunder-call utils.attach_context(task, task_id, span, activation, token) def _trace_postrun(self, *args, **kwargs): task = utils.retrieve_task(kwargs) task_id = utils.retrieve_task_id(kwargs) if task is None or task_id is None: return logger.debug("postrun signal task_id=%s", task_id) # retrieve and finish the Span ctx = utils.retrieve_context(task, task_id) if ctx is None: logger.warning("no existing span found for task_id=%s", task_id) return span, activation, token = ctx # request context tags if span.is_recording(): span.set_attribute(_TASK_TAG_KEY, _TASK_RUN) utils.set_attributes_from_context(span, kwargs) utils.set_attributes_from_context(span, task.request) span.set_attribute(_TASK_NAME_KEY, task.name) activation.__exit__(None, None, None) utils.detach_context(task, task_id) self.update_task_duration_time(task_id) labels = {"task": task.name, "worker": task.request.hostname} self._record_histograms(task_id, labels) self.task_id_to_start_time.pop(task_id, None) # if the process sending the task is not instrumented # there's no incoming context and no token to detach if token is not None: context_api.detach(token) def _trace_before_publish(self, *args, **kwargs): task = utils.retrieve_task_from_sender(kwargs) task_id = utils.retrieve_task_id_from_message(kwargs) if task_id is None: return if task is None: # task is an anonymous task send using send_task or using canvas workflow # Signatures() to send to a task not in the current processes dependency # tree task_name = kwargs.get("sender", "unknown") else: task_name = task.name operation_name = f"{_TASK_APPLY_ASYNC}/{task_name}" span = self._tracer.start_span(operation_name, kind=trace.SpanKind.PRODUCER) # apply some attributes here because most of the data is not available if span.is_recording(): span.set_attribute(_TASK_TAG_KEY, _TASK_APPLY_ASYNC) span.set_attribute(MESSAGING_MESSAGE_ID, task_id) span.set_attribute(_TASK_NAME_KEY, task_name) utils.set_attributes_from_context(span, kwargs) activation = trace.use_span(span, end_on_exit=True) activation.__enter__() # pylint: disable=unnecessary-dunder-call utils.attach_context(task, task_id, span, activation, None, is_publish=True) headers = kwargs.get("headers") if headers: inject(headers) @staticmethod def _trace_after_publish(*args, **kwargs): task = utils.retrieve_task_from_sender(kwargs) task_id = utils.retrieve_task_id_from_message(kwargs) if task is None or task_id is None: return # retrieve and finish the Span ctx = utils.retrieve_context(task, task_id, is_publish=True) if ctx is None: logger.warning("no existing span found for task_id=%s", task_id) return _, activation, _ = ctx activation.__exit__(None, None, None) # pylint: disable=unnecessary-dunder-call utils.detach_context(task, task_id, is_publish=True) @staticmethod def _trace_failure(*args, **kwargs): task = utils.retrieve_task_from_sender(kwargs) task_id = utils.retrieve_task_id(kwargs) if task is None or task_id is None: return ctx = utils.retrieve_context(task, task_id) if ctx is None: return span, _, _ = ctx if not span.is_recording(): return status_kwargs = {"status_code": StatusCode.ERROR} ex = kwargs.get("einfo") if hasattr(task, "throws") and ex is not None and isinstance(ex.exception, task.throws): return if ex is not None: # Unwrap the actual exception wrapped by billiard's # `ExceptionInfo` and `ExceptionWithTraceback`. if isinstance(ex, ExceptionInfo) and ex.exception is not None: ex = ex.exception if ExceptionWithTraceback is not None and isinstance(ex, ExceptionWithTraceback) and ex.exc is not None: ex = ex.exc status_kwargs["description"] = str(ex) span.record_exception(ex) span.set_status(Status(**status_kwargs)) @staticmethod def _trace_retry(*args, **kwargs): task = utils.retrieve_task_from_sender(kwargs) task_id = utils.retrieve_task_id_from_request(kwargs) reason = utils.retrieve_reason(kwargs) if task is None or task_id is None or reason is None: return ctx = utils.retrieve_context(task, task_id) if ctx is None: return span, _, _ = ctx if not span.is_recording(): return # Add retry reason metadata to span # Use `str(reason)` instead of `reason.message` in case we get # something that isn't an `Exception` span.set_attribute(_TASK_RETRY_REASON_KEY, str(reason)) def update_task_duration_time(self, task_id): cur_time = default_timer() task_duration_time_until_now = ( cur_time - self.task_id_to_start_time[task_id] if task_id in self.task_id_to_start_time else cur_time ) self.task_id_to_start_time[task_id] = task_duration_time_until_now def _record_histograms(self, task_id, metric_attributes): if task_id is None: return self.metrics["flower.task.runtime.seconds"].record( self.task_id_to_start_time.get(task_id), attributes=metric_attributes, ) def create_celery_metrics(self, meter) -> None: self.metrics = { "flower.task.runtime.seconds": meter.create_histogram( name="flower.task.runtime.seconds", unit="seconds", description="The time it took to run the task.", ) } package.py000066400000000000000000000001671527070072300445310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("celery >= 4.0, < 6.0",) utils.py000066400000000000000000000203301527070072300442700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging from collections.abc import Mapping from typing import TYPE_CHECKING, Any, Protocol, cast from celery import registry # pylint: disable=no-name-in-module from celery.app.task import Task from opentelemetry.semconv._incubating.attributes.messaging_attributes import ( MESSAGING_MESSAGE_ID, ) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import Span if TYPE_CHECKING: from contextlib import AbstractContextManager ContextKey = tuple[str, bool] ContextTuple = tuple[Span, "AbstractContextManager[Span]", object | None] ContextDict = dict[ContextKey, ContextTuple] class ContextCarrier(Protocol): def get(self, key: str, default: Any = None) -> Any: ... logger = logging.getLogger(__name__) # Celery Context key CTX_KEY = "__otel_task_span" # Celery Context attributes CELERY_CONTEXT_ATTRIBUTES = ( "compression", "correlation_id", "countdown", "delivery_info", "declare", "eta", "exchange", "expires", "hostname", "id", "priority", "queue", "reply_to", "retries", "routing_key", "serializer", "timelimit", "origin", "state", ) # pylint:disable=too-many-branches def set_attributes_from_context( span: Span, context: ContextCarrier, ) -> None: """Helper to extract meta values from a Celery Context""" if not span.is_recording(): return for key in CELERY_CONTEXT_ATTRIBUTES: value = context.get(key) # Skip this key if it is not set if value is None or value == "": continue # Skip `timelimit` if it is not set (it's default/unset value is a # tuple or a list of `None` values if key == "timelimit": if value in [(None, None), [None, None]]: continue if None in value: value = ["" if tl is None else str(tl) for tl in value] # Skip `retries` if it's value is `0` if key == "retries" and value == 0: continue attribute_name = None # Celery 4.0 uses `origin` instead of `hostname`; this change preserves # the same name for the tag despite Celery version if key == "origin": key = "hostname" elif key == "delivery_info": # Get also destination from this routing_key = value.get("routing_key") if routing_key is not None: span.set_attribute(SpanAttributes.MESSAGING_DESTINATION, routing_key) value = str(value) elif key == "id": attribute_name = MESSAGING_MESSAGE_ID elif key == "correlation_id": attribute_name = SpanAttributes.MESSAGING_CONVERSATION_ID elif key == "routing_key": attribute_name = SpanAttributes.MESSAGING_DESTINATION # according to https://docs.celeryproject.org/en/stable/userguide/routing.html#exchange-types elif key == "declare": attribute_name = SpanAttributes.MESSAGING_DESTINATION_KIND for declare in value: if declare.exchange.type == "direct": value = "queue" break if declare.exchange.type == "topic": value = "topic" break # set attribute name if not set specially for a key if attribute_name is None: attribute_name = f"celery.{key}" span.set_attribute(attribute_name, value) def attach_context( task: Task | None, task_id: str, span: Span, activation: AbstractContextManager[Span], token: object | None, is_publish: bool = False, ) -> None: """Helper to propagate a `Span`, `ContextManager` and context token for the given `Task` instance. This function uses a `dict` that stores the Span using the `(task_id, is_publish)` as a key. This is useful when information must be propagated from one Celery signal to another. We use (task_id, is_publish) for the key to ensure that publishing a task from within another task does not cause any conflicts. This mostly happens when either a task fails and a retry policy is in place, or when a task is manually retries (e.g. `task.retry()`), we end up trying to publish a task with the same id as the task currently running. Previously publishing the new task would overwrite the existing `celery.run` span in the `dict` causing that span to be forgotten and never finished NOTE: We cannot test for this well yet, because we do not run a celery worker, and cannot run `task.apply_async()` """ if task is None: return ctx_dict = cast(ContextDict | None, getattr(task, CTX_KEY, None)) if ctx_dict is None: ctx_dict = {} setattr(task, CTX_KEY, ctx_dict) ctx_dict[(task_id, is_publish)] = (span, activation, token) def detach_context(task: Task | None, task_id: str, is_publish: bool = False) -> None: """Helper to remove `Span`, `ContextManager` and context token in a Celery task when it's propagated. This function handles tasks where no values are attached to the `Task`. """ if task is None: return span_dict = cast(ContextDict | None, getattr(task, CTX_KEY, None)) if span_dict is None: return # See note in `attach_context` for key info span_dict.pop((task_id, is_publish), None) def retrieve_context(task: Task | None, task_id: str, is_publish: bool = False) -> ContextTuple | None: """Helper to retrieve an active `Span`, `ContextManager` and context token stored in a `Task` instance """ if task is None: return None span_dict = cast(ContextDict | None, getattr(task, CTX_KEY, None)) if span_dict is None: return None # See note in `attach_context` for key info return span_dict.get((task_id, is_publish), None) def retrieve_task(kwargs: Mapping[str, Any]) -> Task | None: task = kwargs.get("task") if task is None: logger.debug("Unable to retrieve task from signal arguments") return cast(Task | None, task) def retrieve_task_from_sender(kwargs: Mapping[str, Any]) -> Task | None: sender = kwargs.get("sender") if sender is None: logger.debug("Unable to retrieve the sender from signal arguments") # before and after publish signals sender is the task name # for retry and failure signals sender is the task object if isinstance(sender, str): sender = registry.tasks.get(sender) if sender is None: logger.debug("Unable to retrieve the task from sender=%s", sender) return cast(Task | None, sender) def retrieve_task_id(kwargs: Mapping[str, Any]) -> str | None: task_id = kwargs.get("task_id") if task_id is None: logger.debug("Unable to retrieve task_id from signal arguments") return cast(str | None, task_id) def retrieve_task_id_from_request(kwargs: Mapping[str, Any]) -> str | None: # retry signal does not include task_id as argument so use request argument request = kwargs.get("request") if request is None: logger.debug("Unable to retrieve the request from signal arguments") return None task_id = cast(str | None, getattr(request, "id", None)) if task_id is None: logger.debug("Unable to retrieve the task_id from the request") return task_id def retrieve_task_id_from_message(kwargs: Mapping[str, Any]) -> str | None: """Helper to retrieve the `Task` identifier from the message `body`. This helper supports Protocol Version 1 and 2. The Protocol is well detailed in the official documentation: http://docs.celeryproject.org/en/latest/internals/protocol.html """ headers = kwargs.get("headers") body = kwargs.get("body") if headers is not None and len(headers) > 0: # Protocol Version 2 (default from Celery 4.0) return cast(str | None, headers.get("id")) # Protocol Version 1 if body is None: return None return cast(str | None, body.get("id")) def retrieve_reason(kwargs: Mapping[str, Any]) -> object | None: reason = kwargs.get("reason") if not reason: logger.debug("Unable to retrieve the retry reason") return reason version.py000066400000000000000000000001441527070072300446160ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements-0.txt000066400000000000000000000010161527070072300367430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celeryamqp==5.2.0 asgiref==3.8.1 backports.zoneinfo==0.2.1 billiard==4.2.0 celery==5.3.6 click==8.1.7 click-didyoumean==0.3.0 click-plugins==1.1.1 click-repl==0.3.0 Deprecated==1.2.14 iniconfig==2.0.0 kombu==5.3.5 packaging==24.0 pluggy==1.6.0 prompt-toolkit==3.0.43 py-cpuinfo==9.0.0 pytest==7.4.4 python-dateutil==2.8.2 six==1.16.0 tomli==2.0.1 typing_extensions==4.12.2 tzdata==2024.1 vine==5.1.0 wcwidth==0.2.13 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-celery test-requirements-1.txt000066400000000000000000000007641527070072300367550ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celeryamqp==5.2.0 asgiref==3.8.1 billiard==4.2.1 celery==5.3.6 click==8.1.7 click-didyoumean==0.3.0 click-plugins==1.1.1 click-repl==0.3.0 Deprecated==1.2.14 iniconfig==2.0.0 kombu==5.3.5 packaging==24.0 pluggy==1.6.0 prompt-toolkit==3.0.43 py-cpuinfo==9.0.0 pytest==7.4.4 python-dateutil==2.8.2 six==1.16.0 tomli==2.0.1 typing_extensions==4.12.2 tzdata==2024.1 vine==5.1.0 wcwidth==0.2.13 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-celery opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/tests/000077500000000000000000000000001527070072300335705ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300356100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/testscelery_test_tasks.py000066400000000000000000000010241527070072300376070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from celery import Celery from opentelemetry import baggage class Config: result_backend = "rpc" broker_backend = "memory" app = Celery(broker="memory:///") app.config_from_object(Config) class CustomError(Exception): pass @app.task def task_add(num_a, num_b): return num_a + num_b @app.task def task_raises(): raise CustomError("The task failed!") @app.task def task_returns_baggage(): return dict(baggage.get_all()) test_duplicate.py000066400000000000000000000011611527070072300370730ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from opentelemetry.instrumentation.celery import CeleryInstrumentor class TestUtils(unittest.TestCase): def test_duplicate_instrumentaion(self): first = CeleryInstrumentor() first.instrument() second = CeleryInstrumentor() second.instrument() CeleryInstrumentor().uninstrument() self.assertIsNotNone(first.metrics) self.assertIsNotNone(second.metrics) self.assertEqual(first.task_id_to_start_time, {}) self.assertEqual(second.task_id_to_start_time, {}) test_getter.py000066400000000000000000000051521527070072300364170ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase, mock from opentelemetry.instrumentation.celery import CeleryGetter class TestCeleryGetter(TestCase): def test_get_none(self): """Missing attribute on carrier should return None.""" getter = CeleryGetter() carrier = {} val = getter.get(carrier, "test") self.assertIsNone(val) def test_get_str(self): """String attribute should be wrapped in a single-element list.""" mock_obj = mock.Mock() getter = CeleryGetter() mock_obj.test = "val" val = getter.get(mock_obj, "test") self.assertEqual(val, ["val"]) def test_get_iter(self): """Iterable attribute should be returned as a list.""" mock_obj = mock.Mock() getter = CeleryGetter() mock_obj.test = ["val"] val = getter.get(mock_obj, "test") self.assertEqual(val, ["val"]) def test_get_int(self): """Non-string scalar values should be coerced to strings. Celery's Context stores some attributes as ints (e.g. priority). The TextMapPropagator contract requires string values; passing an int to re.split() in TraceState.from_header() causes a TypeError. """ mock_obj = mock.Mock() getter = CeleryGetter() mock_obj.test = 42 val = getter.get(mock_obj, "test") self.assertEqual(val, ["42"]) def test_get_iter_with_non_string_elements(self): """Iterable values containing non-strings should be coerced. Celery's timelimit attribute is a tuple of ints, e.g. (300, 60). """ mock_obj = mock.Mock() getter = CeleryGetter() mock_obj.test = (300, 60) val = getter.get(mock_obj, "test") self.assertEqual(val, ["300", "60"]) def test_get_iter_with_mixed_types(self): """Iterables with a mix of strings and non-strings.""" mock_obj = mock.Mock() getter = CeleryGetter() mock_obj.test = ["val", 123] val = getter.get(mock_obj, "test") self.assertEqual(val, ["val", "123"]) def test_get_non_str_non_iterable(self): """Non-string, non-iterable value should be coerced to [str(value)].""" getter = CeleryGetter() mock_obj = mock.Mock() mock_obj.key = 42 val = getter.get(mock_obj, "key") self.assertEqual(val, ["42"]) def test_keys(self): """keys() should return an empty list for any carrier.""" getter = CeleryGetter() keys = getter.keys({}) self.assertEqual(keys, []) test_metrics.py000066400000000000000000000054121527070072300365720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import threading import time from platform import python_implementation from timeit import default_timer from pytest import mark from opentelemetry.instrumentation.celery import CeleryInstrumentor from opentelemetry.test.test_base import TestBase from .celery_test_tasks import app, task_add SCOPE = "opentelemetry.instrumentation.celery" class TestMetrics(TestBase): def setUp(self): super().setUp() self._worker = app.Worker(app=app, pool="solo", concurrency=1, hostname="celery@akochavi") self._thread = threading.Thread(target=self._worker.start) self._thread.daemon = True self._thread.start() def tearDown(self): super().tearDown() self._worker.stop() self._thread.join() def get_metrics(self): result = task_add.delay(1, 2) timeout = time.time() + 60 * 1 # 1 minutes from now while not result.ready(): if time.time() > timeout: break time.sleep(0.05) return self.get_sorted_metrics(SCOPE) def test_basic_metric(self): CeleryInstrumentor().instrument() start_time = default_timer() task_runtime_estimated = (default_timer() - start_time) * 1000 metrics = self.get_metrics() CeleryInstrumentor().uninstrument() self.assertEqual(len(metrics), 1) task_runtime = metrics[0] print(task_runtime) self.assertEqual(task_runtime.name, "flower.task.runtime.seconds") self.assert_metric_expected( task_runtime, [ self.create_histogram_data_point( count=1, sum_data_point=task_runtime_estimated, max_data_point=task_runtime_estimated, min_data_point=task_runtime_estimated, attributes={ "task": "tests.celery_test_tasks.task_add", "worker": "celery@akochavi", }, ) ], est_value_delta=200, ) @mark.skipif(python_implementation() == "PyPy", reason="Fails randomly in pypy") def test_metric_uninstrument(self): CeleryInstrumentor().instrument() metrics = self.get_metrics() self.assertEqual( metrics[0].data.data_points[0].bucket_counts[1], 1, ) metrics = self.get_metrics() self.assertEqual( metrics[0].data.data_points[0].bucket_counts[1], 2, ) CeleryInstrumentor().uninstrument() metrics = self.get_metrics() self.assertEqual( metrics[0].data.data_points[0].bucket_counts[1], 2, ) test_tasks.py000066400000000000000000000261741527070072300362610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import threading import time from wrapt import wrap_function_wrapper from opentelemetry import baggage, context from opentelemetry.instrumentation.celery import CeleryInstrumentor, utils from opentelemetry.instrumentation.utils import unwrap from opentelemetry.semconv.attributes.exception_attributes import ( EXCEPTION_MESSAGE, EXCEPTION_STACKTRACE, EXCEPTION_TYPE, ) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind, StatusCode from .celery_test_tasks import ( CustomError, app, task_add, task_raises, task_returns_baggage, ) class TestCeleryInstrumentation(TestBase): def setUp(self): super().setUp() self._worker = app.Worker(app=app, pool="solo", concurrency=1) self._thread = threading.Thread(target=self._worker.start) self._thread.daemon = True self._thread.start() def tearDown(self): super().tearDown() CeleryInstrumentor().uninstrument() self._worker.stop() self._thread.join() CeleryInstrumentor().uninstrument() def test_task(self): CeleryInstrumentor().instrument() result = task_add.delay(1, 2) timeout = time.time() + 60 * 1 # 1 minutes from now while not result.ready(): if time.time() > timeout: break time.sleep(0.05) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 2) consumer, producer = spans self.assertEqual(consumer.name, "run/tests.celery_test_tasks.task_add") self.assertEqual(consumer.kind, SpanKind.CONSUMER) self.assertSpanHasAttributes( consumer, { "celery.action": "run", "celery.state": "SUCCESS", SpanAttributes.MESSAGING_DESTINATION: "celery", "celery.task_name": "tests.celery_test_tasks.task_add", }, ) self.assertEqual(consumer.status.status_code, StatusCode.UNSET) self.assertEqual(0, len(consumer.events)) self.assertEqual(producer.name, "apply_async/tests.celery_test_tasks.task_add") self.assertEqual(producer.kind, SpanKind.PRODUCER) self.assertSpanHasAttributes( producer, { "celery.action": "apply_async", "celery.task_name": "tests.celery_test_tasks.task_add", SpanAttributes.MESSAGING_DESTINATION_KIND: "queue", SpanAttributes.MESSAGING_DESTINATION: "celery", }, ) self.assertNotEqual(consumer.parent, producer.context) self.assertEqual(consumer.parent.span_id, producer.context.span_id) self.assertEqual(consumer.context.trace_id, producer.context.trace_id) def test_task_clears_start_time_cache(self): """Test that the `task_id_to_start_time` cache is cleared after a task finishes, to prevent memory leaks.""" instrumentor = CeleryInstrumentor() instrumentor.instrument() result = task_add.delay(1, 2) timeout = time.time() + 60 * 1 # 1 minutes from now while not result.ready(): if time.time() > timeout: break time.sleep(0.05) self.assertTrue(result.ready()) self.assertEqual(result.result, 3) self.assertEqual(instrumentor.task_id_to_start_time, {}) def test_task_raises(self): CeleryInstrumentor().instrument() result = task_raises.delay() timeout = time.time() + 60 * 1 # 1 minutes from now while not result.ready(): if time.time() > timeout: break time.sleep(0.05) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 2) consumer, producer = spans self.assertEqual(consumer.name, "run/tests.celery_test_tasks.task_raises") self.assertEqual(consumer.kind, SpanKind.CONSUMER) self.assertSpanHasAttributes( consumer, { "celery.action": "run", "celery.state": "FAILURE", SpanAttributes.MESSAGING_DESTINATION: "celery", "celery.task_name": "tests.celery_test_tasks.task_raises", }, ) self.assertEqual(consumer.status.status_code, StatusCode.ERROR) self.assertEqual(1, len(consumer.events)) event = consumer.events[0] self.assertIn(EXCEPTION_STACKTRACE, event.attributes) self.assertEqual( f"{CustomError.__module__}.{CustomError.__qualname__}", event.attributes[EXCEPTION_TYPE], ) self.assertEqual( event.attributes[EXCEPTION_MESSAGE], "The task failed!", ) self.assertEqual(producer.name, "apply_async/tests.celery_test_tasks.task_raises") self.assertEqual(producer.kind, SpanKind.PRODUCER) self.assertSpanHasAttributes( producer, { "celery.action": "apply_async", "celery.task_name": "tests.celery_test_tasks.task_raises", SpanAttributes.MESSAGING_DESTINATION_KIND: "queue", SpanAttributes.MESSAGING_DESTINATION: "celery", }, ) self.assertNotEqual(consumer.parent, producer.context) self.assertEqual(consumer.parent.span_id, producer.context.span_id) self.assertEqual(consumer.context.trace_id, producer.context.trace_id) def test_uninstrument(self): CeleryInstrumentor().instrument() CeleryInstrumentor().uninstrument() result = task_add.delay(1, 2) timeout = time.time() + 60 * 1 # 1 minutes from now while not result.ready(): if time.time() > timeout: break time.sleep(0.05) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_baggage(self): CeleryInstrumentor().instrument() ctx = baggage.set_baggage("key", "value") context.attach(ctx) task = task_returns_baggage.delay() timeout = time.time() + 60 * 1 # 1 minutes from now while not task.ready(): if time.time() > timeout: break time.sleep(0.05) self.assertEqual(task.result, {"key": "value"}) def test_task_not_instrumented_does_not_raise(self): def _retrieve_context_wrapper_none_token(wrapped, instance, args, kwargs): ctx = wrapped(*args, **kwargs) if ctx is None: return ctx span, activation, _ = ctx return span, activation, None wrap_function_wrapper( utils, "retrieve_context", _retrieve_context_wrapper_none_token, ) CeleryInstrumentor().instrument() result = task_add.delay(1, 2) timeout = time.time() + 60 * 1 # 1 minutes from now while not result.ready(): if time.time() > timeout: break time.sleep(0.05) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 2) # TODO: assert we don't have "TypeError: expected an instance of Token, got None" in logs self.assertTrue(result) unwrap(utils, "retrieve_context") def test_task_use_span_links(self): CeleryInstrumentor().instrument(use_span_links=True) result = task_add.delay(1, 2) timeout = time.time() + 60 * 1 # 1 minute from now while not result.ready(): if time.time() > timeout: break time.sleep(0.05) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 2) consumer, producer = spans self.assertEqual(consumer.name, "run/tests.celery_test_tasks.task_add") self.assertEqual(consumer.kind, SpanKind.CONSUMER) self.assertSpanHasAttributes( consumer, { "celery.action": "run", "celery.state": "SUCCESS", SpanAttributes.MESSAGING_DESTINATION: "celery", "celery.task_name": "tests.celery_test_tasks.task_add", }, ) self.assertEqual(consumer.status.status_code, StatusCode.UNSET) self.assertEqual(0, len(consumer.events)) self.assertEqual(producer.name, "apply_async/tests.celery_test_tasks.task_add") self.assertEqual(producer.kind, SpanKind.PRODUCER) self.assertSpanHasAttributes( producer, { "celery.action": "apply_async", "celery.task_name": "tests.celery_test_tasks.task_add", SpanAttributes.MESSAGING_DESTINATION_KIND: "queue", SpanAttributes.MESSAGING_DESTINATION: "celery", }, ) # Verify that consumer span is not a child of producer span when using links self.assertIsNone(consumer.parent) self.assertNotEqual(consumer.context.trace_id, producer.context.trace_id) # Verify that consumer span has a link to the producer span self.assertEqual(len(consumer.links), 1) link = consumer.links[0] self.assertEqual(link.context.span_id, producer.context.span_id) self.assertEqual(link.context.trace_id, producer.context.trace_id) class TestCelerySignatureTask(TestBase): def setUp(self): super().setUp() def start_app(*args, **kwargs): # Add an additional task that will not be registered with parent thread @app.task def hidden_task(num_a): return num_a * 2 self._worker = app.Worker(app=app, pool="solo", concurrency=1) return self._worker.start(*args, **kwargs) self._thread = threading.Thread(target=start_app) self._worker = app.Worker(app=app, pool="solo", concurrency=1) self._thread.daemon = True self._thread.start() def tearDown(self): super().tearDown() self._worker.stop() self._thread.join() CeleryInstrumentor().uninstrument() def test_hidden_task(self): # no-op since already instrumented CeleryInstrumentor().instrument() res = app.signature("tests.test_tasks.hidden_task", (2,)).apply_async() while not res.ready(): time.sleep(0.05) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 2) consumer, producer = spans self.assertEqual(consumer.name, "run/tests.test_tasks.hidden_task") self.assertEqual(consumer.kind, SpanKind.CONSUMER) self.assertEqual(producer.name, "apply_async/tests.test_tasks.hidden_task") self.assertEqual(producer.kind, SpanKind.PRODUCER) self.assertNotEqual(consumer.parent, producer.context) self.assertEqual(consumer.parent.span_id, producer.context.span_id) self.assertEqual(consumer.context.trace_id, producer.context.trace_id) test_utils.py000066400000000000000000000234251527070072300362700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-celery/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from unittest import mock from celery import Celery from opentelemetry import trace as trace_api from opentelemetry.instrumentation.celery import utils from opentelemetry.sdk import trace from opentelemetry.semconv._incubating.attributes.messaging_attributes import ( MESSAGING_MESSAGE_ID, ) from opentelemetry.semconv.trace import SpanAttributes class TestUtils(unittest.TestCase): def setUp(self): self.app = Celery("celery.test_app") def test_set_attributes_from_context(self): # it should extract only relevant keys context = { "correlation_id": "44b7f305", "delivery_info": {"eager": True}, "eta": "soon", "expires": "later", "hostname": "localhost", "id": "44b7f305", "reply_to": "44b7f305", "retries": 4, "timelimit": ("now", "later"), "custom_meta": "custom_value", "routing_key": "celery", } span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) utils.set_attributes_from_context(span, context) self.assertEqual( span.attributes.get(MESSAGING_MESSAGE_ID), "44b7f305", ) self.assertEqual( span.attributes.get(SpanAttributes.MESSAGING_CONVERSATION_ID), "44b7f305", ) self.assertEqual(span.attributes.get(SpanAttributes.MESSAGING_DESTINATION), "celery") self.assertEqual(span.attributes["celery.delivery_info"], str({"eager": True})) self.assertEqual(span.attributes.get("celery.eta"), "soon") self.assertEqual(span.attributes.get("celery.expires"), "later") self.assertEqual(span.attributes.get("celery.hostname"), "localhost") self.assertEqual(span.attributes.get("celery.reply_to"), "44b7f305") self.assertEqual(span.attributes.get("celery.retries"), 4) self.assertEqual(span.attributes.get("celery.timelimit"), ("now", "later")) self.assertNotIn("custom_meta", span.attributes) def test_set_attributes_not_recording(self): # it should extract only relevant keys context = { "correlation_id": "44b7f305", "delivery_info": {"eager": True}, "eta": "soon", "expires": "later", "hostname": "localhost", "id": "44b7f305", "reply_to": "44b7f305", "retries": 4, "timelimit": ("now", "later"), "custom_meta": "custom_value", "routing_key": "celery", } mock_span = mock.Mock() mock_span.is_recording.return_value = False utils.set_attributes_from_context(mock_span, context) self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) def test_set_attributes_partial_timelimit_hard_limit(self): # it should extract only relevant keys context = { "correlation_id": "44b7f305", "delivery_info": {"eager": True}, "eta": "soon", "expires": "later", "hostname": "localhost", "id": "44b7f305", "reply_to": "44b7f305", "retries": 4, "timelimit": ("now", None), "custom_meta": "custom_value", "routing_key": "celery", } span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) utils.set_attributes_from_context(span, context) self.assertEqual(span.attributes.get("celery.timelimit"), ("now", "")) def test_set_attributes_partial_timelimit_soft_limit(self): # it should extract only relevant keys context = { "correlation_id": "44b7f305", "delivery_info": {"eager": True}, "eta": "soon", "expires": "later", "hostname": "localhost", "id": "44b7f305", "reply_to": "44b7f305", "retries": 4, "timelimit": (None, "later"), "custom_meta": "custom_value", "routing_key": "celery", } span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) utils.set_attributes_from_context(span, context) self.assertEqual(span.attributes.get("celery.timelimit"), ("", "later")) def test_set_attributes_from_context_empty_keys(self): # it should not extract empty keys context = { "correlation_id": None, "exchange": "", "timelimit": (None, None), "retries": 0, } span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) utils.set_attributes_from_context(span, context) self.assertEqual(len(span.attributes), 0) # edge case: `timelimit` can also be a list of None values context = { "timelimit": [None, None], } utils.set_attributes_from_context(span, context) self.assertEqual(len(span.attributes), 0) def test_span_propagation(self): # ensure spans getter and setter works properly @self.app.task def fn_task(): return 42 # propagate and retrieve a Span task_id = "7c6731af-9533-40c3-83a9-25b58f0d837f" span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) utils.attach_context(fn_task, task_id, span, mock.Mock(), "") ctx = utils.retrieve_context(fn_task, task_id) self.assertIsNotNone(ctx) span_after, _, _ = ctx self.assertIs(span, span_after) def test_span_delete(self): # ensure the helper removes properly a propagated Span @self.app.task def fn_task(): return 42 # propagate a Span task_id = "7c6731af-9533-40c3-83a9-25b58f0d837f" span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) utils.attach_context(fn_task, task_id, span, mock.Mock(), "") # delete the Span utils.detach_context(fn_task, task_id) self.assertEqual(utils.retrieve_context(fn_task, task_id), None) def test_optional_task_span_attach(self): task_id = "7c6731af-9533-40c3-83a9-25b58f0d837f" span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) # assert this is is a no-aop self.assertIsNone(utils.attach_context(None, task_id, span, mock.Mock(), "")) def test_span_delete_empty(self): # ensure detach_span doesn't raise an exception if span is not present @self.app.task def fn_task(): return 42 # delete the Span task_id = "7c6731af-9533-40c3-83a9-25b58f0d837f" try: utils.detach_context(fn_task, task_id) self.assertEqual(utils.retrieve_context(fn_task, task_id), None) except Exception as ex: # pylint: disable=broad-except self.fail(f"Exception was raised: {ex}") def test_task_id_from_protocol_v1(self): # ensures a `task_id` is properly returned when Protocol v1 is used. # `context` is an example of an emitted Signal with Protocol v1 context = { "body": { "expires": None, "utc": True, "args": ["user"], "chord": None, "callbacks": None, "errbacks": None, "taskset": None, "id": "dffcaec1-dd92-4a1a-b3ab-d6512f4beeb7", "retries": 0, "task": "tests.contrib.celery.test_integration.fn_task_parameters", "timelimit": (None, None), "eta": None, "kwargs": {"force_logout": True}, }, "sender": "tests.contrib.celery.test_integration.fn_task_parameters", "exchange": "celery", "routing_key": "celery", "retry_policy": None, "headers": {}, "properties": {}, } task_id = utils.retrieve_task_id_from_message(context) self.assertEqual(task_id, "dffcaec1-dd92-4a1a-b3ab-d6512f4beeb7") def test_task_id_from_protocol_v2(self): # ensures a `task_id` is properly returned when Protocol v2 is used. # `context` is an example of an emitted Signal with Protocol v2 context = { "body": ( ["user"], {"force_logout": True}, { "chord": None, "callbacks": None, "errbacks": None, "chain": None, }, ), "sender": "tests.contrib.celery.test_integration.fn_task_parameters", "exchange": "", "routing_key": "celery", "retry_policy": None, "headers": { "origin": "gen83744@hostname", "root_id": "7e917b83-4018-431d-9832-73a28e1fb6c0", "expires": None, "shadow": None, "id": "7e917b83-4018-431d-9832-73a28e1fb6c0", "kwargsrepr": "{'force_logout': True}", "lang": "py", "retries": 0, "task": "tests.contrib.celery.test_integration.fn_task_parameters", "group": None, "timelimit": [None, None], "parent_id": None, "argsrepr": "['user']", "eta": None, }, "properties": { "reply_to": "c3054a07-5b28-3855-b18c-1623a24aaeca", "correlation_id": "7e917b83-4018-431d-9832-73a28e1fb6c0", }, } task_id = utils.retrieve_task_id_from_message(context) self.assertEqual(task_id, "7e917b83-4018-431d-9832-73a28e1fb6c0") opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-click/000077500000000000000000000000001527070072300322305ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-click/README.rst000066400000000000000000000011301527070072300337120ustar00rootroot00000000000000OpenTelemetry click Instrumentation ==================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-click.svg :target: https://pypi.org/project/opentelemetry-instrumentation-click/ This library allows tracing requests made by the click library. Installation ------------ :: pip install opentelemetry-instrumentation-click References ---------- * `OpenTelemetry click/ Tracing `_ * `OpenTelemetry Project `_ pyproject.toml000066400000000000000000000031531527070072300350670ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-click[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-click" dynamic = ["version"] description = "Click instrumentation for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [ "click >= 8.1.3, < 9.0.0", ] [project.entry-points.opentelemetry_instrumentor] click = "opentelemetry.instrumentation.click:ClickInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-click" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/click/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-click/src/000077500000000000000000000000001527070072300330175ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300356345ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-click/srcinstrumentation/000077500000000000000000000000001527070072300410775ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-click/src/opentelemetryclick/000077500000000000000000000000001527070072300421645ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation__init__.py000066400000000000000000000071161527070072300443020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Instrument `click`_ CLI applications. The instrumentor will avoid instrumenting well-known servers (e.g. *flask run* and *uvicorn*) to avoid unexpected effects like every request having the same Trace ID. .. _click: https://pypi.org/project/click/ Usage ----- .. code-block:: python import click from opentelemetry.instrumentation.click import ClickInstrumentor ClickInstrumentor().instrument() @click.command() def hello(): click.echo(f"Hello world!") if __name__ == "__main__": hello() API --- """ import os import sys from collections.abc import Collection from functools import partial from logging import getLogger import click from wrapt import wrap_function_wrapper try: from flask.cli import ScriptInfo as FlaskScriptInfo except ImportError: FlaskScriptInfo = None from opentelemetry import trace from opentelemetry.instrumentation.click.package import _instruments from opentelemetry.instrumentation.click.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import ( unwrap, ) from opentelemetry.semconv._incubating.attributes.process_attributes import ( PROCESS_COMMAND_ARGS, PROCESS_EXECUTABLE_NAME, PROCESS_EXIT_CODE, PROCESS_PID, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.trace.status import StatusCode _logger = getLogger(__name__) def _skip_servers(ctx: click.Context): # flask run if ctx.info_name == "run" and FlaskScriptInfo and isinstance(ctx.obj, FlaskScriptInfo): return True # uvicorn if ctx.info_name == "uvicorn": return True return False def _command_invoke_wrapper(wrapped, instance, args, kwargs, tracer): # Subclasses of Command include groups and CLI runners, but # we only want to instrument the actual commands which are # instances of Command itself. if instance.__class__ != click.Command: return wrapped(*args, **kwargs) ctx = args[0] # we don't want to create a root span for long running processes like servers # otherwise all requests would have the same trace id if _skip_servers(ctx): return wrapped(*args, **kwargs) span_name = ctx.info_name span_attributes = { PROCESS_COMMAND_ARGS: sys.argv, PROCESS_EXECUTABLE_NAME: sys.argv[0], PROCESS_EXIT_CODE: 0, PROCESS_PID: os.getpid(), } with tracer.start_as_current_span( name=span_name, kind=trace.SpanKind.INTERNAL, attributes=span_attributes, ) as span: try: return wrapped(*args, **kwargs) except Exception as exc: span.set_status(StatusCode.ERROR, str(exc)) if span.is_recording(): span.set_attribute(ERROR_TYPE, exc.__class__.__qualname__) span.set_attribute(PROCESS_EXIT_CODE, getattr(exc, "exit_code", 1)) raise class ClickInstrumentor(BaseInstrumentor): """An instrumentor for click""" def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): tracer = trace.get_tracer( __name__, __version__, kwargs.get("tracer_provider"), ) wrap_function_wrapper( click.core.Command, "invoke", partial(_command_invoke_wrapper, tracer=tracer), ) def _uninstrument(self, **kwargs): unwrap(click.core.Command, "invoke") package.py000066400000000000000000000001721527070072300441310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("click >= 8.1.3, < 9.0.0",) version.py000066400000000000000000000001441527070072300442220ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000010341527070072300364100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-clickasgiref==3.8.1 blinker==1.7.0 click==8.1.7 Deprecated==1.2.14 Flask==3.0.2 iniconfig==2.0.0 itsdangerous==2.1.2 Jinja2==3.1.6 MarkupSafe==2.1.2 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 pytest-asyncio==0.23.5 tomli==2.0.1 typing_extensions==4.12.2 Werkzeug==3.0.6 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-click -e instrumentation/opentelemetry-instrumentation-flask -e instrumentation/opentelemetry-instrumentation-wsgi -e util/opentelemetry-util-http opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-click/tests/000077500000000000000000000000001527070072300333725ustar00rootroot00000000000000test_click.py000066400000000000000000000137551527070072300360240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-click/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os from unittest import mock import click import pytest from click.testing import CliRunner try: from flask import cli as flask_cli except ImportError: flask_cli = None from opentelemetry.instrumentation.click import ClickInstrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind from opentelemetry.trace.status import StatusCode class ClickTestCase(TestBase): # pylint: disable=unbalanced-tuple-unpacking def setUp(self): super().setUp() ClickInstrumentor().instrument() def tearDown(self): super().tearDown() ClickInstrumentor().uninstrument() @mock.patch("sys.argv", ["command.py"]) def test_cli_command_wrapping(self): @click.command() def command(): pass runner = CliRunner() result = runner.invoke(command) self.assertEqual(result.exit_code, 0) (span,) = self.memory_exporter.get_finished_spans() self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.kind, SpanKind.INTERNAL) self.assertEqual(span.name, "command") self.assertEqual( dict(span.attributes), { "process.executable.name": "command.py", "process.command_args": ("command.py",), "process.exit.code": 0, "process.pid": os.getpid(), }, ) @mock.patch("sys.argv", ["flask", "command"]) def test_flask_command_wrapping(self): @click.command() def command(): pass runner = CliRunner() result = runner.invoke(command) self.assertEqual(result.exit_code, 0) (span,) = self.memory_exporter.get_finished_spans() self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.kind, SpanKind.INTERNAL) self.assertEqual(span.name, "command") self.assertEqual( dict(span.attributes), { "process.executable.name": "flask", "process.command_args": ( "flask", "command", ), "process.exit.code": 0, "process.pid": os.getpid(), }, ) @mock.patch("sys.argv", ["command.py"]) def test_cli_command_wrapping_with_name(self): @click.command("mycommand") def renamedcommand(): pass runner = CliRunner() result = runner.invoke(renamedcommand) self.assertEqual(result.exit_code, 0) (span,) = self.memory_exporter.get_finished_spans() self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.kind, SpanKind.INTERNAL) self.assertEqual(span.name, "mycommand") self.assertEqual( dict(span.attributes), { "process.executable.name": "command.py", "process.command_args": ("command.py",), "process.exit.code": 0, "process.pid": os.getpid(), }, ) @mock.patch("sys.argv", ["command.py", "--opt", "argument"]) def test_cli_command_wrapping_with_options(self): @click.command() @click.argument("argument") @click.option("--opt/--no-opt", default=False) def command(argument, opt): pass argv = ["command.py", "--opt", "argument"] runner = CliRunner() result = runner.invoke(command, argv[1:]) self.assertEqual(result.exit_code, 0) (span,) = self.memory_exporter.get_finished_spans() self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.kind, SpanKind.INTERNAL) self.assertEqual(span.name, "command") self.assertEqual( dict(span.attributes), { "process.executable.name": "command.py", "process.command_args": tuple(argv), "process.exit.code": 0, "process.pid": os.getpid(), }, ) @mock.patch("sys.argv", ["command-raises.py"]) def test_cli_command_raises_error(self): @click.command() def command_raises(): raise ValueError() runner = CliRunner() result = runner.invoke(command_raises) self.assertEqual(result.exit_code, 1) (span,) = self.memory_exporter.get_finished_spans() self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(span.kind, SpanKind.INTERNAL) self.assertEqual(span.name, "command-raises") self.assertEqual( dict(span.attributes), { "process.executable.name": "command-raises.py", "process.command_args": ("command-raises.py",), "process.exit.code": 1, "process.pid": os.getpid(), "error.type": "ValueError", }, ) def test_uvicorn_cli_command_ignored(self): @click.command("uvicorn") def command_uvicorn(): pass runner = CliRunner() result = runner.invoke(command_uvicorn) self.assertEqual(result.exit_code, 0) self.assertFalse(self.memory_exporter.get_finished_spans()) @pytest.mark.skipif(flask_cli is None, reason="requires flask") def test_flask_run_command_ignored(self): runner = CliRunner() result = runner.invoke(flask_cli.run_command, obj=flask_cli.ScriptInfo()) self.assertEqual(result.exit_code, 2) self.assertFalse(self.memory_exporter.get_finished_spans()) def test_uninstrument(self): ClickInstrumentor().uninstrument() @click.command() def notracecommand(): pass runner = CliRunner() result = runner.invoke(notracecommand) self.assertEqual(result.exit_code, 0) self.assertFalse(self.memory_exporter.get_finished_spans()) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-confluent-kafka/000077500000000000000000000000001527070072300342135ustar00rootroot00000000000000LICENSE000066400000000000000000000261351527070072300351500ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-confluent-kafka Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000012601527070072300356220ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-confluent-kafkaOpenTelemetry confluent-kafka Instrumentation ============================================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-confluent-kafka.svg :target: https://pypi.org/project/opentelemetry-instrumentation-confluent-kafka/ This library allows tracing requests made by the confluent-kafka library. Installation ------------ :: pip install opentelemetry-instrumentation-confluent-kafka References ---------- * `OpenTelemetry confluent-kafka/ Tracing `_ * `OpenTelemetry Project `_ pyproject.toml000066400000000000000000000032641527070072300370550ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-confluent-kafka[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-confluent-kafka" dynamic = ["version"] description = "OpenTelemetry Confluent Kafka instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-api ~= 1.12", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = ["confluent-kafka >= 1.8.2, < 3.0.0"] [project.entry-points.opentelemetry_instrumentor] confluent_kafka = "opentelemetry.instrumentation.confluent_kafka:ConfluentKafkaInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-confluent-kafka" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/confluent_kafka/version.py" [tool.hatch.build.targets.sdist] include = ["/src", "/tests"] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] src/000077500000000000000000000000001527070072300347235ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-confluent-kafkaopentelemetry/000077500000000000000000000000001527070072300376175ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-confluent-kafka/srcinstrumentation/000077500000000000000000000000001527070072300430625ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetryconfluent_kafka/000077500000000000000000000000001527070072300462145ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation__init__.py000066400000000000000000000360301527070072300503270ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Instrument confluent-kafka-python to report instrumentation-confluent-kafka produced and consumed messages Usage ----- .. code:: python from opentelemetry.instrumentation.confluent_kafka import ConfluentKafkaInstrumentor from confluent_kafka import Producer, Consumer # Instrument kafka ConfluentKafkaInstrumentor().instrument() # report a span of type producer with the default settings conf1 = {'bootstrap.servers': "localhost:9092"} producer = Producer(conf1) producer.produce('my-topic',b'raw_bytes') conf2 = {'bootstrap.servers': "localhost:9092", 'group.id': "foo", 'auto.offset.reset': 'smallest'} # report a span of type consumer with the default settings consumer = Consumer(conf2) def msg_process(msg): print(msg) def basic_consume_loop(consumer, topics): try: consumer.subscribe(topics) running = True while running: msg = consumer.poll(timeout=1.0) if msg is None: continue if msg.error(): if msg.error().code() == KafkaError._PARTITION_EOF: # End of partition event sys.stderr.write(f"{msg.topic()} [{msg.partition()}] reached end at offset {msg.offset()}") elif msg.error(): raise KafkaException(msg.error()) else: msg_process(msg) finally: # Close down consumer to commit final offsets. consumer.close() basic_consume_loop(consumer, ["my-topic"]) The ``_instrument()`` method accepts the following keyword args: - **tracer_provider** (TracerProvider) - an optional tracer provider - **instrument_producer** (Callable) - a function with extra user-defined logic to be performed before sending the message Function signature: .. code:: python def instrument_producer(producer: Producer, tracer_provider=None): ... - **instrument_consumer** (Callable) - a function with extra user-defined logic to be performed after consuming a message Function signature: .. code:: python def instrument_consumer(consumer: Consumer, tracer_provider=None): ... For example: .. code:: python from opentelemetry.instrumentation.confluent_kafka import ConfluentKafkaInstrumentor from opentelemetry.trace import get_tracer_provider from confluent_kafka import Producer, Consumer inst = ConfluentKafkaInstrumentor() tracer_provider = get_tracer_provider() p = Producer({'bootstrap.servers': 'localhost:9092'}) c = Consumer({ 'bootstrap.servers': 'localhost:9092', 'group.id': 'mygroup', 'auto.offset.reset': 'earliest' }) # instrument confluent kafka with produce and consume hooks p = inst.instrument_producer(p, tracer_provider) c = inst.instrument_consumer(c, tracer_provider=tracer_provider) # Using kafka as normal now will automatically generate spans, # including user custom attributes added from the hooks conf = {'bootstrap.servers': "localhost:9092"} p.produce('my-topic',b'raw_bytes') msg = c.poll() """ from collections.abc import Collection import confluent_kafka import wrapt from confluent_kafka import Consumer, Producer from opentelemetry import context, propagate, trace from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap from opentelemetry.semconv._incubating.attributes.messaging_attributes import ( MessagingOperationTypeValues, ) from opentelemetry.trace import Tracer from .package import _instruments from .utils import ( KafkaPropertiesExtractor, _create_new_consume_span, _end_current_consume_span, _enrich_span, _get_span_name, _kafka_setter, ) from .version import __version__ def _capture_config(args, kwargs): """Return the config dict that was passed to a Producer/Consumer constructor, regardless of whether it was supplied positionally, as ``conf=`` kwarg, or (for Consumer) expanded as **kwargs.""" if args and isinstance(args[0], dict): return args[0] conf = kwargs.get("conf") if isinstance(conf, dict): return conf # confluent_kafka.Consumer also supports Consumer(**conf) — in that case # the kwargs themselves are the config. if kwargs: return dict(kwargs) return None class AutoInstrumentedProducer(Producer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.config = _capture_config(args, kwargs) # This method is deliberately implemented in order to allow wrapt to wrap this function def produce(self, topic, value=None, *args, **kwargs): # pylint: disable=keyword-arg-before-vararg,useless-super-delegation super().produce(topic, value, *args, **kwargs) class AutoInstrumentedConsumer(Consumer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.config = _capture_config(args, kwargs) self._current_consume_span = None # This method is deliberately implemented in order to allow wrapt to wrap this function def poll(self, timeout=-1): # pylint: disable=useless-super-delegation return super().poll(timeout) # This method is deliberately implemented in order to allow wrapt to wrap this function def consume(self, *args, **kwargs): # pylint: disable=useless-super-delegation return super().consume(*args, **kwargs) # This method is deliberately implemented in order to allow wrapt to wrap this function def close(self): # pylint: disable=useless-super-delegation return super().close() class ProxiedProducer(Producer): def __init__(self, producer: Producer, tracer: Tracer): self._producer = producer self._tracer = tracer # Surface the wrapped producer's config (if any) so that # KafkaPropertiesExtractor.extract_bootstrap_servers can read it # through this proxy. self.config = getattr(producer, "config", None) def flush(self, timeout=-1): return self._producer.flush(timeout) def poll(self, timeout=-1): return self._producer.poll(timeout) def purge(self, in_queue=True, in_flight=True, blocking=True): self._producer.purge(in_queue, in_flight, blocking) def produce(self, topic, value=None, *args, **kwargs): # pylint: disable=keyword-arg-before-vararg new_kwargs = kwargs.copy() new_kwargs["topic"] = topic new_kwargs["value"] = value return ConfluentKafkaInstrumentor.wrap_produce(self._producer.produce, self, self._tracer, args, new_kwargs) def original_producer(self): return self._producer class ProxiedConsumer(Consumer): def __init__(self, consumer: Consumer, tracer: Tracer): self._consumer = consumer self._tracer = tracer self._current_consume_span = None self._current_context_token = None # See ProxiedProducer.__init__ for rationale. self.config = getattr(consumer, "config", None) def close(self, *args, **kwargs): return ConfluentKafkaInstrumentor.wrap_close(self._consumer.close, self, args, kwargs) def committed(self, partitions, timeout=-1): return self._consumer.committed(partitions, timeout) def commit(self, *args, **kwargs): return self._consumer.commit(*args, **kwargs) def consume(self, *args, **kwargs): return ConfluentKafkaInstrumentor.wrap_consume( self._consumer.consume, self, self._tracer, args, kwargs, ) def get_watermark_offsets(self, partition, timeout=-1, *args, **kwargs): # pylint: disable=keyword-arg-before-vararg return self._consumer.get_watermark_offsets(partition, timeout, *args, **kwargs) def offsets_for_times(self, partitions, timeout=-1): return self._consumer.offsets_for_times(partitions, timeout) def poll(self, timeout=-1): return ConfluentKafkaInstrumentor.wrap_poll(self._consumer.poll, self, self._tracer, [timeout], {}) def subscribe(self, topics, on_assign=lambda *args: None, *args, **kwargs): # pylint: disable=keyword-arg-before-vararg self._consumer.subscribe(topics, on_assign, *args, **kwargs) def original_consumer(self): return self._consumer class ConfluentKafkaInstrumentor(BaseInstrumentor): """An instrumentor for confluent kafka module See `BaseInstrumentor` """ @staticmethod def instrument_producer(producer: Producer, tracer_provider=None) -> ProxiedProducer: tracer = trace.get_tracer( __name__, __version__, tracer_provider=tracer_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) manual_producer = ProxiedProducer(producer, tracer) return manual_producer @staticmethod def instrument_consumer(consumer: Consumer, tracer_provider=None) -> ProxiedConsumer: tracer = trace.get_tracer( __name__, __version__, tracer_provider=tracer_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) manual_consumer = ProxiedConsumer(consumer, tracer) return manual_consumer @staticmethod def uninstrument_producer(producer: Producer) -> Producer: if isinstance(producer, ProxiedProducer): return producer.original_producer() return producer @staticmethod def uninstrument_consumer(consumer: Consumer) -> Consumer: if isinstance(consumer, ProxiedConsumer): return consumer.original_consumer() return consumer def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): # TODO: should probably wrap methods directly instead of going through # these classes. Hopefully it'll make the patching work if called after # the original classes have already been imported, #4270 self._original_kafka_producer = confluent_kafka.Producer self._original_kafka_consumer = confluent_kafka.Consumer confluent_kafka.Producer = AutoInstrumentedProducer confluent_kafka.Consumer = AutoInstrumentedConsumer tracer_provider = kwargs.get("tracer_provider") tracer = trace.get_tracer( __name__, __version__, tracer_provider=tracer_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) self._tracer = tracer def _inner_wrap_produce(func, instance, args, kwargs): return ConfluentKafkaInstrumentor.wrap_produce(func, instance, self._tracer, args, kwargs) def _inner_wrap_poll(func, instance, args, kwargs): return ConfluentKafkaInstrumentor.wrap_poll(func, instance, self._tracer, args, kwargs) def _inner_wrap_consume(func, instance, args, kwargs): return ConfluentKafkaInstrumentor.wrap_consume(func, instance, self._tracer, args, kwargs) def _inner_wrap_close(func, instance, args, kwargs): return ConfluentKafkaInstrumentor.wrap_close(func, instance, args, kwargs) wrapt.wrap_function_wrapper( AutoInstrumentedProducer, "produce", _inner_wrap_produce, ) wrapt.wrap_function_wrapper( AutoInstrumentedConsumer, "poll", _inner_wrap_poll, ) wrapt.wrap_function_wrapper( AutoInstrumentedConsumer, "consume", _inner_wrap_consume, ) wrapt.wrap_function_wrapper( AutoInstrumentedConsumer, "close", _inner_wrap_close, ) def _uninstrument(self, **kwargs): confluent_kafka.Producer = self._original_kafka_producer confluent_kafka.Consumer = self._original_kafka_consumer unwrap(AutoInstrumentedProducer, "produce") unwrap(AutoInstrumentedConsumer, "poll") @staticmethod def wrap_produce(func, instance, tracer, args, kwargs): topic = kwargs.get("topic") if not topic: topic = args[0] span_name = _get_span_name("send", topic) with tracer.start_as_current_span(name=span_name, kind=trace.SpanKind.PRODUCER) as span: headers = KafkaPropertiesExtractor.extract_produce_headers(args, kwargs) if headers is None: headers = [] kwargs["headers"] = headers topic = KafkaPropertiesExtractor.extract_produce_topic(args, kwargs) bootstrap_servers = KafkaPropertiesExtractor.extract_bootstrap_servers(instance) _enrich_span( span, topic, operation=MessagingOperationTypeValues.PUBLISH, bootstrap_servers=bootstrap_servers, ) # Publish propagate.inject( headers, setter=_kafka_setter, ) return func(*args, **kwargs) @staticmethod def wrap_poll(func, instance, tracer, args, kwargs): if instance._current_consume_span: _end_current_consume_span(instance) record = func(*args, **kwargs) if record: bootstrap_servers = KafkaPropertiesExtractor.extract_bootstrap_servers(instance) with tracer.start_as_current_span("recv", end_on_exit=True, kind=trace.SpanKind.CONSUMER): _create_new_consume_span(instance, tracer, [record]) _enrich_span( instance._current_consume_span, record.topic(), record.partition(), record.offset(), operation=MessagingOperationTypeValues.PROCESS, bootstrap_servers=bootstrap_servers, ) instance._current_context_token = context.attach(trace.set_span_in_context(instance._current_consume_span)) return record @staticmethod def wrap_consume(func, instance, tracer, args, kwargs): if instance._current_consume_span: _end_current_consume_span(instance) records = func(*args, **kwargs) if len(records) > 0: bootstrap_servers = KafkaPropertiesExtractor.extract_bootstrap_servers(instance) with tracer.start_as_current_span("recv", end_on_exit=True, kind=trace.SpanKind.CONSUMER): _create_new_consume_span(instance, tracer, records) _enrich_span( instance._current_consume_span, records[0].topic(), operation=MessagingOperationTypeValues.PROCESS, bootstrap_servers=bootstrap_servers, ) instance._current_context_token = context.attach(trace.set_span_in_context(instance._current_consume_span)) return records @staticmethod def wrap_close(func, instance, args, kwargs): if instance._current_consume_span: _end_current_consume_span(instance) func(*args, **kwargs) package.py000066400000000000000000000002041527070072300501550ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("confluent-kafka >= 1.8.2, < 3.0.0",) utils.py000066400000000000000000000136721527070072300477370ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from logging import getLogger from opentelemetry import context, propagate from opentelemetry.propagators import textmap from opentelemetry.semconv._incubating.attributes.messaging_attributes import ( MESSAGING_MESSAGE_ID, MESSAGING_OPERATION, MESSAGING_SYSTEM, MessagingOperationTypeValues, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.semconv.trace import ( MessagingDestinationKindValues, SpanAttributes, ) from opentelemetry.trace import Link, SpanKind _LOG = getLogger(__name__) class KafkaPropertiesExtractor: @staticmethod def extract_bootstrap_servers(instance): config = getattr(instance, "config", None) if not isinstance(config, dict): return None # confluent-kafka uses the dotted key "bootstrap.servers"; also accept # the python-style "bootstrap_servers" for robustness. servers = config.get("bootstrap.servers") or config.get("bootstrap_servers") if isinstance(servers, (list, tuple)): servers = ",".join(str(s) for s in servers) return servers @staticmethod def _extract_argument(key, position, default_value, args, kwargs): if len(args) > position: return args[position] return kwargs.get(key, default_value) @staticmethod def extract_produce_topic(args, kwargs): """extract topic from `produce` method arguments in Producer class""" return kwargs.get("topic") or (args[0] if args else "unknown") @staticmethod def extract_produce_headers(args, kwargs): """extract headers from `produce` method arguments in Producer class""" return KafkaPropertiesExtractor._extract_argument("headers", 6, None, args, kwargs) class KafkaContextGetter(textmap.Getter): def get(self, carrier: textmap.CarrierT, key: str) -> list[str] | None: if carrier is None: return None carrier_items = carrier if isinstance(carrier, dict): carrier_items = carrier.items() for item_key, value in carrier_items: if item_key == key: if value is not None: return [value.decode()] return None def keys(self, carrier: textmap.CarrierT) -> list[str]: if carrier is None: return [] carrier_items = carrier if isinstance(carrier, dict): carrier_items = carrier.items() return [key for (key, value) in carrier_items] class KafkaContextSetter(textmap.Setter): def set(self, carrier: textmap.CarrierT, key: str, value: str) -> None: if carrier is None or key is None: return if value: value = value.encode() if isinstance(carrier, list): carrier.append((key, value)) if isinstance(carrier, dict): carrier[key] = value _kafka_getter = KafkaContextGetter() def _end_current_consume_span(instance): if instance._current_context_token: context.detach(instance._current_context_token) instance._current_context_token = None instance._current_consume_span.end() instance._current_consume_span = None def _create_new_consume_span(instance, tracer, records): links = _get_links_from_records(records) instance._current_consume_span = tracer.start_span( name=f"{records[0].topic()} process", links=links, kind=SpanKind.CONSUMER, ) def _get_links_from_records(records): links = [] for record in records: ctx = propagate.extract(record.headers(), getter=_kafka_getter) if ctx: for item in ctx.values(): if hasattr(item, "get_span_context"): links.append(Link(context=item.get_span_context())) return links def _set_bootstrap_servers_attributes(span, bootstrap_servers): """Populate server.address and server.port from a bootstrap.servers string (e.g. ``host1:9092,host2:9092``).""" if not bootstrap_servers: return first_broker = bootstrap_servers.split(",")[0].strip() if not first_broker: return if ":" in first_broker: host, _, port = first_broker.rpartition(":") span.set_attribute(SERVER_ADDRESS, host) try: span.set_attribute(SERVER_PORT, int(port)) except ValueError: # Port wasn't numeric; skip rather than emit a bad attribute. _LOG.debug("non-numeric port in bootstrap.servers: %r", port) else: span.set_attribute(SERVER_ADDRESS, first_broker) def _enrich_span( span, topic, partition: int | None = None, offset: int | None = None, operation: MessagingOperationTypeValues | None = None, bootstrap_servers: str | None = None, ): if not span.is_recording(): return span.set_attribute(MESSAGING_SYSTEM, "kafka") span.set_attribute(SpanAttributes.MESSAGING_DESTINATION, topic) if partition is not None: span.set_attribute(SpanAttributes.MESSAGING_KAFKA_PARTITION, partition) span.set_attribute( SpanAttributes.MESSAGING_DESTINATION_KIND, MessagingDestinationKindValues.QUEUE.value, ) if operation: span.set_attribute(MESSAGING_OPERATION, operation.value) else: span.set_attribute(SpanAttributes.MESSAGING_TEMP_DESTINATION, True) _set_bootstrap_servers_attributes(span, bootstrap_servers) # https://stackoverflow.com/questions/65935155/identify-and-find-specific-message-in-kafka-topic # A message within Kafka is uniquely defined by its topic name, topic partition and offset. if partition is not None and offset is not None and topic: span.set_attribute( MESSAGING_MESSAGE_ID, f"{topic}.{partition}.{offset}", ) _kafka_setter = KafkaContextSetter() def _get_span_name(operation: str, topic: str): return f"{topic} {operation}" version.py000066400000000000000000000001441527070072300502520ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000004551527070072300404010ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-confluent-kafkaasgiref==3.8.1 confluent-kafka==2.13.2 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-confluent-kafka tests/000077500000000000000000000000001527070072300352765ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-confluent-kafka__init__.py000066400000000000000000000000001527070072300373750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-confluent-kafka/teststest_instrumentation.py000066400000000000000000000425311527070072300421570ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=no-name-in-module,import-outside-toplevel from opentelemetry.instrumentation.confluent_kafka import ( AutoInstrumentedConsumer, AutoInstrumentedProducer, ConfluentKafkaInstrumentor, ProxiedConsumer, ProxiedProducer, ) from opentelemetry.instrumentation.confluent_kafka.utils import ( KafkaContextGetter, KafkaContextSetter, ) from opentelemetry.semconv._incubating.attributes.messaging_attributes import ( MESSAGING_MESSAGE_ID, MESSAGING_OPERATION, MESSAGING_SYSTEM, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.semconv.trace import ( MessagingDestinationKindValues, SpanAttributes, ) from opentelemetry.test.test_base import TestBase from .utils import MockConsumer, MockedMessage, MockedProducer class TestConfluentKafka(TestBase): def test_instrument_api(self) -> None: from confluent_kafka import Consumer, Producer # noqa: PLC0415 instrumentation = ConfluentKafkaInstrumentor() producer = Producer({"bootstrap.servers": "localhost:29092"}) producer = instrumentation.instrument_producer(producer) self.assertEqual(producer.__class__, ProxiedProducer) producer = instrumentation.uninstrument_producer(producer) self.assertEqual(producer.__class__, Producer) consumer = Consumer( { "bootstrap.servers": "localhost:29092", "group.id": "mygroup", "auto.offset.reset": "earliest", } ) consumer = instrumentation.instrument_consumer(consumer) self.assertEqual(consumer.__class__, ProxiedConsumer) consumer = instrumentation.uninstrument_consumer(consumer) self.assertEqual(consumer.__class__, Consumer) consumer = Consumer( **{ "bootstrap.servers": "localhost:29092", "group.id": "mygroup", "auto.offset.reset": "earliest", } ) consumer = instrumentation.instrument_consumer(consumer) self.assertEqual(consumer.__class__, ProxiedConsumer) consumer = instrumentation.uninstrument_consumer(consumer) self.assertEqual(consumer.__class__, Consumer) def test_instrument_api_with_instrument(self) -> None: ConfluentKafkaInstrumentor().instrument() from confluent_kafka import Consumer, Producer # noqa: PLC0415 producer = Producer({"bootstrap.servers": "localhost:29092"}) self.assertEqual(producer.__class__, AutoInstrumentedProducer) consumer = Consumer( { "bootstrap.servers": "localhost:29092", "group.id": "mygroup", "auto.offset.reset": "earliest", } ) self.assertEqual(consumer.__class__, AutoInstrumentedConsumer) consumer = Consumer( **{ "bootstrap.servers": "localhost:29092", "group.id": "mygroup", "auto.offset.reset": "earliest", } ) self.assertEqual(consumer.__class__, AutoInstrumentedConsumer) ConfluentKafkaInstrumentor().uninstrument() def test_consumer_commit_method_exists(self) -> None: from confluent_kafka import Consumer # noqa: PLC0415 instrumentation = ConfluentKafkaInstrumentor() consumer = Consumer( { "bootstrap.servers": "localhost:29092", "group.id": "mygroup", "auto.offset.reset": "earliest", } ) consumer = instrumentation.instrument_consumer(consumer) self.assertEqual(consumer.__class__, ProxiedConsumer) self.assertTrue(hasattr(consumer, "commit")) def test_context_setter(self) -> None: context_setter = KafkaContextSetter() carrier_dict = {"key1": "val1"} context_setter.set(carrier_dict, "key2", "val2") self.assertGreaterEqual(carrier_dict.items(), {"key2": b"val2"}.items()) carrier_list = [("key1", "val1")] context_setter.set(carrier_list, "key2", "val2") self.assertTrue(("key2", b"val2") in carrier_list) def test_context_getter(self) -> None: context_setter = KafkaContextSetter() context_getter = KafkaContextGetter() carrier_dict = {} context_setter.set(carrier_dict, "key1", "val1") self.assertEqual(context_getter.get(carrier_dict, "key1"), ["val1"]) self.assertEqual(["key1"], context_getter.keys(carrier_dict)) carrier_list = [] context_setter.set(carrier_list, "key1", "val1") self.assertEqual(context_getter.get(carrier_list, "key1"), ["val1"]) self.assertEqual(["key1"], context_getter.keys(carrier_list)) def test_poll(self) -> None: instrumentation = ConfluentKafkaInstrumentor() mocked_messages = [ MockedMessage("topic-10", 0, 0, []), MockedMessage("topic-20", 2, 4, []), MockedMessage("topic-30", 1, 3, []), ] expected_spans = [ {"name": "recv", "attributes": {}}, { "name": "topic-10 process", "attributes": { MESSAGING_OPERATION: "process", SpanAttributes.MESSAGING_KAFKA_PARTITION: 0, MESSAGING_SYSTEM: "kafka", SpanAttributes.MESSAGING_DESTINATION: "topic-10", SpanAttributes.MESSAGING_DESTINATION_KIND: MessagingDestinationKindValues.QUEUE.value, MESSAGING_MESSAGE_ID: "topic-10.0.0", }, }, {"name": "recv", "attributes": {}}, { "name": "topic-20 process", "attributes": { MESSAGING_OPERATION: "process", SpanAttributes.MESSAGING_KAFKA_PARTITION: 2, MESSAGING_SYSTEM: "kafka", SpanAttributes.MESSAGING_DESTINATION: "topic-20", SpanAttributes.MESSAGING_DESTINATION_KIND: MessagingDestinationKindValues.QUEUE.value, MESSAGING_MESSAGE_ID: "topic-20.2.4", }, }, {"name": "recv", "attributes": {}}, { "name": "topic-30 process", "attributes": { MESSAGING_OPERATION: "process", SpanAttributes.MESSAGING_KAFKA_PARTITION: 1, MESSAGING_SYSTEM: "kafka", SpanAttributes.MESSAGING_DESTINATION: "topic-30", SpanAttributes.MESSAGING_DESTINATION_KIND: MessagingDestinationKindValues.QUEUE.value, MESSAGING_MESSAGE_ID: "topic-30.1.3", }, }, ] consumer = MockConsumer( mocked_messages, { "bootstrap.servers": "localhost:29092", "group.id": "mygroup", "auto.offset.reset": "earliest", }, ) self.memory_exporter.clear() consumer = instrumentation.instrument_consumer(consumer) consumer.poll() consumer.poll() consumer.poll() consumer.poll() # empty poll — must not produce a span span_list = self.memory_exporter.get_finished_spans() self._compare_spans(span_list, expected_spans) def test_consume(self) -> None: instrumentation = ConfluentKafkaInstrumentor() mocked_messages = [ MockedMessage("topic-1", 0, 0, []), MockedMessage("topic-1", 2, 1, []), MockedMessage("topic-1", 3, 2, []), MockedMessage("topic-2", 0, 0, []), MockedMessage("topic-3", 0, 3, []), MockedMessage("topic-2", 0, 1, []), ] expected_spans = [ {"name": "recv", "attributes": {}}, { "name": "topic-1 process", "attributes": { MESSAGING_OPERATION: "process", MESSAGING_SYSTEM: "kafka", SpanAttributes.MESSAGING_DESTINATION: "topic-1", SpanAttributes.MESSAGING_DESTINATION_KIND: MessagingDestinationKindValues.QUEUE.value, }, }, {"name": "recv", "attributes": {}}, { "name": "topic-2 process", "attributes": { MESSAGING_OPERATION: "process", MESSAGING_SYSTEM: "kafka", SpanAttributes.MESSAGING_DESTINATION: "topic-2", SpanAttributes.MESSAGING_DESTINATION_KIND: MessagingDestinationKindValues.QUEUE.value, }, }, {"name": "recv", "attributes": {}}, { "name": "topic-3 process", "attributes": { MESSAGING_OPERATION: "process", MESSAGING_SYSTEM: "kafka", SpanAttributes.MESSAGING_DESTINATION: "topic-3", SpanAttributes.MESSAGING_DESTINATION_KIND: MessagingDestinationKindValues.QUEUE.value, }, }, ] consumer = MockConsumer( mocked_messages, { "bootstrap.servers": "localhost:29092", "group.id": "mygroup", "auto.offset.reset": "earliest", }, ) self.memory_exporter.clear() consumer = instrumentation.instrument_consumer(consumer) consumer.consume(3) consumer.consume(1) consumer.consume(2) consumer.consume(1) # empty consume — must not produce a span span_list = self.memory_exporter.get_finished_spans() self._compare_spans(span_list, expected_spans) def test_poll_empty_does_not_create_span(self) -> None: instrumentation = ConfluentKafkaInstrumentor() consumer = MockConsumer( [], { "bootstrap.servers": "localhost:29092", "group.id": "mygroup", "auto.offset.reset": "earliest", }, ) self.memory_exporter.clear() consumer = instrumentation.instrument_consumer(consumer) consumer.poll() consumer.poll() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) def test_consume_empty_does_not_create_span(self) -> None: instrumentation = ConfluentKafkaInstrumentor() consumer = MockConsumer( [], { "bootstrap.servers": "localhost:29092", "group.id": "mygroup", "auto.offset.reset": "earliest", }, ) self.memory_exporter.clear() consumer = instrumentation.instrument_consumer(consumer) consumer.consume(5) consumer.consume(5) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) def test_poll_empty_cleans_up_previous_span_and_token(self) -> None: instrumentation = ConfluentKafkaInstrumentor() consumer = MockConsumer( [MockedMessage("topic-1", 0, 0, [])], { "bootstrap.servers": "localhost:29092", "group.id": "mygroup", "auto.offset.reset": "earliest", }, ) consumer = instrumentation.instrument_consumer(consumer) consumer.poll() # non-empty: sets _current_consume_span and _current_context_token self.assertIsNotNone(consumer._current_consume_span) self.assertIsNotNone(consumer._current_context_token) consumer.poll() # empty: should clean up both self.assertIsNone(consumer._current_consume_span) self.assertIsNone(consumer._current_context_token) def test_consume_empty_cleans_up_previous_span_and_token(self) -> None: instrumentation = ConfluentKafkaInstrumentor() consumer = MockConsumer( [MockedMessage("topic-1", 0, 0, [])], { "bootstrap.servers": "localhost:29092", "group.id": "mygroup", "auto.offset.reset": "earliest", }, ) consumer = instrumentation.instrument_consumer(consumer) consumer.consume(1) # non-empty: sets _current_consume_span and _current_context_token self.assertIsNotNone(consumer._current_consume_span) self.assertIsNotNone(consumer._current_context_token) consumer.consume(1) # empty: should clean up both self.assertIsNone(consumer._current_consume_span) self.assertIsNone(consumer._current_context_token) def test_close(self) -> None: instrumentation = ConfluentKafkaInstrumentor() mocked_messages = [ MockedMessage("topic-a", 0, 0, []), ] expected_spans = [ {"name": "recv", "attributes": {}}, { "name": "topic-a process", "attributes": { MESSAGING_OPERATION: "process", SpanAttributes.MESSAGING_KAFKA_PARTITION: 0, MESSAGING_SYSTEM: "kafka", SpanAttributes.MESSAGING_DESTINATION: "topic-a", SpanAttributes.MESSAGING_DESTINATION_KIND: MessagingDestinationKindValues.QUEUE.value, MESSAGING_MESSAGE_ID: "topic-a.0.0", }, }, ] consumer = MockConsumer( mocked_messages, { "bootstrap.servers": "localhost:29092", "group.id": "mygroup", "auto.offset.reset": "earliest", }, ) self.memory_exporter.clear() consumer = instrumentation.instrument_consumer(consumer) consumer.poll() consumer.close() span_list = self.memory_exporter.get_finished_spans() self._compare_spans(span_list, expected_spans) def _compare_spans(self, spans, expected_spans): self.assertEqual(len(spans), len(expected_spans)) for span, expected_span in zip(spans, expected_spans): self.assertEqual(expected_span["name"], span.name) for attribute_key, expected_attribute_value in expected_span["attributes"].items(): self.assertEqual(expected_attribute_value, span.attributes[attribute_key]) def _assert_topic(self, span, expected_topic: str) -> None: self.assertEqual( span.attributes[SpanAttributes.MESSAGING_DESTINATION], expected_topic, ) def _assert_span_count(self, span_list, expected_count: int) -> None: self.assertEqual(len(span_list), expected_count) def test_producer_poll(self) -> None: instrumentation = ConfluentKafkaInstrumentor() message_queue = [] producer = MockedProducer( message_queue, { "bootstrap.servers": "localhost:29092", }, ) producer = instrumentation.instrument_producer(producer) producer.produce(topic="topic-1", key="key-1", value="value-1") msg = producer.poll() self.assertIsNotNone(msg) span_list = self.memory_exporter.get_finished_spans() self._assert_span_count(span_list, 1) self._assert_topic(span_list[0], "topic-1") def test_producer_flush(self) -> None: instrumentation = ConfluentKafkaInstrumentor() message_queue = [] producer = MockedProducer( message_queue, { "bootstrap.servers": "localhost:29092", }, ) producer = instrumentation.instrument_producer(producer) producer.produce(topic="topic-1", key="key-1", value="value-1") msg = producer.flush() self.assertIsNotNone(msg) span_list = self.memory_exporter.get_finished_spans() self._assert_span_count(span_list, 1) self._assert_topic(span_list[0], "topic-1") def test_producer_sets_bootstrap_servers_attributes(self) -> None: instrumentation = ConfluentKafkaInstrumentor() producer = MockedProducer( [], { "bootstrap.servers": "broker-a:9092,broker-b:9093", }, ) producer = instrumentation.instrument_producer(producer) producer.produce(topic="topic-1", key="k", value="v") span = self.memory_exporter.get_finished_spans()[0] self.assertEqual(span.attributes[SERVER_ADDRESS], "broker-a") self.assertEqual(span.attributes[SERVER_PORT], 9092) def test_consumer_sets_bootstrap_servers_attributes(self) -> None: instrumentation = ConfluentKafkaInstrumentor() consumer = MockConsumer( [MockedMessage("topic-1", 0, 0, [])], { "bootstrap.servers": "broker-1:9092", "group.id": "g", "auto.offset.reset": "earliest", }, ) self.memory_exporter.clear() consumer = instrumentation.instrument_consumer(consumer) consumer.poll() # Second (empty) poll ends the in-flight ` process` span so it # shows up in the exporter. consumer.poll() process_span = next(s for s in self.memory_exporter.get_finished_spans() if s.name == "topic-1 process") self.assertEqual(process_span.attributes[SERVER_ADDRESS], "broker-1") self.assertEqual(process_span.attributes[SERVER_PORT], 9092) utils.py000066400000000000000000000037221527070072300370140ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from confluent_kafka import Consumer, Producer class MockConsumer(Consumer): def __init__(self, queue, config): self._queue = queue self.config = config super().__init__(config) def consume(self, num_messages=1, *args, **kwargs): # pylint: disable=keyword-arg-before-vararg messages = self._queue[:num_messages] self._queue = self._queue[num_messages:] return messages def poll(self, timeout=None): if len(self._queue) > 0: return self._queue.pop(0) return None class MockedMessage: def __init__( self, topic: str, partition: int, offset: int, headers, key: str | None = None, value: str | None = None, ): self._topic = topic self._partition = partition self._offset = offset self._headers = headers self._key = key self._value = value def topic(self): return self._topic def partition(self): return self._partition def offset(self): return self._offset def headers(self): return self._headers def key(self): return self._key def value(self): return self._value class MockedProducer(Producer): def __init__(self, queue, config): self._queue = queue self.config = config super().__init__(config) def produce(self, *args, **kwargs): # pylint: disable=keyword-arg-before-vararg self._queue.append( MockedMessage( topic=kwargs.get("topic"), partition=0, offset=0, headers=[], key=kwargs.get("key"), value=kwargs.get("value"), ) ) def poll(self, *args, **kwargs): return len(self._queue) def flush(self, *args, **kwargs): return len(self._queue) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-dbapi/000077500000000000000000000000001527070072300322225ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-dbapi/LICENSE000066400000000000000000000261351527070072300332360ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-dbapi/README.rst000066400000000000000000000012451527070072300337130ustar00rootroot00000000000000OpenTelemetry Database API instrumentation ========================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-dbapi.svg :target: https://pypi.org/project/opentelemetry-instrumentation-dbapi/ Installation ------------ :: pip install opentelemetry-instrumentation-dbapi References ---------- * `OpenTelemetry Database API Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000027471527070072300350710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-dbapi[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-dbapi" dynamic = ["version"] description = "OpenTelemetry Database API instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-dbapi" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/dbapi/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-dbapi/src/000077500000000000000000000000001527070072300330115ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300356265ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-dbapi/srcinstrumentation/000077500000000000000000000000001527070072300410715ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetrydbapi/000077500000000000000000000000001527070072300421505ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation__init__.py000066400000000000000000001157031527070072300442700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines """ The trace integration with Database API supports libraries that follow the Python Database API Specification v2.0. ``_ Usage ----- The DB-API instrumentor and its utilities provide common, core functionality for database framework or object relation mapper (ORM) instrumentations. Users will typically instrument database client code with those framework/ORM-specific instrumentations, instead of directly using this DB-API integration. Features such as sqlcommenter can be configured at framework/ORM level as well. See full list at `instrumentation`_. .. _instrumentation: https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation If an instrumentation for your needs does not exist, then DB-API integration can be used directly as follows. .. code-block:: python import mysql.connector import pyodbc from opentelemetry.instrumentation.dbapi import ( trace_integration, wrap_connect, ) # Example: mysql.connector trace_integration(mysql.connector, "connect", "mysql") # Example: pyodbc trace_integration(pyodbc, "connect", "odbc") # Or, directly call wrap_connect for more configurability. wrap_connect(__name__, mysql.connector, "connect", "mysql") wrap_connect(__name__, pyodbc, "connect", "odbc") Configuration ------------- SQLCommenter ************ You can optionally enable sqlcommenter which enriches the query with contextual information. Queries made after setting up trace integration with sqlcommenter enabled will have configurable key-value pairs appended to them, e.g. ``"select * from auth_users; /*traceparent=00-01234567-abcd-01*/"``. This supports context propagation between database client and server when database log records are enabled. For more information, see: * `Semantic Conventions - Database Spans `_ * `sqlcommenter `_ .. code:: python import mysql.connector from opentelemetry.instrumentation.dbapi import wrap_connect # Opts into sqlcomment for MySQL trace integration. wrap_connect( __name__, mysql.connector, "connect", "mysql", enable_commenter=True, ) SQLCommenter with commenter_options *********************************** The key-value pairs appended to the query can be configured using ``commenter_options``. When sqlcommenter is enabled, all available KVs/tags are calculated by default. ``commenter_options`` supports *opting out* of specific KVs. .. code:: python import mysql.connector from opentelemetry.instrumentation.dbapi import wrap_connect # Opts into sqlcomment for MySQL trace integration. # Opts out of tags for libpq_version, db_driver. wrap_connect( __name__, mysql.connector, "connect", "mysql", enable_commenter=True, commenter_options={ "libpq_version": False, "db_driver": False, } ) Available commenter_options ########################### The following sqlcomment key-values can be opted out of through ``commenter_options``: +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | Commenter Option | Description | Example | +===========================+===========================================================+===========================================================================+ | ``db_driver`` | Database driver name with version. | ``mysql.connector=2.2.9`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``dbapi_threadsafety`` | DB-API threadsafety value: 0-3 or unknown. | ``dbapi_threadsafety=2`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``dbapi_level`` | DB-API API level: 1.0, 2.0, or unknown. | ``dbapi_level=2.0`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``driver_paramstyle`` | DB-API paramstyle for SQL statement parameter. | ``driver_paramstyle='pyformat'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``libpq_version`` | PostgreSQL libpq version (checked for PostgreSQL only). | ``libpq_version=140001`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``mysql_client_version`` | MySQL client version (checked for MySQL only). | ``mysql_client_version='123'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``opentelemetry_values`` | OpenTelemetry context as traceparent at time of query. | ``traceparent='00-03afa25236b8cd948fa853d67038ac79-405ff022e8247c46-01'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ SQLComment in span attribute **************************** If sqlcommenter is enabled, you can opt into the inclusion of sqlcomment in the query span ``db.statement`` and/or ``db.query.text`` attribute for your needs. If ``commenter_options`` have been set, the span attribute comment will also be configured by this setting. .. code:: python import mysql.connector from opentelemetry.instrumentation.dbapi import wrap_connect # Opts into sqlcomment for MySQL trace integration. # Opts into sqlcomment for `db.statement` and/or `db.query.text` span attribute. wrap_connect( __name__, mysql.connector, "connect", "mysql", enable_commenter=True, enable_attribute_commenter=True, ) API --- """ from __future__ import annotations import functools import logging import re import sys import time from collections.abc import Awaitable, Callable, Sequence from typing import TYPE_CHECKING, Any, Generic, TypeVar from wrapt import wrap_function_wrapper try: # wrapt 2.0.0+ from wrapt import ( # pylint: disable=no-name-in-module BaseObjectProxy, ObjectProxy, ) except ImportError: from wrapt import ObjectProxy from wrapt import ObjectProxy as BaseObjectProxy from opentelemetry import trace as trace_api from opentelemetry.instrumentation._semconv import ( _get_schema_url_for_signal_types, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _report_new, _set_db_name, _set_db_statement, _set_db_system, _set_db_user, _set_http_net_peer_name_client, _set_http_peer_port_client, ) from opentelemetry.instrumentation.dbapi.version import __version__ from opentelemetry.instrumentation.sqlcommenter_utils import _add_sql_comment from opentelemetry.instrumentation.utils import ( _get_opentelemetry_values, is_instrumentation_enabled, unwrap, ) from opentelemetry.metrics import MeterProvider, get_meter from opentelemetry.semconv._incubating.metrics.db_metrics import ( create_db_client_operation_duration, create_db_client_response_returned_rows, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_NAMESPACE, DB_OPERATION_NAME, DB_SYSTEM_NAME, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.trace import SpanKind, TracerProvider, get_tracer from opentelemetry.util._importlib_metadata import version as util_version if sys.version_info >= (3, 14): from string.templatelib import Template as _Template else: _Template = () if TYPE_CHECKING: if sys.version_info >= (3, 14): from string.templatelib import Template else: from typing import Never Template = Never _DB_DRIVER_ALIASES = { "MySQLdb": "mysqlclient", } _logger = logging.getLogger(__name__) ConnectionT = TypeVar("ConnectionT") CursorT = TypeVar("CursorT") def trace_integration( connect_module: Callable[..., Any], connect_method_name: str, database_system: str, connection_attributes: dict[str, Any] | None = None, tracer_provider: TracerProvider | None = None, capture_parameters: bool = False, enable_commenter: bool = False, db_api_integration_factory: type[DatabaseApiIntegration] | None = None, enable_attribute_commenter: bool = False, commenter_options: dict[str, Any] | None = None, meter_provider: MeterProvider | None = None, ): """Integrate with DB API library. https://www.python.org/dev/peps/pep-0249/ Args: connect_module: Module name where connect method is available. connect_method_name: The connect method name. database_system: An identifier for the database management system (DBMS) product being used. connection_attributes: Attribute names for database, port, host and user in Connection object. tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to use. If omitted the current configured one is used. capture_parameters: Configure if db.statement.parameters should be captured. enable_commenter: Flag to enable/disable sqlcommenter. db_api_integration_factory: The `DatabaseApiIntegration` to use. If none is passed the default one is used. enable_attribute_commenter: Flag to enable/disable sqlcomment inclusion in `db.statement` and/or `db.query.text` span attribute. Only available if enable_commenter=True. commenter_options: Configurations for tags to be appended at the sql query. meter_provider: The :class:`opentelemetry.metrics.MeterProvider` to use. If omitted the current configured one is used. """ wrap_connect( __name__, connect_module, connect_method_name, database_system, connection_attributes, version=__version__, tracer_provider=tracer_provider, capture_parameters=capture_parameters, enable_commenter=enable_commenter, db_api_integration_factory=db_api_integration_factory, enable_attribute_commenter=enable_attribute_commenter, commenter_options=commenter_options, meter_provider=meter_provider, ) # pylint: disable-next=too-many-positional-arguments def wrap_connect( name: str, connect_module: Callable[..., Any], connect_method_name: str, database_system: str, connection_attributes: dict[str, Any] | None = None, version: str = "", tracer_provider: TracerProvider | None = None, capture_parameters: bool = False, enable_commenter: bool = False, db_api_integration_factory: type[DatabaseApiIntegration] | None = None, commenter_options: dict[str, Any] | None = None, enable_attribute_commenter: bool = False, meter_provider: MeterProvider | None = None, ): """Integrate with DB API library. https://www.python.org/dev/peps/pep-0249/ Args: connect_module: Module name where connect method is available. connect_method_name: The connect method name. database_system: An identifier for the database management system (DBMS) product being used. connection_attributes: Attribute names for database, port, host and user in Connection object. tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to use. If omitted the current configured one is used. capture_parameters: Configure if db.statement.parameters should be captured. enable_commenter: Flag to enable/disable sqlcommenter. db_api_integration_factory: The `DatabaseApiIntegration` to use. If none is passed the default one is used. commenter_options: Configurations for tags to be appended at the sql query. enable_attribute_commenter: Flag to enable/disable sqlcomment inclusion in `db.statement` and/or `db.query.text` span attribute. Only available if enable_commenter=True. meter_provider: The :class:`opentelemetry.metrics.MeterProvider` to use. If omitted the current configured one is used. """ db_api_integration_factory = db_api_integration_factory or DatabaseApiIntegration # pylint: disable=unused-argument def wrap_connect_( wrapped: Callable[..., Any], instance: Any, args: tuple[Any, Any], kwargs: dict[Any, Any], ): db_integration = db_api_integration_factory( name, database_system, connection_attributes=connection_attributes, version=version, tracer_provider=tracer_provider, capture_parameters=capture_parameters, enable_commenter=enable_commenter, commenter_options=commenter_options, connect_module=connect_module, enable_attribute_commenter=enable_attribute_commenter, meter_provider=meter_provider, ) return db_integration.wrapped_connection(wrapped, args, kwargs) try: wrap_function_wrapper(connect_module, connect_method_name, wrap_connect_) except Exception as ex: # pylint: disable=broad-except _logger.warning("Failed to integrate with DB API. %s", str(ex)) def unwrap_connect(connect_module: Callable[..., Any], connect_method_name: str): """Disable integration with DB API library. https://www.python.org/dev/peps/pep-0249/ Args: connect_module: Module name where the connect method is available. connect_method_name: The connect method name. """ unwrap(connect_module, connect_method_name) # pylint: disable-next=too-many-positional-arguments def instrument_connection( name: str, connection: ConnectionT | TracedConnectionProxy[ConnectionT], database_system: str, connection_attributes: dict[str, Any] | None = None, version: str = "", tracer_provider: TracerProvider | None = None, capture_parameters: bool = False, enable_commenter: bool = False, commenter_options: dict[str, Any] | None = None, connect_module: Callable[..., Any] | None = None, enable_attribute_commenter: bool = False, db_api_integration_factory: type[DatabaseApiIntegration] | None = None, meter_provider: MeterProvider | None = None, ) -> TracedConnectionProxy[ConnectionT]: """Enable instrumentation in a database connection. Args: name: The instrumentation module name. connection: The connection to instrument. database_system: An identifier for the database management system (DBMS) product being used. connection_attributes: Attribute names for database, port, host and user in a connection object. tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to use. If omitted the current configured one is used. capture_parameters: Configure if db.statement.parameters should be captured. enable_commenter: Flag to enable/disable sqlcommenter. commenter_options: Configurations for tags to be appended at the sql query. connect_module: Module name where connect method is available. enable_attribute_commenter: Flag to enable/disable sqlcomment inclusion in `db.statement` and/or `db.query.text` span attribute. Only available if enable_commenter=True. db_api_integration_factory: A class or factory function to use as a replacement for :class:`DatabaseApiIntegration`. Can be used to obtain connection attributes from the connect method instead of from the connection itself (as done by the pymssql intrumentor). meter_provider: The :class:`opentelemetry.metrics.MeterProvider` to use. If omitted the current configured one is used. Returns: An instrumented connection. """ if isinstance(connection, BaseObjectProxy): _logger.warning("Connection already instrumented") return connection db_api_integration_factory = db_api_integration_factory or DatabaseApiIntegration db_integration = db_api_integration_factory( name, database_system, connection_attributes=connection_attributes, version=version, tracer_provider=tracer_provider, capture_parameters=capture_parameters, enable_commenter=enable_commenter, commenter_options=commenter_options, connect_module=connect_module, enable_attribute_commenter=enable_attribute_commenter, meter_provider=meter_provider, ) db_integration.get_connection_attributes(connection) return get_traced_connection_proxy(connection, db_integration) def uninstrument_connection( connection: ConnectionT | TracedConnectionProxy[ConnectionT], ) -> ConnectionT: """Disable instrumentation in a database connection. Args: connection: The connection to uninstrument. Returns: An uninstrumented connection. """ if isinstance(connection, BaseObjectProxy): return connection.__wrapped__ _logger.warning("Connection is not instrumented") return connection class DatabaseApiIntegration: def __init__( self, name: str, database_system: str, connection_attributes: dict[str, Any] | None = None, version: str = "", tracer_provider: TracerProvider | None = None, capture_parameters: bool = False, enable_commenter: bool = False, commenter_options: dict[str, Any] | None = None, connect_module: Callable[..., Any] | None = None, enable_attribute_commenter: bool = False, meter_provider: MeterProvider | None = None, ): # Initialize semantic conventions opt-in if needed _OpenTelemetrySemanticConventionStability._initialize() self._sem_conv_opt_in_mode_db = ( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.DATABASE, ) ) self._sem_conv_opt_in_mode_http = ( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) ) if connection_attributes is None: self.connection_attributes = { "database": "database", "port": "port", "host": "host", "user": "user", } else: self.connection_attributes = connection_attributes self._name = name self._version = version self._tracer = get_tracer( self._name, instrumenting_library_version=self._version, tracer_provider=tracer_provider, schema_url=_get_schema_url_for_signal_types( [ _OpenTelemetryStabilitySignalType.DATABASE, _OpenTelemetryStabilitySignalType.HTTP, ] ), ) self._meter = None self._duration_histogram = None self._returned_rows_histogram = None if _report_new(self._sem_conv_opt_in_mode_db): self._meter = get_meter( self._name, self._version, meter_provider, schema_url=_get_schema_url_for_signal_types([_OpenTelemetryStabilitySignalType.DATABASE]), ) self._duration_histogram = create_db_client_operation_duration(self._meter) self._returned_rows_histogram = create_db_client_response_returned_rows(self._meter) self.capture_parameters = capture_parameters self.enable_commenter = enable_commenter self.commenter_options = commenter_options self.enable_attribute_commenter = enable_attribute_commenter self.database_system = database_system self.connection_props: dict[str, Any] = {} self.span_attributes: dict[str, Any] = {} self.name = "" self.database = "" self._server_address: str | None = None self._server_port: int | None = None self.connect_module = connect_module self.commenter_data = self.calculate_commenter_data() def _get_db_version(self, db_driver: str) -> str: if db_driver in _DB_DRIVER_ALIASES: return util_version(_DB_DRIVER_ALIASES[db_driver]) db_version = "" try: db_version = self.connect_module.__version__ except AttributeError: db_version = "unknown" return db_version def calculate_commenter_data(self) -> dict[str, Any]: commenter_data: dict[str, Any] = {} if not self.enable_commenter: return commenter_data db_driver = getattr(self.connect_module, "__name__", "unknown") db_version = self._get_db_version(db_driver) commenter_data = { "db_driver": f"{db_driver}:{db_version.split(' ')[0]}", # PEP 249-compliant drivers should have the following attributes. # We can assume apilevel "1.0" if not given. # We use "unknown" for others to prevent uncaught AttributeError. # https://peps.python.org/pep-0249/#globals "dbapi_threadsafety": getattr(self.connect_module, "threadsafety", "unknown"), "dbapi_level": getattr(self.connect_module, "apilevel", "1.0"), "driver_paramstyle": getattr(self.connect_module, "paramstyle", "unknown"), } if self.database_system == "postgresql": libpq_version = None # psycopg try: libpq_version = self.connect_module.pq.version() except AttributeError: pass # psycopg2 if libpq_version is None: # this the libpq version the client has been built against libpq_version = getattr(self.connect_module, "__libpq_version__", None) # we instrument psycopg modules that are not the root one, in that case you # won't get the libpq_version if libpq_version is not None: commenter_data.update({"libpq_version": libpq_version}) elif self.database_system == "mysql": mysqlc_version = "" if db_driver == "MySQLdb": mysqlc_version = self.connect_module._mysql.get_client_info() elif db_driver == "pymysql": mysqlc_version = self.connect_module.get_client_info() commenter_data.update({"mysql_client_version": mysqlc_version}) return commenter_data def wrapped_connection( self, connect_method: Callable[..., ConnectionT], args: tuple[Any, ...], kwargs: dict[Any, Any], ) -> TracedConnectionProxy[ConnectionT]: """Add object proxy to connection object.""" connection = connect_method(*args, **kwargs) self.get_connection_attributes(connection) return get_traced_connection_proxy(connection, self) def get_connection_attributes(self, connection: object) -> None: # Populate span fields using connection for key, value in self.connection_attributes.items(): # Allow attributes nested in connection object attribute = functools.reduce( lambda attribute, attribute_value: getattr(attribute, attribute_value, None), value.split("."), connection, ) if attribute: self.connection_props[key] = attribute self.name = self.database_system self.database = self.connection_props.get("database", "") if self.database: # PyMySQL encodes names with utf-8 if hasattr(self.database, "decode"): self.database = self.database.decode(errors="ignore") self.name += "." + self.database user = self.connection_props.get("user") # PyMySQL encodes this data if user and isinstance(user, bytes): user = user.decode() if user is not None: _set_db_user(self.span_attributes, str(user), self._sem_conv_opt_in_mode_db) host = self.connection_props.get("host") if host is not None: _set_http_net_peer_name_client( self.span_attributes, host, self._sem_conv_opt_in_mode_http, ) self._server_address = host port = self.connection_props.get("port") if port is not None: _set_http_peer_port_client(self.span_attributes, port, self._sem_conv_opt_in_mode_http) self._server_port = port # pylint: disable=abstract-method,no-member class TracedConnectionProxy(BaseObjectProxy, Generic[ConnectionT]): # pylint: disable=unused-argument def __init__( self, connection: ConnectionT, db_api_integration: DatabaseApiIntegration | None = None, ): BaseObjectProxy.__init__(self, connection) self._self_db_api_integration = db_api_integration def __getattribute__(self, name: str): if object.__getattribute__(self, name): return object.__getattribute__(self, name) return object.__getattribute__(object.__getattribute__(self, "_connection"), name) def cursor(self, *args: Any, **kwargs: Any): return get_traced_cursor_proxy( self.__wrapped__.cursor(*args, **kwargs), self._self_db_api_integration, ) def __enter__(self): self.__wrapped__.__enter__() return self def __exit__(self, *args: Any, **kwargs: Any): self.__wrapped__.__exit__(*args, **kwargs) def get_traced_connection_proxy( connection: ConnectionT, db_api_integration: DatabaseApiIntegration | None, *args: Any, **kwargs: Any, ) -> TracedConnectionProxy[ConnectionT]: return TracedConnectionProxy(connection, db_api_integration) def _t_string_to_str(template: Template) -> str: """Render a PEP 750 Template as a string with expression placeholders.""" parts: list[str] = [] for idx, literal in enumerate(template.strings): parts.append(literal) if idx < len(template.interpolations): parts.append(f"{{{template.interpolations[idx].expression}}}") return "".join(parts) class CursorTracer(Generic[CursorT]): def __init__(self, db_api_integration: DatabaseApiIntegration) -> None: self._db_api_integration = db_api_integration self._commenter_enabled = self._db_api_integration.enable_commenter self._commenter_options = ( self._db_api_integration.commenter_options if self._db_api_integration.commenter_options else {} ) self._enable_attribute_commenter = self._db_api_integration.enable_attribute_commenter self._connect_module = self._db_api_integration.connect_module self._leading_comment_remover = re.compile(r"^/\*.*?\*/") def _capture_mysql_version(self, cursor) -> None: """Lazy capture of mysql-connector client version using cursor, if applicable""" if ( self._db_api_integration.database_system == "mysql" and self._db_api_integration.connect_module.__name__ == "mysql.connector" and not self._db_api_integration.commenter_data["mysql_client_version"] ): try: # Autoinstrumentation and some programmatic calls client_version = cursor._cnx._cmysql.get_client_info() except AttributeError: # Other programmatic instrumentation with reassigned wrapped connection try: client_version = cursor._connection._cmysql.get_client_info() except AttributeError as exc: _logger.debug("Could not set mysql_client_version: %s", exc) client_version = "unknown" self._db_api_integration.commenter_data["mysql_client_version"] = client_version def _get_commenter_data(self) -> dict: """Uses DB-API integration to return commenter data for sqlcomment""" commenter_data = dict(self._db_api_integration.commenter_data) if self._commenter_options.get("opentelemetry_values", True): commenter_data.update(**_get_opentelemetry_values()) return {k: v for k, v in commenter_data.items() if self._commenter_options.get(k, True)} def _update_args_with_added_sql_comment(self, args, cursor) -> tuple: """Updates args with cursor info and adds sqlcomment to query statement""" try: args_list = list(args) self._capture_mysql_version(cursor) commenter_data = self._get_commenter_data() if not isinstance(args_list[0], str) and not isinstance(args_list[0], _Template): # Convert sql statement to string, handling psycopg2.sql.Composable object if hasattr(args_list[0], "as_string"): args_list[0] = args_list[0].as_string(cursor.connection) args_list[0] = str(args_list[0]) args_list[0] = _add_sql_comment(args_list[0], **commenter_data) args = tuple(args_list) except Exception as exc: # pylint: disable=broad-except _logger.exception("Exception while generating sql comment: %s", exc) return args def _populate_span( self, span: trace_api.Span, cursor: CursorT, *args: tuple[Any, ...], ): if not span.is_recording(): return statement = self.get_statement(cursor, args) sem_conv_mode = self._db_api_integration._sem_conv_opt_in_mode_db span_attrs = {} _set_db_system( span_attrs, self._db_api_integration.database_system, sem_conv_mode, ) _set_db_name( span_attrs, self._db_api_integration.database, sem_conv_mode, ) _set_db_statement(span_attrs, statement, sem_conv_mode) # Set all collected attributes span.set_attributes(span_attrs) for ( attribute_key, attribute_value, ) in self._db_api_integration.span_attributes.items(): span.set_attribute(attribute_key, attribute_value) if self._db_api_integration.capture_parameters and len(args) > 1: span.set_attribute("db.statement.parameters", str(args[1])) def get_operation_name(self, cursor: CursorT, args: Sequence[Any]) -> str: # pylint: disable=no-self-use if not args: return "" query = args[0] if isinstance(query, _Template): first_literal = query.strings[0] if query.strings else "" tokens = self._leading_comment_remover.sub("", first_literal).split() return tokens[0] if tokens else "" # `query` can be an empty string. See #2643 if query and isinstance(query, str): # Strip leading comments so we get the operation name. return self._leading_comment_remover.sub("", query).split()[0] return "" def get_statement(self, cursor: CursorT, args: Sequence[Any]) -> str: # pylint: disable=no-self-use if not args: return "" statement = args[0] if isinstance(statement, _Template): return _t_string_to_str(statement) if isinstance(statement, bytes): return statement.decode("utf8", "replace") if isinstance(statement, str): return statement return "" def _get_metric_attributes( self, operation_name: str, error: Exception | None, ) -> dict[str, Any]: attributes: dict[str, Any] = { DB_SYSTEM_NAME: self._db_api_integration.database_system, } if self._db_api_integration.database: attributes[DB_NAMESPACE] = self._db_api_integration.database if operation_name: attributes[DB_OPERATION_NAME] = operation_name if self._db_api_integration._server_address is not None: attributes[SERVER_ADDRESS] = self._db_api_integration._server_address if self._db_api_integration._server_port is not None: attributes[SERVER_PORT] = self._db_api_integration._server_port if error is not None: attributes[ERROR_TYPE] = type(error).__qualname__ return attributes def _record_metrics( self, cursor: CursorT, operation_name: str, start_time: float, error: Exception | None, ) -> None: if not _report_new(self._db_api_integration._sem_conv_opt_in_mode_db): # DB Metrics are not supported without Database semconv opt-in return elapsed = time.perf_counter() - start_time attributes = self._get_metric_attributes(operation_name, error) self._db_api_integration._duration_histogram.record(elapsed, attributes=attributes) if error is None: rowcount = getattr(cursor, "rowcount", None) if isinstance(rowcount, int) and rowcount >= 0: self._db_api_integration._returned_rows_histogram.record(rowcount, attributes=attributes) def traced_execution( self, cursor: CursorT, query_method: Callable[..., Any], *args: tuple[Any, ...], **kwargs: dict[Any, Any], ): if not is_instrumentation_enabled(): return query_method(*args, **kwargs) operation_name = self.get_operation_name(cursor, args) name = operation_name if not name: name = ( self._db_api_integration.database if self._db_api_integration.database else self._db_api_integration.name ) with self._db_api_integration._tracer.start_as_current_span(name, kind=SpanKind.CLIENT) as span: if span.is_recording(): if args and self._commenter_enabled: if self._enable_attribute_commenter: # sqlcomment is added to executed query and db.statement and/or db.query.text span attribute args = self._update_args_with_added_sql_comment(args, cursor) self._populate_span(span, cursor, *args) else: # sqlcomment is only added to executed query # so db.statement and/or db.query.text are set before add_sql_comment self._populate_span(span, cursor, *args) args = self._update_args_with_added_sql_comment(args, cursor) else: # no sqlcomment anywhere self._populate_span(span, cursor, *args) start_time = time.perf_counter() error: Exception | None = None try: return query_method(*args, **kwargs) except Exception as exc: error = exc raise finally: self._record_metrics(cursor, operation_name, start_time, error) async def traced_execution_async( self, cursor: CursorT, query_method: Callable[..., Awaitable[Any]], *args: tuple[Any, ...], **kwargs: dict[Any, Any], ): if not is_instrumentation_enabled(): return await query_method(*args, **kwargs) operation_name = self.get_operation_name(cursor, args) name = operation_name if not name: name = ( self._db_api_integration.database if self._db_api_integration.database else self._db_api_integration.name ) with self._db_api_integration._tracer.start_as_current_span(name, kind=SpanKind.CLIENT) as span: if span.is_recording(): if args and self._commenter_enabled: if self._enable_attribute_commenter: # sqlcomment is added to executed query and db.statement and/or db.query.text span attribute args = self._update_args_with_added_sql_comment(args, cursor) self._populate_span(span, cursor, *args) else: # sqlcomment is only added to executed query # so db.statement and/or db.query.text are set before add_sql_comment self._populate_span(span, cursor, *args) args = self._update_args_with_added_sql_comment(args, cursor) else: # no sqlcomment anywhere self._populate_span(span, cursor, *args) start_time = time.perf_counter() error: Exception | None = None try: return await query_method(*args, **kwargs) except Exception as exc: error = exc raise finally: self._record_metrics(cursor, operation_name, start_time, error) # pylint: disable=abstract-method,no-member class TracedCursorProxy(ObjectProxy, Generic[CursorT]): # pylint: disable=unused-argument def __init__( self, cursor: CursorT, db_api_integration: DatabaseApiIntegration, ): ObjectProxy.__init__(self, cursor) self._self_cursor_tracer = CursorTracer[CursorT](db_api_integration) def execute(self, *args: Any, **kwargs: Any): return self._self_cursor_tracer.traced_execution(self.__wrapped__, self.__wrapped__.execute, *args, **kwargs) def executemany(self, *args: Any, **kwargs: Any): return self._self_cursor_tracer.traced_execution( self.__wrapped__, self.__wrapped__.executemany, *args, **kwargs ) def callproc(self, *args: Any, **kwargs: Any): return self._self_cursor_tracer.traced_execution(self.__wrapped__, self.__wrapped__.callproc, *args, **kwargs) def __enter__(self): self.__wrapped__.__enter__() return self def __exit__(self, *args, **kwargs): self.__wrapped__.__exit__(*args, **kwargs) def get_traced_cursor_proxy( cursor: CursorT, db_api_integration: DatabaseApiIntegration, *args: Any, **kwargs: Any, ) -> TracedCursorProxy[CursorT]: return TracedCursorProxy(cursor, db_api_integration) package.py000066400000000000000000000002041527070072300441110ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = tuple() _semconv_status = "migration" py.typed000066400000000000000000000000001527070072300436350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapiversion.py000066400000000000000000000001741527070072300442110ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" _instruments = tuple() test-requirements-wrapt1.txt000066400000000000000000000000471527070072300376210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-dbapi-r test-requirements.txt wrapt==1.17.3 test-requirements-wrapt2.txt000066400000000000000000000000461527070072300376210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-dbapi-r test-requirements.txt wrapt==2.1.2 test-requirements.txt000066400000000000000000000003521527070072300364040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-dbapiasgiref==3.8.1 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-dbapi opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-dbapi/tests/000077500000000000000000000000001527070072300333645ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300354040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-dbapi/teststest_dbapi_integration.py000066400000000000000000002251621527070072300404100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-dbapi/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines import asyncio import contextlib import logging import re import sys from unittest import mock import pytest from opentelemetry import context from opentelemetry import trace as trace_api from opentelemetry.instrumentation import dbapi from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.sdk import resources from opentelemetry.semconv._incubating.attributes import net_attributes from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_STATEMENT, DB_SYSTEM, DB_USER, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv._incubating.metrics.db_metrics import ( DB_CLIENT_OPERATION_DURATION, DB_CLIENT_RESPONSE_RETURNED_ROWS, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_NAMESPACE, DB_OPERATION_NAME, DB_QUERY_TEXT, DB_SYSTEM_NAME, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.test.test_base import TestBase def _get_default_connection_props(): """Returns standard connection properties for testing.""" return { "database": "testdatabase", "server_host": "testhost", "server_port": 123, "user": "testuser", } def _get_default_connection_attributes(): """Returns standard connection attributes for testing.""" return { "database": "database", "port": "server_port", "host": "server_host", "user": "user", } @contextlib.contextmanager def use_semconv_opt_in(sem_conv_mode): env_patch = mock.patch.dict( "os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode}, ) _OpenTelemetrySemanticConventionStability._initialized = False env_patch.start() try: yield finally: env_patch.stop() _OpenTelemetrySemanticConventionStability._initialized = False # pylint: disable=too-many-public-methods class TestDBApiIntegration(TestBase): def setUp(self): super().setUp() self.tracer = self.tracer_provider.get_tracer(__name__) _OpenTelemetrySemanticConventionStability._initialized = False def test_span_succeeded(self): connection_props = _get_default_connection_props() connection_attributes = _get_default_connection_attributes() db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", connection_attributes, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, connection_props) cursor = mock_connection.cursor() cursor.execute("Test query", ("param1Value", False)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "Test") self.assertIs(span.kind, trace_api.SpanKind.CLIENT) self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.11.0", ) self.assertEqual(span.attributes[DB_SYSTEM], "testcomponent") self.assertEqual(span.attributes[DB_NAME], "testdatabase") self.assertEqual(span.attributes[DB_STATEMENT], "Test query") self.assertFalse("db.statement.parameters" in span.attributes) self.assertEqual(span.attributes[DB_USER], "testuser") self.assertEqual(span.attributes[NET_PEER_NAME], "testhost") self.assertEqual(span.attributes[NET_PEER_PORT], 123) self.assertIs(span.status.status_code, trace_api.StatusCode.UNSET) def test_suppress_instrumentation_async(self): execute = mock.AsyncMock(return_value="result") db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", ) with suppress_instrumentation(): result = asyncio.run( dbapi.CursorTracer(db_integration).traced_execution_async(MockCursor(), execute, "SELECT 1") ) self.assertEqual(result, "result") execute.assert_awaited_once_with("SELECT 1") self.assertEqual(len(self.memory_exporter.get_finished_spans()), 0) def test_span_succeeded_new_semconv(self): with use_semconv_opt_in("database,http"): connection_props = _get_default_connection_props() connection_attributes = _get_default_connection_attributes() db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", connection_attributes, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, connection_props) cursor = mock_connection.cursor() cursor.execute("Test query", ("param1Value", False)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "Test") self.assertIs(span.kind, trace_api.SpanKind.CLIENT) self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.25.0", ) # Stable attributes only self.assertEqual(span.attributes[DB_SYSTEM_NAME], "testcomponent") self.assertEqual(span.attributes[DB_NAMESPACE], "testdatabase") self.assertEqual(span.attributes[DB_QUERY_TEXT], "Test query") self.assertFalse("db.statement.parameters" in span.attributes) # db.user removed in stable - no replacement self.assertFalse(DB_USER in span.attributes) self.assertEqual(span.attributes[SERVER_ADDRESS], "testhost") self.assertEqual(span.attributes[SERVER_PORT], 123) # Old attributes should not be present self.assertFalse(DB_SYSTEM in span.attributes) self.assertFalse(DB_NAME in span.attributes) self.assertFalse(DB_STATEMENT in span.attributes) self.assertFalse(NET_PEER_NAME in span.attributes) self.assertFalse(NET_PEER_PORT in span.attributes) self.assertIs(span.status.status_code, trace_api.StatusCode.UNSET) def test_span_succeeded_both_semconv(self): with use_semconv_opt_in("database/dup,http/dup"): connection_props = _get_default_connection_props() connection_attributes = _get_default_connection_attributes() db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", connection_attributes, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, connection_props) cursor = mock_connection.cursor() cursor.execute("Test query", ("param1Value", False)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "Test") self.assertIs(span.kind, trace_api.SpanKind.CLIENT) self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.25.0", ) # Both old and new attributes should be present # Old attributes self.assertEqual(span.attributes[DB_SYSTEM], "testcomponent") self.assertEqual(span.attributes[DB_NAME], "testdatabase") self.assertEqual(span.attributes[DB_STATEMENT], "Test query") self.assertEqual(span.attributes[DB_USER], "testuser") self.assertEqual(span.attributes[NET_PEER_NAME], "testhost") self.assertEqual(span.attributes[NET_PEER_PORT], 123) # New stable attributes self.assertEqual(span.attributes[DB_SYSTEM_NAME], "testcomponent") self.assertEqual(span.attributes[DB_NAMESPACE], "testdatabase") self.assertEqual(span.attributes[DB_QUERY_TEXT], "Test query") self.assertEqual(span.attributes[SERVER_ADDRESS], "testhost") self.assertEqual(span.attributes[SERVER_PORT], 123) self.assertFalse("db.statement.parameters" in span.attributes) self.assertIs(span.status.status_code, trace_api.StatusCode.UNSET) def test_span_succeeded_database_only_semconv(self): with use_semconv_opt_in("database"): connection_props = _get_default_connection_props() connection_attributes = _get_default_connection_attributes() db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", connection_attributes, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, connection_props) cursor = mock_connection.cursor() cursor.execute("Test query", ("param1Value", False)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.25.0", ) # DB attributes should be stable-only self.assertEqual(span.attributes[DB_SYSTEM_NAME], "testcomponent") self.assertEqual(span.attributes[DB_NAMESPACE], "testdatabase") self.assertEqual(span.attributes[DB_QUERY_TEXT], "Test query") self.assertFalse(DB_SYSTEM in span.attributes) self.assertFalse(DB_NAME in span.attributes) self.assertFalse(DB_STATEMENT in span.attributes) # Network attributes should remain old semconv self.assertEqual(span.attributes[NET_PEER_NAME], "testhost") self.assertEqual(span.attributes[NET_PEER_PORT], 123) self.assertFalse(SERVER_ADDRESS in span.attributes) self.assertFalse(SERVER_PORT in span.attributes) def test_span_succeeded_http_only_semconv(self): with use_semconv_opt_in("http"): connection_props = _get_default_connection_props() connection_attributes = _get_default_connection_attributes() db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", connection_attributes, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, connection_props) cursor = mock_connection.cursor() cursor.execute("Test query", ("param1Value", False)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.21.0", ) # DB attributes should remain old semconv self.assertEqual(span.attributes[DB_SYSTEM], "testcomponent") self.assertEqual(span.attributes[DB_NAME], "testdatabase") self.assertEqual(span.attributes[DB_STATEMENT], "Test query") self.assertFalse(DB_SYSTEM_NAME in span.attributes) self.assertFalse(DB_NAMESPACE in span.attributes) self.assertFalse(DB_QUERY_TEXT in span.attributes) # Network attributes should be stable-only self.assertEqual(span.attributes[SERVER_ADDRESS], "testhost") self.assertEqual(span.attributes[SERVER_PORT], 123) self.assertFalse(NET_PEER_NAME in span.attributes) self.assertFalse(NET_PEER_PORT in span.attributes) def test_span_name(self): db_integration = dbapi.DatabaseApiIntegration("instrumenting_module_test_name", "testcomponent", {}) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.execute("Test query", ("param1Value", False)) cursor.execute( """multi line query""" ) cursor.execute("tab\tseparated query") cursor.execute("/* leading comment */ query") cursor.execute("/* leading comment */ query /* trailing comment */") cursor.execute("query /* trailing comment */") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 6) self.assertEqual(spans_list[0].name, "Test") self.assertEqual(spans_list[1].name, "multi") self.assertEqual(spans_list[2].name, "tab") self.assertEqual(spans_list[3].name, "query") self.assertEqual(spans_list[4].name, "query") self.assertEqual(spans_list[5].name, "query") def test_span_succeeded_with_capture_of_statement_parameters(self): connection_props = _get_default_connection_props() connection_attributes = _get_default_connection_attributes() db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", connection_attributes, capture_parameters=True, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, connection_props) cursor = mock_connection.cursor() cursor.execute("Test query", ("param1Value", False)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "Test") self.assertIs(span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(span.attributes[DB_SYSTEM], "testcomponent") self.assertEqual(span.attributes[DB_NAME], "testdatabase") self.assertEqual(span.attributes[DB_STATEMENT], "Test query") self.assertEqual( span.attributes["db.statement.parameters"], "('param1Value', False)", ) self.assertEqual(span.attributes[DB_USER], "testuser") self.assertEqual(span.attributes[net_attributes.NET_PEER_NAME], "testhost") self.assertEqual(span.attributes[net_attributes.NET_PEER_PORT], 123) self.assertIs(span.status.status_code, trace_api.StatusCode.UNSET) def test_span_succeeded_with_capture_of_statement_parameters_new_semconv( self, ): with use_semconv_opt_in("database,http"): connection_props = _get_default_connection_props() connection_attributes = _get_default_connection_attributes() db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", connection_attributes, capture_parameters=True, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, connection_props) cursor = mock_connection.cursor() cursor.execute("Test query", ("param1Value", False)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "Test") self.assertIs(span.kind, trace_api.SpanKind.CLIENT) # Stable attributes only self.assertEqual(span.attributes[DB_SYSTEM_NAME], "testcomponent") self.assertEqual(span.attributes[DB_NAMESPACE], "testdatabase") self.assertEqual(span.attributes[DB_QUERY_TEXT], "Test query") self.assertEqual( span.attributes["db.statement.parameters"], "('param1Value', False)", ) # db.user removed in stable - no replacement self.assertFalse(DB_USER in span.attributes) self.assertEqual(span.attributes[SERVER_ADDRESS], "testhost") self.assertEqual(span.attributes[SERVER_PORT], 123) # Old attributes should not be present self.assertFalse(DB_SYSTEM in span.attributes) self.assertFalse(DB_NAME in span.attributes) self.assertFalse(DB_STATEMENT in span.attributes) self.assertFalse(NET_PEER_NAME in span.attributes) self.assertFalse(NET_PEER_PORT in span.attributes) self.assertIs(span.status.status_code, trace_api.StatusCode.UNSET) def test_span_succeeded_with_capture_of_statement_parameters_both_semconv( self, ): with use_semconv_opt_in("database/dup,http/dup"): connection_props = _get_default_connection_props() connection_attributes = _get_default_connection_attributes() db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", connection_attributes, capture_parameters=True, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, connection_props) cursor = mock_connection.cursor() cursor.execute("Test query", ("param1Value", False)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "Test") self.assertIs(span.kind, trace_api.SpanKind.CLIENT) # Both old and new attributes should be present # Old attributes self.assertEqual(span.attributes[DB_SYSTEM], "testcomponent") self.assertEqual(span.attributes[DB_NAME], "testdatabase") self.assertEqual(span.attributes[DB_STATEMENT], "Test query") self.assertEqual( span.attributes["db.statement.parameters"], "('param1Value', False)", ) self.assertEqual(span.attributes[DB_USER], "testuser") self.assertEqual(span.attributes[net_attributes.NET_PEER_NAME], "testhost") self.assertEqual(span.attributes[net_attributes.NET_PEER_PORT], 123) # New stable attributes self.assertEqual(span.attributes[DB_SYSTEM_NAME], "testcomponent") self.assertEqual(span.attributes[DB_NAMESPACE], "testdatabase") self.assertEqual(span.attributes[DB_QUERY_TEXT], "Test query") self.assertEqual(span.attributes[SERVER_ADDRESS], "testhost") self.assertEqual(span.attributes[SERVER_PORT], 123) self.assertIs(span.status.status_code, trace_api.StatusCode.UNSET) def test_span_not_recording(self): connection_props = _get_default_connection_props() connection_attributes = _get_default_connection_attributes() mock_span = mock.Mock() mock_span.is_recording.return_value = False db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", connection_attributes, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, connection_props) cursor = mock_connection.cursor() cursor.execute("Test query", ("param1Value", False)) self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) def test_span_failed(self): db_integration = dbapi.DatabaseApiIntegration("instrumenting_module_test_name", "testcomponent") mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() with self.assertRaises(Exception): cursor.execute("Test query", throw_exception=True) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_STATEMENT], "Test query") self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) self.assertEqual(span.status.description, "Exception: Test Exception") def test_custom_tracer_provider_dbapi(self): resource = resources.Resource.create({"db-resource-key": "value"}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", tracer_provider=tracer_provider, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() with self.assertRaises(Exception): cursor.execute("Test query", throw_exception=True) spans_list = exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.resource.attributes["db-resource-key"], "value") self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) def test_no_op_tracer_provider(self): db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", tracer_provider=trace_api.NoOpTracerProvider(), ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Test query") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) def test_suppress_instrumentation(self): db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) with suppress_instrumentation(): cursor = mock_connection.cursor() cursor.execute("Test query", ("param1Value", False)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) def _get_metric(self, name): return next( (metric for metric in self.get_sorted_metrics() if metric.name == name), None, ) def test_metrics_not_emitted_in_default_mode(self): # Without OTEL_SEMCONV_STABILITY_OPT_IN=database, no metrics are exported db_integration = dbapi.DatabaseApiIntegration("instrumenting_module_test_name", "testcomponent") mock_connection = db_integration.wrapped_connection(mock_connect, (), {}) cursor = mock_connection.cursor() cursor.execute("SELECT 1", rowcount=3) self.assertIsNone(self._get_metric(DB_CLIENT_OPERATION_DURATION)) self.assertIsNone(self._get_metric(DB_CLIENT_RESPONSE_RETURNED_ROWS)) @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "database"}) def test_operation_duration_recorded(self): connection_props = { "database": "testdatabase", "server_host": "testhost", "server_port": 123, "user": "testuser", } connection_attributes = { "database": "database", "port": "server_port", "host": "server_host", "user": "user", } db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", connection_attributes, ) mock_connection = db_integration.wrapped_connection(mock_connect, (), connection_props) cursor = mock_connection.cursor() cursor.execute("SELECT * FROM users") duration_metric = self._get_metric(DB_CLIENT_OPERATION_DURATION) self.assertIsNotNone(duration_metric) self.assertEqual(duration_metric.unit, "s") points = list(duration_metric.data.data_points) self.assertEqual(len(points), 1) attributes = dict(points[0].attributes) self.assertEqual(attributes[DB_SYSTEM_NAME], "testcomponent") self.assertEqual(attributes[DB_NAMESPACE], "testdatabase") self.assertEqual(attributes[DB_OPERATION_NAME], "SELECT") self.assertEqual(attributes[SERVER_ADDRESS], "testhost") self.assertEqual(attributes[SERVER_PORT], 123) self.assertNotIn(ERROR_TYPE, attributes) self.assertEqual(points[0].count, 1) self.assertGreaterEqual(points[0].sum, 0.0) @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "database"}) def test_operation_duration_error_type(self): db_integration = dbapi.DatabaseApiIntegration("instrumenting_module_test_name", "testcomponent") mock_connection = db_integration.wrapped_connection(mock_connect, (), {}) cursor = mock_connection.cursor() with self.assertRaises(Exception): cursor.execute("SELECT 1", throw_exception=True) duration_metric = self._get_metric(DB_CLIENT_OPERATION_DURATION) self.assertIsNotNone(duration_metric) points = list(duration_metric.data.data_points) self.assertEqual(len(points), 1) attributes = dict(points[0].attributes) self.assertEqual(attributes[ERROR_TYPE], "Exception") self.assertEqual(attributes[DB_OPERATION_NAME], "SELECT") # returned_rows should NOT be recorded on error rows_metric = self._get_metric(DB_CLIENT_RESPONSE_RETURNED_ROWS) self.assertIsNone(rows_metric) @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "database"}) def test_returned_rows_recorded(self): db_integration = dbapi.DatabaseApiIntegration("instrumenting_module_test_name", "testcomponent") mock_connection = db_integration.wrapped_connection(mock_connect, (), {}) cursor = mock_connection.cursor() cursor.execute("SELECT 1", rowcount=3) rows_metric = self._get_metric(DB_CLIENT_RESPONSE_RETURNED_ROWS) self.assertIsNotNone(rows_metric) self.assertEqual(rows_metric.unit, "{row}") points = list(rows_metric.data.data_points) self.assertEqual(len(points), 1) self.assertEqual(points[0].sum, 3) self.assertEqual(points[0].count, 1) @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "database"}) def test_returned_rows_skipped_when_rowcount_unknown(self): db_integration = dbapi.DatabaseApiIntegration("instrumenting_module_test_name", "testcomponent") mock_connection = db_integration.wrapped_connection(mock_connect, (), {}) cursor = mock_connection.cursor() # default rowcount is -1 on MockCursor cursor.execute("CREATE TABLE t (x INT)") rows_metric = self._get_metric(DB_CLIENT_RESPONSE_RETURNED_ROWS) self.assertIsNone(rows_metric) # duration is still recorded self.assertIsNotNone(self._get_metric(DB_CLIENT_OPERATION_DURATION)) @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "database"}) def test_custom_meter_provider(self): meter_provider, metrics_reader = self.create_meter_provider() db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", meter_provider=meter_provider, ) mock_connection = db_integration.wrapped_connection(mock_connect, (), {}) cursor = mock_connection.cursor() cursor.execute("SELECT 1", rowcount=1) metrics_data = metrics_reader.get_metrics_data() names = {m.name for rm in metrics_data.resource_metrics for sm in rm.scope_metrics for m in sm.metrics} self.assertIn(DB_CLIENT_OPERATION_DURATION, names) self.assertIn(DB_CLIENT_RESPONSE_RETURNED_ROWS, names) @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "database"}) def test_async_operation_duration_recorded(self): db_integration = dbapi.DatabaseApiIntegration("instrumenting_module_test_name", "testcomponent") cursor_tracer = dbapi.CursorTracer(db_integration) mock_cursor = MockCursor() async def async_execute(_query, rowcount=-1): mock_cursor.rowcount = rowcount asyncio.run(cursor_tracer.traced_execution_async(mock_cursor, async_execute, "SELECT 1", rowcount=5)) duration_metric = self._get_metric(DB_CLIENT_OPERATION_DURATION) self.assertIsNotNone(duration_metric) points = list(duration_metric.data.data_points) self.assertEqual(len(points), 1) self.assertEqual(dict(points[0].attributes)[DB_OPERATION_NAME], "SELECT") rows_metric = self._get_metric(DB_CLIENT_RESPONSE_RETURNED_ROWS) self.assertIsNotNone(rows_metric) rows_points = list(rows_metric.data.data_points) self.assertEqual(rows_points[0].sum, 5) def test_commenter_options_propagation(self): db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", commenter_options={"opentelemetry_values": False}, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() with self.assertRaises(Exception): cursor.execute("SELECT 1", throw_exception=True) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes.get("db.system"), "testcomponent") self.assertIn("opentelemetry_values", db_integration.commenter_options) self.assertFalse(db_integration.commenter_options["opentelemetry_values"]) @mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect") def test_trace_integration_passes_commenter_options(self, mock_wrap_connect): fake_connect_module = mock.Mock() fake_options = {"opentelemetry_values": False, "foo": "bar"} dbapi.trace_integration( connect_module=fake_connect_module, connect_method_name="connect", database_system="testdb", commenter_options=fake_options, ) mock_wrap_connect.assert_called_once() _, _, kwargs = mock_wrap_connect.mock_calls[0] self.assertIn("commenter_options", kwargs) self.assertEqual(kwargs["commenter_options"], fake_options) def test_executemany(self): db_integration = dbapi.DatabaseApiIntegration("instrumenting_module_test_name", "testcomponent") mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Test query") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_STATEMENT], "Test query") # pylint: disable=no-self-use def test_executemany_iterable_cursor(self): db_integration = dbapi.DatabaseApiIntegration("instrumenting_module_test_name", "testcomponent") mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Test query") for _row in cursor: pass def test_executemany_comment(self): connect_module = mock.MagicMock() connect_module.__name__ = "test" connect_module.__version__ = mock.MagicMock() connect_module.pq.version.return_value = 123 connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "postgresql", enable_commenter=True, commenter_options={"db_driver": False, "dbapi_level": False}, connect_module=connect_module, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_executemany_comment_stmt_enabled(self): connect_module = mock.MagicMock() connect_module.__name__ = "test" connect_module.__version__ = mock.MagicMock() connect_module.pq.version.return_value = 123 connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "postgresql", enable_commenter=True, commenter_options={"db_driver": False, "dbapi_level": False}, connect_module=connect_module, enable_attribute_commenter=True, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertRegex( span.attributes[DB_STATEMENT], r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) def test_executemany_comment_non_pep_249_compliant(self): class MockConnectModule: def __getattr__(self, name): if name == "__name__": return "test" if name == "__version__": return mock.MagicMock() if name == "__libpq_version__": return 123 raise AttributeError("attribute missing") connect_module = MockConnectModule() db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "postgresql", enable_commenter=True, connect_module=connect_module, commenter_options={"db_driver": False}, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*dbapi_level='1.0',dbapi_threadsafety='unknown',driver_paramstyle='unknown',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_executemany_comment_non_pep_249_compliant_stmt_enabled(self): class MockConnectModule: def __getattr__(self, name): if name == "__name__": return "test" if name == "__version__": return mock.MagicMock() if name == "__libpq_version__": return 123 raise AttributeError("attribute missing") connect_module = MockConnectModule() db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "postgresql", enable_commenter=True, connect_module=connect_module, commenter_options={"db_driver": False}, enable_attribute_commenter=True, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*dbapi_level='1.0',dbapi_threadsafety='unknown',driver_paramstyle='unknown',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertRegex( span.attributes[DB_STATEMENT], r"Select 1 /\*dbapi_level='1.0',dbapi_threadsafety='unknown',driver_paramstyle='unknown',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) def test_executemany_comment_stmt_enabled_matches_db_statement_attribute( self, ): connect_module = mock.MagicMock() connect_module.__version__ = mock.MagicMock() connect_module.pq.version.return_value = 123 connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "postgresql", enable_commenter=True, commenter_options={"db_driver": False, "dbapi_level": False}, connect_module=connect_module, enable_attribute_commenter=True, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertRegex( span.attributes[DB_STATEMENT], r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/", ) cursor_span_id = re.search(r"[a-zA-Z0-9_]{16}", cursor.query).group() db_statement_span_id = re.search(r"[a-zA-Z0-9_]{16}", span.attributes[DB_STATEMENT]).group() self.assertEqual(cursor_span_id, db_statement_span_id) def test_compatible_build_version_psycopg2_libpq(self): connect_module = mock.MagicMock() connect_module.__name__ = "test" connect_module.__version__ = mock.MagicMock() connect_module.pq.version.side_effect = AttributeError connect_module.__libpq_version__ = 123 connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "postgresql", enable_commenter=True, commenter_options={"db_driver": False, "dbapi_level": False}, connect_module=connect_module, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_compatible_build_version_psycopg_libpq(self): connect_module = mock.MagicMock() connect_module.__name__ = "test" connect_module.__version__ = mock.MagicMock() connect_module.pq.version.return_value = 123 connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "postgresql", enable_commenter=True, commenter_options={"db_driver": False, "dbapi_level": False}, connect_module=connect_module, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_no_libpq_version_when_we_dont_instrument_the_root_module( self, ): connect_module = mock.MagicMock() connect_module.__name__ = "test" connect_module.__version__ = mock.MagicMock() connect_module.pq.version.side_effect = AttributeError connect_module.__libpq_version__ = None connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "postgresql", enable_commenter=True, commenter_options={"db_driver": False, "dbapi_level": False}, connect_module=connect_module, enable_attribute_commenter=True, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertRegex( span.attributes[DB_STATEMENT], r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) def test_executemany_psycopg2_integration_comment(self): connect_module = mock.MagicMock() connect_module.__name__ = "psycopg2" connect_module.__version__ = "1.2.3" connect_module.pq.version.side_effect = AttributeError connect_module.__libpq_version__ = 123 connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "postgresql", enable_commenter=True, commenter_options={"db_driver": True, "dbapi_level": False}, connect_module=connect_module, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*db_driver='psycopg2%%3A1.2.3',dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_executemany_psycopg2_integration_comment_stmt_enabled(self): connect_module = mock.MagicMock() connect_module.__name__ = "psycopg2" connect_module.__version__ = "1.2.3" connect_module.pq.version.side_effect = AttributeError connect_module.__libpq_version__ = 123 connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "postgresql", enable_commenter=True, commenter_options={"db_driver": True, "dbapi_level": False}, connect_module=connect_module, enable_attribute_commenter=True, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*db_driver='psycopg2%%3A1.2.3',dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertRegex( span.attributes[DB_STATEMENT], r"Select 1 /\*db_driver='psycopg2%%3A1.2.3',dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) def test_executemany_psycopg_integration_comment(self): connect_module = mock.MagicMock() connect_module.__name__ = "psycopg" connect_module.__version__ = "1.2.3" connect_module.pq.version.return_value = 123 connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "postgresql", enable_commenter=True, commenter_options={"db_driver": True, "dbapi_level": False}, connect_module=connect_module, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*db_driver='psycopg%%3A1.2.3',dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_executemany_psycopg_integration_comment_stmt_enabled(self): connect_module = mock.MagicMock() connect_module.__name__ = "psycopg" connect_module.__version__ = "1.2.3" connect_module.pq.version.return_value = 123 connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "postgresql", enable_commenter=True, commenter_options={"db_driver": True, "dbapi_level": False}, connect_module=connect_module, enable_attribute_commenter=True, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*db_driver='psycopg%%3A1.2.3',dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertRegex( span.attributes[DB_STATEMENT], r"Select 1 /\*db_driver='psycopg%%3A1.2.3',dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) def test_executemany_mysqlconnector_integration_comment(self): connect_module = mock.MagicMock() connect_module.__name__ = "mysql.connector" connect_module.__version__ = "1.2.3" connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "mysql", enable_commenter=True, commenter_options={"db_driver": True, "dbapi_level": False}, connect_module=connect_module, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*db_driver='mysql.connector%%3A1.2.3',dbapi_threadsafety=123,driver_paramstyle='test',mysql_client_version='1.2.3',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_executemany_mysqlconnector_integration_comment_stmt_enabled(self): connect_module = mock.MagicMock() connect_module.__name__ = "mysql.connector" connect_module.__version__ = "1.2.3" connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "mysql", enable_commenter=True, commenter_options={"db_driver": True, "dbapi_level": False}, connect_module=connect_module, enable_attribute_commenter=True, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*db_driver='mysql.connector%%3A1.2.3',dbapi_threadsafety=123,driver_paramstyle='test',mysql_client_version='1.2.3',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertRegex( span.attributes[DB_STATEMENT], r"Select 1 /\*db_driver='mysql.connector%%3A1.2.3',dbapi_threadsafety=123,driver_paramstyle='test',mysql_client_version='1.2.3',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) @mock.patch("opentelemetry.instrumentation.dbapi.util_version") def test_executemany_mysqlclient_integration_comment( self, mock_dbapi_util_version, ): mock_dbapi_util_version.return_value = "1.2.3" connect_module = mock.MagicMock() connect_module.__name__ = "MySQLdb" connect_module.__version__ = "1.2.3" connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" connect_module._mysql = mock.MagicMock() connect_module._mysql.get_client_info = mock.MagicMock(return_value="123") db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "mysql", enable_commenter=True, commenter_options={"db_driver": True, "dbapi_level": False}, connect_module=connect_module, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*db_driver='MySQLdb%%3A1.2.3',dbapi_threadsafety=123,driver_paramstyle='test',mysql_client_version='123',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) @mock.patch("opentelemetry.instrumentation.dbapi.util_version") def test_executemany_mysqlclient_integration_comment_stmt_enabled( self, mock_dbapi_util_version, ): mock_dbapi_util_version.return_value = "1.2.3" connect_module = mock.MagicMock() connect_module.__name__ = "MySQLdb" connect_module.__version__ = "1.2.3" connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" connect_module._mysql = mock.MagicMock() connect_module._mysql.get_client_info = mock.MagicMock(return_value="123") db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "mysql", enable_commenter=True, commenter_options={"db_driver": True, "dbapi_level": False}, connect_module=connect_module, enable_attribute_commenter=True, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*db_driver='MySQLdb%%3A1.2.3',dbapi_threadsafety=123,driver_paramstyle='test',mysql_client_version='123',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertRegex( span.attributes[DB_STATEMENT], r"Select 1 /\*db_driver='MySQLdb%%3A1.2.3',dbapi_threadsafety=123,driver_paramstyle='test',mysql_client_version='123',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) def test_executemany_pymysql_integration_comment(self): connect_module = mock.MagicMock() connect_module.__name__ = "pymysql" connect_module.__version__ = "1.2.3" connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" connect_module.get_client_info = mock.MagicMock(return_value="123") db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "mysql", enable_commenter=True, commenter_options={"db_driver": True, "dbapi_level": False}, connect_module=connect_module, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*db_driver='pymysql%%3A1.2.3',dbapi_threadsafety=123,driver_paramstyle='test',mysql_client_version='123',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_executemany_pymysql_integration_comment_stmt_enabled(self): connect_module = mock.MagicMock() connect_module.__name__ = "pymysql" connect_module.__version__ = "1.2.3" connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" connect_module.get_client_info = mock.MagicMock(return_value="123") db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "mysql", enable_commenter=True, commenter_options={"db_driver": True, "dbapi_level": False}, connect_module=connect_module, enable_attribute_commenter=True, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*db_driver='pymysql%%3A1.2.3',dbapi_threadsafety=123,driver_paramstyle='test',mysql_client_version='123',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertRegex( span.attributes[DB_STATEMENT], r"Select 1 /\*db_driver='pymysql%%3A1.2.3',dbapi_threadsafety=123,driver_paramstyle='test',mysql_client_version='123',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) def test_executemany_flask_integration_comment(self): connect_module = mock.MagicMock() connect_module.__name__ = "test" connect_module.pq.version.return_value = 123 connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "postgresql", enable_commenter=True, commenter_options={"db_driver": False, "dbapi_level": False}, connect_module=connect_module, ) current_context = context.get_current() sqlcommenter_context = context.set_value("SQLCOMMENTER_ORM_TAGS_AND_VALUES", {"flask": 1}, current_context) token = context.attach(sqlcommenter_context) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',flask=1,libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) context.detach(token) def test_executemany_flask_integration_comment_stmt_enabled(self): connect_module = mock.MagicMock() connect_module.__name__ = "test" connect_module.__version__ = mock.MagicMock() connect_module.pq.version.return_value = 123 connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "postgresql", enable_commenter=True, commenter_options={"db_driver": False, "dbapi_level": False}, connect_module=connect_module, enable_attribute_commenter=True, ) current_context = context.get_current() sqlcommenter_context = context.set_value("SQLCOMMENTER_ORM_TAGS_AND_VALUES", {"flask": 1}, current_context) token = context.attach(sqlcommenter_context) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.executemany("Select 1;") self.assertRegex( cursor.query, r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',flask=1,libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertRegex( span.attributes[DB_STATEMENT], r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',flask=1,libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) context.detach(token) def test_callproc(self): db_integration = dbapi.DatabaseApiIntegration("instrumenting_module_test_name", "testcomponent") mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.callproc("Test stored procedure") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Test stored procedure", ) @mock.patch("opentelemetry.instrumentation.dbapi") def test_wrap_connect(self, mock_dbapi): dbapi.wrap_connect(self.tracer, mock_dbapi, "connect", "-") connection = mock_dbapi.connect() self.assertEqual(mock_dbapi.connect.call_count, 1) self.assertIsInstance(connection.__wrapped__, mock.Mock) # pylint: disable=no-member @mock.patch("opentelemetry.instrumentation.dbapi") def test_unwrap_connect(self, mock_dbapi): dbapi.wrap_connect(self.tracer, mock_dbapi, "connect", "-") connection = mock_dbapi.connect() self.assertEqual(mock_dbapi.connect.call_count, 1) dbapi.unwrap_connect(mock_dbapi, "connect") connection = mock_dbapi.connect() self.assertEqual(mock_dbapi.connect.call_count, 2) self.assertIsInstance(connection, mock.Mock) def test_instrument_connection(self): mocked_conn = MockConnection("dbname", "999", "dbhost", "dbuser") # Avoid get_attributes failing because can't concatenate mock connection2 = dbapi.instrument_connection("instrumenting_module_test_name", mocked_conn, "dbname") self.assertIs(connection2.__wrapped__, mocked_conn) # pylint: disable=no-member @mock.patch("opentelemetry.instrumentation.dbapi.DatabaseApiIntegration") def test_instrument_connection_kwargs_defaults(self, mock_dbapiint): mocked_conn = MockConnection("dbname", "999", "dbhost", "dbuser") dbapi.instrument_connection("instrumenting_module_test_name", mocked_conn, "foo") kwargs = mock_dbapiint.call_args[1] self.assertEqual(kwargs["connection_attributes"], None) self.assertEqual(kwargs["version"], "") self.assertEqual(kwargs["tracer_provider"], None) self.assertEqual(kwargs["capture_parameters"], False) self.assertEqual(kwargs["enable_commenter"], False) self.assertEqual(kwargs["commenter_options"], None) self.assertEqual(kwargs["connect_module"], None) self.assertEqual(kwargs["enable_attribute_commenter"], False) @mock.patch("opentelemetry.instrumentation.dbapi.DatabaseApiIntegration") def test_instrument_connection_kwargs_provided(self, mock_dbapiint): mocked_conn = MockConnection("dbname", "999", "dbhost", "dbuser") mock_tracer_provider = mock.MagicMock() mock_connect_module = mock.MagicMock() dbapi.instrument_connection( "instrumenting_module_test_name", mocked_conn, "foo", connection_attributes={"foo": "bar"}, version="test", tracer_provider=mock_tracer_provider, capture_parameters=True, enable_commenter=True, commenter_options={"foo": "bar"}, connect_module=mock_connect_module, enable_attribute_commenter=True, ) kwargs = mock_dbapiint.call_args[1] self.assertEqual(kwargs["connection_attributes"], {"foo": "bar"}) self.assertEqual(kwargs["version"], "test") self.assertIs(kwargs["tracer_provider"], mock_tracer_provider) self.assertEqual(kwargs["capture_parameters"], True) self.assertEqual(kwargs["enable_commenter"], True) self.assertEqual(kwargs["commenter_options"], {"foo": "bar"}) self.assertIs(kwargs["connect_module"], mock_connect_module) self.assertEqual(kwargs["enable_attribute_commenter"], True) def test_instrument_connection_db_api_integration_factory(self): mocked_conn = MockConnection("dbname", "999", "dbhost", "dbuser") class DBApiIntegrationTestClass(dbapi.DatabaseApiIntegration): pass conn = dbapi.instrument_connection( "instrumenting_module_test_name", mocked_conn, "dbsystem", db_api_integration_factory=DBApiIntegrationTestClass, ) self.assertIsInstance(conn._self_db_api_integration, DBApiIntegrationTestClass) def test_uninstrument_connection(self): mocked_conn = MockConnection("dbname", "999", "dbhost", "dbuser") connection2 = dbapi.instrument_connection("instrumenting_module_test_name", mocked_conn, "-") self.assertIs(connection2.__wrapped__, mocked_conn) # pylint: disable=no-member connection3 = dbapi.uninstrument_connection(connection2) self.assertIs(connection3, mocked_conn) with self.assertLogs(level=logging.WARNING): connection4 = dbapi.uninstrument_connection(mocked_conn) self.assertIs(connection4, mocked_conn) def test_non_string_sql_conversion(self): """Test that non-string SQL statements are converted to strings before adding comments.""" # Create a mock connect_module with required attributes connect_module = mock.MagicMock() connect_module.__name__ = "test" connect_module.__version__ = "1.0" connect_module.pq.version.return_value = 123 connect_module.apilevel = "2.0" connect_module.threadsafety = 1 connect_module.paramstyle = "test" db_integration = dbapi.DatabaseApiIntegration( "testname", "postgresql", enable_commenter=True, connect_module=connect_module, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() input_sql = mock.MagicMock(as_string=lambda conn: "SELECT 2") expected_sql = "SELECT 2" cursor.executemany(input_sql) # Extract the SQL part (before the comment) actual_sql = cursor.query.split("/*")[0].strip() self.assertEqual(actual_sql, expected_sql) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) def test_capture_mysql_version_primary_success(self): connect_module = mock.MagicMock() connect_module.__name__ = "mysql.connector" connect_module.__version__ = "2.2.9" db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "mysql", enable_commenter=True, connect_module=connect_module, ) mock_cursor = mock.MagicMock() mock_cursor._cnx._cmysql.get_client_info.return_value = "8.0.32" mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor._cnx = mock_cursor._cnx cursor.execute("SELECT 1;") mock_cursor._cnx._cmysql.get_client_info.assert_called_once() self.assertEqual(db_integration.commenter_data["mysql_client_version"], "8.0.32") def test_capture_mysql_version_fallback_success(self): connect_module = mock.MagicMock() connect_module.__name__ = "mysql.connector" connect_module.__version__ = "2.2.9" db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "mysql", enable_commenter=True, connect_module=connect_module, ) mock_cursor = mock.MagicMock() mock_cursor._cnx._cmysql.get_client_info.side_effect = AttributeError("Primary method failed") mock_cursor._connection._cmysql.get_client_info.return_value = "8.0.33" mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor._cnx = mock_cursor._cnx cursor._connection = mock_cursor._connection cursor.execute("SELECT 1;") mock_cursor._cnx._cmysql.get_client_info.assert_called_once() mock_cursor._connection._cmysql.get_client_info.assert_called_once() self.assertEqual(db_integration.commenter_data["mysql_client_version"], "8.0.33") @mock.patch("opentelemetry.instrumentation.dbapi._logger") def test_capture_mysql_version_fallback(self, mock_logger): connect_module = mock.MagicMock() connect_module.__name__ = "mysql.connector" connect_module.__version__ = "2.2.9" db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "mysql", enable_commenter=True, connect_module=connect_module, ) mock_cursor = mock.MagicMock() mock_cursor._cnx._cmysql.get_client_info.side_effect = AttributeError("Primary method failed") mock_cursor._connection._cmysql.get_client_info.side_effect = AttributeError("Fallback method failed") mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor._cnx = mock_cursor._cnx cursor._connection = mock_cursor._connection cursor.execute("SELECT 1;") mock_cursor._cnx._cmysql.get_client_info.assert_called_once() mock_cursor._connection._cmysql.get_client_info.assert_called_once() mock_logger.debug.assert_called_once() self.assertEqual(db_integration.commenter_data["mysql_client_version"], "unknown") @pytest.mark.skipif( sys.version_info < (3, 14), reason="requires Python 3.14+ for t-strings", ) def test_t_string_span_attributes(self): # pylint: disable-next=import-outside-toplevel,no-name-in-module from string.templatelib import ( # noqa: PLC0415 Interpolation, Template, ) connection_props = _get_default_connection_props() connection_attributes = _get_default_connection_attributes() db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", connection_attributes, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, connection_props) cursor = mock_connection.cursor() value = 42 template = Template("SELECT ", Interpolation(value, "value"), " FROM foo") cursor.execute(template) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "SELECT") self.assertEqual(span.attributes[DB_STATEMENT], "SELECT {value} FROM foo") @pytest.mark.skipif( sys.version_info < (3, 14), reason="requires Python 3.14+ for t-strings", ) def test_t_string_commenter(self): # pylint: disable-next=import-outside-toplevel,no-name-in-module from string.templatelib import Interpolation, Template # noqa: PLC0415 connect_module = mock.MagicMock() connect_module.__name__ = "test" connect_module.__version__ = mock.MagicMock() connect_module.pq.version.return_value = 123 connect_module.apilevel = 123 connect_module.threadsafety = 123 connect_module.paramstyle = "test" db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "postgresql", enable_commenter=True, commenter_options={"db_driver": False, "dbapi_level": False}, connect_module=connect_module, ) mock_connection = db_integration.wrapped_connection(mock_connect, {}, {}) cursor = mock_connection.cursor() value = 42 template = Template("SELECT ", Interpolation(value, "value"), " FROM foo") cursor.executemany(template) self.assertIsInstance(cursor.query, Template) self.assertRegex( cursor.query.strings[-1], r" FROM foo /\*dbapi_threadsafety=123,driver_paramstyle='test'," r"libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "SELECT") self.assertEqual(span.attributes[DB_STATEMENT], "SELECT {value} FROM foo") # pylint: disable=unused-argument def mock_connect(*args, **kwargs): database = kwargs.get("database") server_host = kwargs.get("server_host") server_port = kwargs.get("server_port") user = kwargs.get("user") return MockConnection(database, server_port, server_host, user) class MockConnection: def __init__(self, database, server_port, server_host, user): self.database = database self.server_port = server_port self.server_host = server_host self.user = user # pylint: disable=no-self-use def cursor(self): return MockCursor() class MockCursor: def __init__(self) -> None: self.query = "" self.params = None self.connection = None self.rowcount = -1 # Mock mysql.connector modules and method self._cnx = mock.MagicMock() self._cnx._cmysql = mock.MagicMock() self._cnx._cmysql.get_client_info = mock.MagicMock(return_value="1.2.3") self._items = [] # pylint: disable=unused-argument, no-self-use def execute(self, query, params=None, throw_exception=False, rowcount=-1): if throw_exception: # pylint: disable=broad-exception-raised raise Exception("Test Exception") self.rowcount = rowcount def __iter__(self): yield from self._items # pylint: disable=unused-argument, no-self-use def executemany(self, query, params=None, throw_exception=False, rowcount=-1): if throw_exception: # pylint: disable=broad-exception-raised raise Exception("Test Exception") self.query = query self.params = params self.rowcount = rowcount # pylint: disable=unused-argument, no-self-use def callproc(self, query, params=None, throw_exception=False, rowcount=-1): if throw_exception: # pylint: disable=broad-exception-raised raise Exception("Test Exception") self.rowcount = rowcount opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/000077500000000000000000000000001527070072300324055ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/LICENSE000066400000000000000000000261351527070072300334210ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/README.rst000066400000000000000000000013671527070072300341030ustar00rootroot00000000000000OpenTelemetry Django Tracing ============================ |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-django.svg :target: https://pypi.org/project/opentelemetry-instrumentation-django/ This library allows tracing requests for Django applications. Installation ------------ :: pip install opentelemetry-instrumentation-django References ---------- * `Django `_ * `OpenTelemetry Instrumentation for Django `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000033531527070072300352460ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-django" dynamic = ["version"] description = "OpenTelemetry Instrumentation for Django" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-instrumentation-wsgi == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-util-http == 0.66b2", ] [project.optional-dependencies] asgi = [ "opentelemetry-instrumentation-asgi == 0.66b2", ] instruments = [ "django >= 2.0", ] [project.entry-points.opentelemetry_instrumentor] django = "opentelemetry.instrumentation.django:DjangoInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-django" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/django/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/src/000077500000000000000000000000001527070072300331745ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300360115ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/srcinstrumentation/000077500000000000000000000000001527070072300412545ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/src/opentelemetrydjango/000077500000000000000000000000001527070072300425165ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation__init__.py000066400000000000000000000533411527070072300446350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Instrument `django`_ to trace Django applications. .. _django: https://pypi.org/project/django/ Usage ----- .. code:: python from opentelemetry.instrumentation.django import DjangoInstrumentor DjangoInstrumentor().instrument() ASGI support ------------ When running a Django application through ASGI, install the optional ASGI dependency so this instrumentation can use the ASGI context helpers: .. code:: console pip install "opentelemetry-instrumentation-django[asgi]" Then instrument Django the same way as a WSGI application. Configuration ------------- Exclude lists ************* To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_DJANGO_EXCLUDED_URLS`` (or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the URLs. For example, :: export OTEL_PYTHON_DJANGO_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. Request attributes ****************** To extract attributes from Django's request object and use them as span attributes, set the environment variable ``OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS`` to a comma delimited list of request attribute names. For example, :: export OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS='path_info,content_type' will extract the ``path_info`` and ``content_type`` attributes from every traced request and add them as span attributes. * `Django Request object reference `_ Request and Response hooks ************************** This instrumentation supports request and response hooks. These are functions that get called right after a span is created for a request and right before the span is finished for the response. The hooks can be configured as follows: .. code:: python from opentelemetry.instrumentation.django import DjangoInstrumentor def request_hook(span, request): pass def response_hook(span, request, response): pass DjangoInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook) * `Django Request object `_ * `Django Response object `_ Adding attributes from middleware context ######################################### In many Django applications, certain request attributes become available only *after* specific middlewares have executed. For example: - ``django.contrib.auth.middleware.AuthenticationMiddleware`` populates ``request.user`` - ``django.contrib.sites.middleware.CurrentSiteMiddleware`` populates ``request.site`` Because the OpenTelemetry instrumentation creates the span **before** Django middlewares run, these attributes are **not yet available** in the ``request_hook`` stage. Therefore, such attributes should be safely attached in the **response_hook**, which executes after Django finishes processing the request (and after all middlewares have completed). Example: Attaching the authenticated user and current site to the span: .. code:: python def response_hook(span, request, response): # Attach user information if available if request.user.is_authenticated: span.set_attribute("enduser.id", request.user.pk) span.set_attribute("enduser.username", request.user.get_username()) # Attach current site (if provided by CurrentSiteMiddleware) if hasattr(request, "site"): span.set_attribute("site.id", getattr(request.site, "pk", None)) span.set_attribute("site.domain", getattr(request.site, "domain", None)) DjangoInstrumentor().instrument(response_hook=response_hook) This ensures that middleware-dependent context (like user or site information) is properly recorded once Django’s middleware stack has finished execution. Custom Django middleware can also attach arbitrary data to the ``request`` object, which can later be included as span attributes in the ``response_hook``. * `Django middleware reference `_ Best practices ############## - Use **response_hook** (not request_hook) when accessing attributes added by Django middlewares. - Common middleware-provided attributes include: - ``request.user`` (AuthenticationMiddleware) - ``request.site`` (CurrentSiteMiddleware) - Avoid adding large or sensitive data (e.g., passwords, session tokens, PII) to spans. - Use **namespaced attribute keys**, e.g., ``enduser.*``, ``site.*``, or ``custom.*``, for clarity. - Hooks should execute quickly — avoid blocking or long-running operations. - Hooks can be safely combined with OpenTelemetry **Context propagation** or **Baggage** for consistent tracing across services. * `OpenTelemetry semantic conventions `_ Middleware execution order ########################## In Django’s request lifecycle, the OpenTelemetry `request_hook` is executed before the first middleware runs. Therefore: - At `request_hook` time → only the bare `HttpRequest` object is available. - After middlewares → `request.user`, `request.site` etc. become available. - At `response_hook` time → all middlewares (including authentication and site middlewares) have already run, making it the correct place to attach these attributes. Developers who need to trace attributes from middlewares should always use `response_hook` to ensure complete and accurate span data. Capture HTTP request and response headers ***************************************** You can configure the agent to capture specified HTTP headers as span attributes, according to the `semantic conventions `_. Request headers *************** To capture HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. Request header names in Django are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" Would match all request headers that start with ``Accept`` and ``X-``. To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*" The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a single item list containing all the header values. For example: ``http.request.header.custom_request_header = [","]`` Response headers **************** To capture HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. Response header names in Django are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" Would match all response headers that start with ``Content`` and ``X-``. To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*" The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a single item list containing all the header values. For example: ``http.response.header.custom_response_header = [","]`` Sanitizing headers ****************** In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be matched in a case-insensitive manner. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. Note: The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. SQLCommenter ************ You can optionally enable sqlcommenter which enriches the query with contextual information. Queries made after setting up trace integration with sqlcommenter enabled will have configurable key-value pairs appended to them, e.g. ``Users().objects.all()`` will result in ``"select * from auth_users; /*traceparent=00-01234567-abcd-01*/"``. This supports context propagation between database client and server when database log records are enabled. For more information, see: * `Semantic Conventions - Database Spans `_ * `sqlcommenter `_ .. code:: python from opentelemetry.instrumentation.django import DjangoInstrumentor DjangoInstrumentor().instrument(is_sql_commentor_enabled=True) Warning: Duplicate sqlcomments may be appended to the sqlquery log if DjangoInstrumentor sqlcommenter is enabled in addition to sqlcommenter for an active instrumentation of a database driver or object-relational mapper (ORM) in the same database client stack. For example, if psycopg2 driver is used and Psycopg2Instrumentor has sqlcommenter enabled, then both DjangoInstrumentor and Psycopg2Instrumentor will append comments to the query statement. SQLCommenter with commenter_options *********************************** The key-value pairs appended to the query can be configured using variables in Django ``settings.py``. When sqlcommenter is enabled, all available KVs/tags are calculated by default, i.e. ``True`` for each. The ``settings.py`` values support *opting out* of specific KVs. Available settings.py commenter options ####################################### We can configure the tags to be appended to the sqlquery log by adding below variables to ``settings.py``, e.g. ``SQLCOMMENTER_WITH_FRAMEWORK = False`` +-------------------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``settings.py`` variable | Description | Example | +=====================================+===========================================================+===========================================================================+ | ``SQLCOMMENTER_WITH_FRAMEWORK`` | Django framework name with version (URL encoded). | ``framework='django%%%%3A4.2.0'`` | +-------------------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``SQLCOMMENTER_WITH_CONTROLLER`` | Django controller/view name that handles the request. | ``controller='index'`` | +-------------------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``SQLCOMMENTER_WITH_ROUTE`` | URL path pattern that handles the request. | ``route='polls/'`` | +-------------------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``SQLCOMMENTER_WITH_APP_NAME`` | Django app name that handles the request. | ``app_name='polls'`` | +-------------------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``SQLCOMMENTER_WITH_OPENTELEMETRY`` | OpenTelemetry context as traceparent at time of query. | ``traceparent='00-fd720cffceba94bbf75940ff3caaf3cc-4fd1a2bdacf56388-01'`` | +-------------------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``SQLCOMMENTER_WITH_DB_DRIVER`` | Database driver name used by Django. | ``db_driver='django.db.backends.postgresql'`` | +-------------------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ API --- """ from collections.abc import Collection from logging import getLogger from os import environ from django.conf import settings from django.core.exceptions import ImproperlyConfigured from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, _get_schema_url, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _report_new, _report_old, ) from opentelemetry.instrumentation.django.environment_variables import ( OTEL_PYTHON_DJANGO_INSTRUMENT, ) from opentelemetry.instrumentation.django.middleware.otel_middleware import ( _DjangoMiddleware, ) from opentelemetry.instrumentation.django.package import _instruments from opentelemetry.instrumentation.django.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.metrics import get_meter from opentelemetry.semconv._incubating.metrics.http_metrics import ( create_http_server_active_requests, ) from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.semconv.metrics.http_metrics import ( HTTP_SERVER_REQUEST_DURATION, ) from opentelemetry.trace import get_tracer from opentelemetry.util.http import get_excluded_urls, parse_excluded_urls _excluded_urls_from_env = get_excluded_urls("DJANGO") _django_middleware_setting = "MIDDLEWARE" _logger = getLogger(__name__) def _get_django_otel_middleware_position(middleware_length, default_middleware_position=0): otel_position = environ.get("OTEL_PYTHON_DJANGO_MIDDLEWARE_POSITION") try: middleware_position = int(otel_position) except (ValueError, TypeError): _logger.debug( "Invalid OTEL_PYTHON_DJANGO_MIDDLEWARE_POSITION value: (%s). Using default position: %d.", otel_position, default_middleware_position, ) middleware_position = default_middleware_position if middleware_position < 0 or middleware_position > middleware_length: _logger.debug( "Middleware position %d is out of range (0-%d). Using 0 as the position", middleware_position, middleware_length, ) middleware_position = 0 return middleware_position class DjangoInstrumentor(BaseInstrumentor): """An instrumentor for Django See `BaseInstrumentor` """ _opentelemetry_middleware = ".".join([_DjangoMiddleware.__module__, _DjangoMiddleware.__qualname__]) _sql_commenter_middleware = "opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware.SqlCommenter" def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): # FIXME this is probably a pattern that will show up in the rest of the # ext. Find a better way of implementing this. if environ.get(OTEL_PYTHON_DJANGO_INSTRUMENT) == "False": return # initialize semantic conventions opt-in if needed _OpenTelemetrySemanticConventionStability._initialize() sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) tracer_provider = kwargs.get("tracer_provider") meter_provider = kwargs.get("meter_provider") _excluded_urls = kwargs.get("excluded_urls") tracer = get_tracer( __name__, __version__, tracer_provider=tracer_provider, schema_url=_get_schema_url(sem_conv_opt_in_mode), ) meter = get_meter( __name__, __version__, meter_provider=meter_provider, schema_url=_get_schema_url(sem_conv_opt_in_mode), ) _DjangoMiddleware._sem_conv_opt_in_mode = sem_conv_opt_in_mode _DjangoMiddleware._tracer = tracer _DjangoMiddleware._meter = meter _DjangoMiddleware._excluded_urls = ( _excluded_urls_from_env if _excluded_urls is None else parse_excluded_urls(_excluded_urls) ) _DjangoMiddleware._otel_request_hook = kwargs.pop("request_hook", None) _DjangoMiddleware._otel_response_hook = kwargs.pop("response_hook", None) _DjangoMiddleware._duration_histogram_old = None if _report_old(sem_conv_opt_in_mode): _DjangoMiddleware._duration_histogram_old = meter.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", description="Measures the duration of inbound HTTP requests.", ) _DjangoMiddleware._duration_histogram_new = None if _report_new(sem_conv_opt_in_mode): _DjangoMiddleware._duration_histogram_new = meter.create_histogram( name=HTTP_SERVER_REQUEST_DURATION, description="Duration of HTTP server requests.", unit="s", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) _DjangoMiddleware._active_request_counter = create_http_server_active_requests(meter) # This can not be solved, but is an inherent problem of this approach: # the order of middleware entries matters, and here you have no control # on that: # https://docs.djangoproject.com/en/3.0/topics/http/middleware/#activating-middleware # https://docs.djangoproject.com/en/3.0/ref/middleware/#middleware-ordering settings_middleware = [] try: settings_middleware = getattr(settings, _django_middleware_setting, []) except ImproperlyConfigured as exception: _logger.debug( "DJANGO_SETTINGS_MODULE environment variable not configured. Defaulting to empty settings: %s", exception, ) settings.configure() settings_middleware = getattr(settings, _django_middleware_setting, []) except ModuleNotFoundError as exception: _logger.debug( "DJANGO_SETTINGS_MODULE points to a non-existent module. Defaulting to empty settings: %s", exception, ) settings.configure() settings_middleware = getattr(settings, _django_middleware_setting, []) # Django allows to specify middlewares as a tuple, so we convert this tuple to a # list, otherwise we wouldn't be able to call append/remove if isinstance(settings_middleware, tuple): settings_middleware = list(settings_middleware) is_sql_commentor_enabled = kwargs.pop("is_sql_commentor_enabled", None) middleware_position = _get_django_otel_middleware_position( len(settings_middleware), kwargs.pop("middleware_position", 0) ) if is_sql_commentor_enabled: settings_middleware.insert(middleware_position, self._sql_commenter_middleware) settings_middleware.insert(middleware_position, self._opentelemetry_middleware) setattr(settings, _django_middleware_setting, settings_middleware) def _uninstrument(self, **kwargs): settings_middleware = getattr(settings, _django_middleware_setting, None) # FIXME This is starting to smell like trouble. We have 2 mechanisms # that may make this condition be True, one implemented in # BaseInstrumentor and another one implemented in _instrument. Both # stop _instrument from running and thus, settings_middleware not being # set. if settings_middleware is None or (self._opentelemetry_middleware not in settings_middleware): return settings_middleware.remove(self._opentelemetry_middleware) setattr(settings, _django_middleware_setting, settings_middleware) environment_variables.py000066400000000000000000000002151527070072300474620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 OTEL_PYTHON_DJANGO_INSTRUMENT = "OTEL_PYTHON_DJANGO_INSTRUMENT" middleware/000077500000000000000000000000001527070072300446335ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django__init__.py000066400000000000000000000000001527070072300467320ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middlewareotel_middleware.py000066400000000000000000000402361527070072300503520ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import types from collections.abc import Callable from logging import getLogger from time import time from timeit import default_timer from django import VERSION as django_version from django.http import HttpRequest, HttpResponse from opentelemetry.context import detach from opentelemetry.instrumentation._semconv import ( _filter_semconv_active_request_count_attr, _filter_semconv_duration_attrs, _report_new, _report_old, _server_active_requests_count_attrs_new, _server_active_requests_count_attrs_old, _server_duration_attrs_new, _server_duration_attrs_old, _StabilityMode, ) from opentelemetry.instrumentation.propagators import ( get_global_response_propagator, ) from opentelemetry.instrumentation.utils import ( _start_internal_or_server_span, extract_attributes_from_object, ) from opentelemetry.instrumentation.wsgi import ( add_response_attributes, wsgi_getter, ) from opentelemetry.instrumentation.wsgi import ( collect_custom_request_headers_attributes as wsgi_collect_custom_request_headers_attributes, ) from opentelemetry.instrumentation.wsgi import ( collect_custom_response_headers_attributes as wsgi_collect_custom_response_headers_attributes, ) from opentelemetry.instrumentation.wsgi import ( collect_request_attributes as wsgi_collect_request_attributes, ) from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_TARGET, ) from opentelemetry.semconv.attributes.http_attributes import HTTP_ROUTE from opentelemetry.trace import Span, SpanKind, use_span from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, SanitizeValue, get_custom_headers, get_excluded_urls, get_traced_request_attrs, normalise_request_header_name, normalise_response_header_name, sanitize_method, ) try: from django.core.urlresolvers import ( # pylint: disable=no-name-in-module Resolver404, resolve, ) except ImportError: from django.urls import Resolver404, resolve DJANGO_3_0 = django_version >= (3, 0) if DJANGO_3_0: from django.core.handlers.asgi import ASGIRequest else: ASGIRequest = None # try/except block exclusive for optional ASGI imports. try: from opentelemetry.instrumentation.asgi import ( asgi_getter, asgi_setter, set_status_code, ) from opentelemetry.instrumentation.asgi import ( collect_custom_headers_attributes as asgi_collect_custom_headers_attributes, ) from opentelemetry.instrumentation.asgi import ( collect_request_attributes as asgi_collect_request_attributes, ) _is_asgi_supported = True except ImportError: asgi_getter = None asgi_collect_request_attributes = None set_status_code = None _is_asgi_supported = False _logger = getLogger(__name__) def _is_asgi_request(request: HttpRequest) -> bool: return ASGIRequest is not None and isinstance(request, ASGIRequest) class _DjangoMiddleware: """Django Middleware for OpenTelemetry""" _environ_activation_key = "opentelemetry-instrumentor-django.activation_key" _environ_token = "opentelemetry-instrumentor-django.token" _environ_span_key = "opentelemetry-instrumentor-django.span_key" _environ_exception_key = "opentelemetry-instrumentor-django.exception_key" _environ_active_request_attr_key = "opentelemetry-instrumentor-django.active_request_attr_key" _environ_duration_attr_key = "opentelemetry-instrumentor-django.duration_attr_key" _environ_timer_key = "opentelemetry-instrumentor-django.timer_key" _traced_request_attrs = get_traced_request_attrs("DJANGO") _excluded_urls = get_excluded_urls("DJANGO") _tracer = None _meter = None _duration_histogram_old = None _duration_histogram_new = None _active_request_counter = None _sem_conv_opt_in_mode = _StabilityMode.DEFAULT _otel_request_hook: Callable[[Span, HttpRequest], None] = None _otel_response_hook: Callable[[Span, HttpRequest, HttpResponse], None] = None def __init__(self, get_response): self.get_response = get_response def __call__(self, request): self.process_request(request) response = self.get_response(request) return self.process_response(request, response) @staticmethod def _get_span_name(request): method = sanitize_method(request.method.strip()) if method == "_OTHER": return "HTTP" try: if request.resolver_match: match = request.resolver_match else: match = resolve(request.path) if hasattr(match, "route") and match.route: return f"{method} {match.route}" if hasattr(match, "url_name") and match.url_name: return f"{method} {match.url_name}" return request.method except Resolver404: return request.method # pylint: disable=too-many-locals # pylint: disable=too-many-branches def process_request(self, request): # request.META is a dictionary containing all available HTTP headers # Read more about request.META here: # https://docs.djangoproject.com/en/3.0/ref/request-response/#django.http.HttpRequest.META if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): return is_asgi_request = _is_asgi_request(request) if not _is_asgi_supported and is_asgi_request: return # pylint:disable=W0212 request._otel_start_time = time() request_meta = request.META if is_asgi_request: carrier = request.scope carrier_getter = asgi_getter collect_request_attributes = asgi_collect_request_attributes else: carrier = request_meta carrier_getter = wsgi_getter collect_request_attributes = wsgi_collect_request_attributes attributes = collect_request_attributes( carrier, self._sem_conv_opt_in_mode, ) span, token = _start_internal_or_server_span( tracer=self._tracer, span_name=self._get_span_name(request), start_time=request_meta.get("opentelemetry-instrumentor-django.starttime_key"), context_carrier=carrier, context_getter=carrier_getter, attributes=attributes, ) active_requests_count_attrs = _parse_active_request_count_attrs( attributes, self._sem_conv_opt_in_mode, ) request.META[self._environ_active_request_attr_key] = active_requests_count_attrs # Pass all of attributes to duration key because we will filter during response request.META[self._environ_duration_attr_key] = attributes self._active_request_counter.add(1, active_requests_count_attrs) if span.is_recording(): attributes = extract_attributes_from_object(request, self._traced_request_attrs, attributes) if is_asgi_request: # ASGI requests include extra attributes in request.scope.headers. attributes = extract_attributes_from_object( types.SimpleNamespace( **{ name.decode("latin1"): value.decode("latin1") for name, value in request.scope.get("headers", []) } ), self._traced_request_attrs, attributes, ) if span.is_recording() and span.kind == SpanKind.SERVER: attributes.update( asgi_collect_custom_headers_attributes( carrier, SanitizeValue( get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS) ), get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST), normalise_request_header_name, ) ) else: if span.is_recording() and span.kind == SpanKind.SERVER: custom_attributes = wsgi_collect_custom_request_headers_attributes(carrier) if len(custom_attributes) > 0: span.set_attributes(custom_attributes) for key, value in attributes.items(): span.set_attribute(key, value) activation = use_span(span, end_on_exit=True) activation.__enter__() # pylint: disable=unnecessary-dunder-call request_start_time = default_timer() request.META[self._environ_timer_key] = request_start_time request.META[self._environ_activation_key] = activation request.META[self._environ_span_key] = span if token: request.META[self._environ_token] = token if _DjangoMiddleware._otel_request_hook: try: _DjangoMiddleware._otel_request_hook( # pylint: disable=not-callable span, request ) except Exception: # pylint: disable=broad-exception-caught # Raising an exception here would leak the request span since process_response # would not be called. Log the exception instead. _logger.exception("Exception raised by request_hook") # pylint: disable=unused-argument def process_view(self, request, view_func, *args, **kwargs): # Process view is executed before the view function, here we get the # route template from request.resolver_match. It is not set yet in process_request if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): return if self._environ_activation_key in request.META.keys() and self._environ_span_key in request.META.keys(): span = request.META[self._environ_span_key] match = getattr(request, "resolver_match", None) if match: route = getattr(match, "route", None) if route: if span.is_recording(): # http.route is present for both old and new semconv span.set_attribute(HTTP_ROUTE, route) duration_attrs = request.META[self._environ_duration_attr_key] if _report_old(self._sem_conv_opt_in_mode): duration_attrs[HTTP_TARGET] = route if _report_new(self._sem_conv_opt_in_mode): duration_attrs[HTTP_ROUTE] = route def process_exception(self, request, exception): if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): return if self._environ_activation_key in request.META.keys(): request.META[self._environ_exception_key] = exception # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements def process_response(self, request, response): if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): return response is_asgi_request = _is_asgi_request(request) if not _is_asgi_supported and is_asgi_request: return response activation = request.META.pop(self._environ_activation_key, None) span = request.META.pop(self._environ_span_key, None) active_requests_count_attrs = request.META.pop(self._environ_active_request_attr_key, None) duration_attrs = request.META.pop(self._environ_duration_attr_key, None) request_start_time = request.META.pop(self._environ_timer_key, None) if activation and span: if is_asgi_request: set_status_code( span, response.status_code, metric_attributes=duration_attrs, sem_conv_opt_in_mode=self._sem_conv_opt_in_mode, ) if span.is_recording() and span.kind == SpanKind.SERVER: custom_headers = {} for key, value in response.items(): asgi_setter.set(custom_headers, key, value) custom_res_attributes = asgi_collect_custom_headers_attributes( custom_headers, SanitizeValue(get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS)), get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE), normalise_response_header_name, ) for key, value in custom_res_attributes.items(): span.set_attribute(key, value) else: add_response_attributes( span, f"{response.status_code} {response.reason_phrase}", response.items(), duration_attrs=duration_attrs, sem_conv_opt_in_mode=self._sem_conv_opt_in_mode, ) if span.is_recording() and span.kind == SpanKind.SERVER: custom_attributes = wsgi_collect_custom_response_headers_attributes(response.items()) if len(custom_attributes) > 0: span.set_attributes(custom_attributes) propagator = get_global_response_propagator() if propagator: propagator.inject(response) # record any exceptions raised while processing the request exception = request.META.pop(self._environ_exception_key, None) if _DjangoMiddleware._otel_response_hook: try: _DjangoMiddleware._otel_response_hook( # pylint: disable=not-callable span, request, response ) except Exception: # pylint: disable=broad-exception-caught _logger.exception("Exception raised by response_hook") if request_start_time is not None: duration_s = default_timer() - request_start_time if self._duration_histogram_old: duration_attrs_old = _parse_duration_attrs(duration_attrs, _StabilityMode.DEFAULT) # http.target to be included in old semantic conventions target = duration_attrs.get(HTTP_TARGET) if target: duration_attrs_old[HTTP_TARGET] = target self._duration_histogram_old.record( max(round(duration_s * 1000), 0), duration_attrs_old, ) if self._duration_histogram_new: duration_attrs_new = _parse_duration_attrs(duration_attrs, _StabilityMode.HTTP) self._duration_histogram_new.record( max(duration_s, 0), duration_attrs_new, ) self._active_request_counter.add(-1, active_requests_count_attrs) if activation and span: if exception: activation.__exit__( type(exception), exception, getattr(exception, "__traceback__", None), ) else: activation.__exit__(None, None, None) if request.META.get(self._environ_token, None) is not None: detach(request.META.get(self._environ_token)) request.META.pop(self._environ_token) return response def _parse_duration_attrs(req_attrs, sem_conv_opt_in_mode=_StabilityMode.DEFAULT): return _filter_semconv_duration_attrs( req_attrs, _server_duration_attrs_old, _server_duration_attrs_new, sem_conv_opt_in_mode, ) def _parse_active_request_count_attrs(req_attrs, sem_conv_opt_in_mode=_StabilityMode.DEFAULT): return _filter_semconv_active_request_count_attr( req_attrs, _server_active_requests_count_attrs_old, _server_active_requests_count_attrs_new, sem_conv_opt_in_mode, ) sqlcommenter_middleware.py000066400000000000000000000067021527070072300521200ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from contextlib import ExitStack from logging import getLogger from typing import Any, TypeVar # pylint: disable=no-name-in-module from django import conf, get_version from django.db import connections from opentelemetry.instrumentation.sqlcommenter_utils import _add_sql_comment from opentelemetry.instrumentation.utils import _get_opentelemetry_values from opentelemetry.trace.propagation.tracecontext import ( TraceContextTextMapPropagator, ) _propagator = TraceContextTextMapPropagator() _django_version = get_version() _logger = getLogger(__name__) T = TypeVar("T") # pylint: disable-msg=invalid-name class SqlCommenter: """ Middleware to append a comment to each database query with details about the framework and the execution context. """ def __init__(self, get_response) -> None: self.get_response = get_response def __call__(self, request) -> Any: with ExitStack() as stack: for db_alias in connections: stack.enter_context(connections[db_alias].execute_wrapper(_QueryWrapper(request))) return self.get_response(request) class _QueryWrapper: def __init__(self, request) -> None: self.request = request def __call__(self, execute: type[T], sql, params, many, context) -> T: # pylint: disable-msg=too-many-locals with_framework = getattr(conf.settings, "SQLCOMMENTER_WITH_FRAMEWORK", True) with_controller = getattr(conf.settings, "SQLCOMMENTER_WITH_CONTROLLER", True) with_route = getattr(conf.settings, "SQLCOMMENTER_WITH_ROUTE", True) with_app_name = getattr(conf.settings, "SQLCOMMENTER_WITH_APP_NAME", True) with_opentelemetry = getattr(conf.settings, "SQLCOMMENTER_WITH_OPENTELEMETRY", True) with_db_driver = getattr(conf.settings, "SQLCOMMENTER_WITH_DB_DRIVER", True) db_driver = context["connection"].settings_dict.get("ENGINE", "") resolver_match = self.request.resolver_match # Convert sql statement to string, handling psycopg2.sql.Composable object if hasattr(sql, "as_string"): sql = sql.as_string(context["connection"]) sql = str(sql) sql = _add_sql_comment( sql, # Information about the controller. controller=(resolver_match.view_name if resolver_match and with_controller else None), # route is the pattern that matched a request with a controller i.e. the regex # See https://docs.djangoproject.com/en/stable/ref/urlresolvers/#django.urls.ResolverMatch.route # getattr() because the attribute doesn't exist in Django < 2.2. route=(getattr(resolver_match, "route", None) if resolver_match and with_route else None), # app_name is the application namespace for the URL pattern that matches the URL. # See https://docs.djangoproject.com/en/stable/ref/urlresolvers/#django.urls.ResolverMatch.app_name app_name=((resolver_match.app_name or None) if resolver_match and with_app_name else None), # Framework centric information. framework=f"django:{_django_version}" if with_framework else None, # Information about the database and driver. db_driver=db_driver if with_db_driver else None, **_get_opentelemetry_values() if with_opentelemetry else {}, ) return execute(sql, params, many, context) package.py000066400000000000000000000002111527070072300444550ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("django >= 2.0",) _supports_metrics = True version.py000066400000000000000000000001441527070072300445540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements-0.txt000066400000000000000000000006741527070072300367330ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-djangoasgiref==3.8.1 Deprecated==1.2.14 Django==2.2.28 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 pytz==2024.1 sqlparse==0.5.0 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 setuptools==79.0.1 ; python_version >= "3.12" -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-wsgi -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-django test-requirements-1.txt000066400000000000000000000007041527070072300367260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-djangoasgiref==3.8.1 Deprecated==1.2.14 Django==3.2.25 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 pytz==2024.1 sqlparse==0.5.0 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-wsgi -e instrumentation/opentelemetry-instrumentation-asgi -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-django test-requirements-2.txt000066400000000000000000000006671527070072300367370ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-djangoasgiref==3.8.1 Deprecated==1.2.14 Django==4.2.17 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 sqlparse==0.5.0 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-wsgi -e instrumentation/opentelemetry-instrumentation-asgi -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-django test-requirements-3.txt000066400000000000000000000006671527070072300367400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-djangoasgiref==3.9.1 Deprecated==1.2.14 Django==5.2.11 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 sqlparse==0.5.0 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-wsgi -e instrumentation/opentelemetry-instrumentation-asgi -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-django opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/tests/000077500000000000000000000000001527070072300335475ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300355670ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/teststest_middleware.py000066400000000000000000001233011527070072300372160ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=E0611 # pylint: disable=too-many-lines from sys import modules from timeit import default_timer from unittest.mock import Mock, patch from django import VERSION, conf from django.http import HttpRequest, HttpResponse from django.test.client import Client from django.test.utils import setup_test_environment, teardown_test_environment from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.django import ( DjangoInstrumentor, _DjangoMiddleware, ) from opentelemetry.instrumentation.propagators import ( TraceResponsePropagator, set_global_response_propagator, ) from opentelemetry.sdk import resources from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, NumberDataPoint, ) from opentelemetry.sdk.trace import Span from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.trace import ( SpanKind, StatusCode, format_span_id, format_trace_id, ) from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, get_excluded_urls, get_traced_request_attrs, ) # pylint: disable=import-error from .views import ( error, excluded, excluded_noarg, excluded_noarg2, response_with_custom_header, route_span_name, traced, traced_template, ) DJANGO_2_0 = VERSION >= (2, 0) DJANGO_2_2 = VERSION >= (2, 2) DJANGO_3_0 = VERSION >= (3, 0) if DJANGO_2_0: from django.urls import path, re_path else: from django.conf.urls import url as re_path def path(path_argument, *args, **kwargs): return re_path(rf"^{path_argument}$", *args, **kwargs) urlpatterns = [ re_path(r"^traced/", traced), re_path(r"^traced_custom_header/", response_with_custom_header), re_path(r"^route/(?P[0-9]{4})/template/$", traced_template), re_path(r"^error/", error), re_path(r"^excluded_arg/", excluded), re_path(r"^excluded_noarg/", excluded_noarg), re_path(r"^excluded_noarg2/", excluded_noarg2), re_path(r"^span_name/([0-9]{4})/$", route_span_name), path("", traced, name="empty"), ] _django_instrumentor = DjangoInstrumentor() SCOPE = "opentelemetry.instrumentation.django" # pylint: disable=too-many-public-methods class TestMiddleware(WsgiTestBase): @classmethod def setUpClass(cls): conf.settings.configure( ROOT_URLCONF=modules[__name__], DATABASES={ "default": {}, "other": {}, }, # db.connections gets populated only at first test execution ) super().setUpClass() def setUp(self): super().setUp() setup_test_environment() test_name = "" if hasattr(self, "_testMethodName"): test_name = self._testMethodName sem_conv_mode = "default" if "new_semconv" in test_name: sem_conv_mode = "http" elif "both_semconv" in test_name: sem_conv_mode = "http/dup" self.env_patch = patch.dict( "os.environ", { "OTEL_PYTHON_DJANGO_EXCLUDED_URLS": "http://testserver/excluded_arg/123,excluded_noarg", "OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS": "path_info,content_type,non_existing_variable", OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) _OpenTelemetrySemanticConventionStability._initialized = False self.env_patch.start() _django_instrumentor.instrument() self.exclude_patch = patch( "opentelemetry.instrumentation.django.middleware.otel_middleware._DjangoMiddleware._excluded_urls", get_excluded_urls("DJANGO"), ) self.traced_patch = patch( "opentelemetry.instrumentation.django.middleware.otel_middleware._DjangoMiddleware._traced_request_attrs", get_traced_request_attrs("DJANGO"), ) self.exclude_patch.start() self.traced_patch.start() def tearDown(self): super().tearDown() self.env_patch.stop() self.exclude_patch.stop() self.traced_patch.stop() teardown_test_environment() _django_instrumentor.uninstrument() @classmethod def tearDownClass(cls): super().tearDownClass() conf.settings = conf.LazySettings() def test_middleware_added_at_position(self): _django_instrumentor.uninstrument() if DJANGO_2_0: middleware = conf.settings.MIDDLEWARE else: middleware = conf.settings.MIDDLEWARE_CLASSES # adding two dummy middlewares temprory_middelware = "django.utils.deprecation.MiddlewareMixin" middleware.append(temprory_middelware) middleware.append(temprory_middelware) middleware_position = 1 _django_instrumentor.instrument(middleware_position=middleware_position) self.assertEqual( middleware[middleware_position], "opentelemetry.instrumentation.django.middleware.otel_middleware._DjangoMiddleware", ) def test_middleware_added_at_position_if_wrong_position(self): _django_instrumentor.uninstrument() if DJANGO_2_0: middleware = conf.settings.MIDDLEWARE else: middleware = conf.settings.MIDDLEWARE_CLASSES # adding middleware temprory_middelware = "django.utils.deprecation.MiddlewareMixin" middleware.append(temprory_middelware) middleware_position = 756 # wrong position out of bound of middleware length _django_instrumentor.instrument(middleware_position=middleware_position) self.assertEqual( middleware[0], "opentelemetry.instrumentation.django.middleware.otel_middleware._DjangoMiddleware", ) def test_templated_route_get(self): Client().get("/route/2020/template/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual( span.name, ("GET ^route/(?P[0-9]{4})/template/$" if DJANGO_2_2 else "GET"), ) self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes["http.method"], "GET") self.assertEqual( span.attributes["http.url"], "http://testserver/route/2020/template/", ) if DJANGO_2_2: self.assertEqual( span.attributes["http.route"], "^route/(?P[0-9]{4})/template/$", ) self.assertEqual(span.attributes["http.scheme"], "http") self.assertEqual(span.attributes["http.status_code"], 200) def test_traced_get(self): Client().get("/traced/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET ^traced/" if DJANGO_2_2 else "GET") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes["http.method"], "GET") self.assertEqual( span.attributes["http.url"], "http://testserver/traced/", ) if DJANGO_2_2: self.assertEqual(span.attributes["http.route"], "^traced/") self.assertEqual(span.attributes["http.scheme"], "http") self.assertEqual(span.attributes["http.status_code"], 200) def test_traced_get_new_semconv(self): Client().get("/traced/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET ^traced/" if DJANGO_2_2 else "GET") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes["http.request.method"], "GET") self.assertEqual(span.attributes["url.scheme"], "http") self.assertEqual(span.attributes["server.port"], 80) self.assertEqual(span.attributes["client.address"], "127.0.0.1") self.assertEqual(span.attributes["network.protocol.version"], "1.1") if DJANGO_2_2: self.assertEqual(span.attributes["http.route"], "^traced/") self.assertEqual(span.attributes["http.response.status_code"], 200) def test_traced_get_both_semconv(self): Client().get("/traced/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET ^traced/" if DJANGO_2_2 else "GET") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes["http.method"], "GET") self.assertEqual( span.attributes["http.url"], "http://testserver/traced/", ) if DJANGO_2_2: self.assertEqual(span.attributes["http.route"], "^traced/") self.assertEqual(span.attributes["http.scheme"], "http") self.assertEqual(span.attributes["http.status_code"], 200) self.assertEqual(span.attributes["http.request.method"], "GET") self.assertEqual(span.attributes["url.scheme"], "http") self.assertEqual(span.attributes["server.port"], 80) self.assertEqual(span.attributes["client.address"], "127.0.0.1") self.assertEqual(span.attributes["network.protocol.version"], "1.1") if DJANGO_2_2: self.assertEqual(span.attributes["http.route"], "^traced/") self.assertEqual(span.attributes["http.response.status_code"], 200) def test_not_recording(self): mock_tracer = Mock() mock_span = Mock() mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span with patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer Client().get("/traced/") self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) def test_empty_path(self): Client().get("/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET empty") def test_traced_post(self): Client().post("/traced/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "POST ^traced/" if DJANGO_2_2 else "POST") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes["http.method"], "POST") self.assertEqual( span.attributes["http.url"], "http://testserver/traced/", ) if DJANGO_2_2: self.assertEqual(span.attributes["http.route"], "^traced/") self.assertEqual(span.attributes["http.scheme"], "http") self.assertEqual(span.attributes["http.status_code"], 200) def test_traced_post_new_semconv(self): Client().post("/traced/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "POST ^traced/" if DJANGO_2_2 else "POST") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes["http.request.method"], "POST") self.assertEqual(span.attributes["url.scheme"], "http") self.assertEqual(span.attributes["server.port"], 80) self.assertEqual(span.attributes["client.address"], "127.0.0.1") self.assertEqual(span.attributes["network.protocol.version"], "1.1") if DJANGO_2_2: self.assertEqual(span.attributes["http.route"], "^traced/") self.assertEqual(span.attributes["http.response.status_code"], 200) def test_traced_post_both_semconv(self): Client().post("/traced/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "POST ^traced/" if DJANGO_2_2 else "POST") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes["http.method"], "POST") self.assertEqual( span.attributes["http.url"], "http://testserver/traced/", ) self.assertEqual(span.attributes["http.scheme"], "http") self.assertEqual(span.attributes["http.status_code"], 200) self.assertEqual(span.attributes["http.request.method"], "POST") self.assertEqual(span.attributes["url.scheme"], "http") self.assertEqual(span.attributes["server.port"], 80) self.assertEqual(span.attributes["client.address"], "127.0.0.1") self.assertEqual(span.attributes["network.protocol.version"], "1.1") if DJANGO_2_2: self.assertEqual(span.attributes["http.route"], "^traced/") self.assertEqual(span.attributes["http.response.status_code"], 200) def test_error(self): with self.assertRaises(ValueError): Client().get("/error/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET ^error/" if DJANGO_2_2 else "GET") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(span.attributes["http.method"], "GET") self.assertEqual( span.attributes["http.url"], "http://testserver/error/", ) if DJANGO_2_2: self.assertEqual(span.attributes["http.route"], "^error/") self.assertEqual(span.attributes["http.scheme"], "http") self.assertEqual(span.attributes["http.status_code"], 500) self.assertEqual(len(span.events), 1) event = span.events[0] self.assertEqual(event.name, "exception") self.assertEqual(event.attributes["exception.type"], "ValueError") self.assertEqual(event.attributes["exception.message"], "error") def test_error_new_semconv(self): with self.assertRaises(ValueError): Client().get("/error/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET ^error/" if DJANGO_2_2 else "GET") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(span.attributes["http.request.method"], "GET") if DJANGO_2_2: self.assertEqual(span.attributes["http.route"], "^error/") self.assertEqual(span.attributes["url.scheme"], "http") self.assertEqual(span.attributes["http.response.status_code"], 500) self.assertEqual(len(span.events), 1) event = span.events[0] self.assertEqual(event.name, "exception") self.assertEqual(event.attributes["exception.type"], "ValueError") self.assertEqual(event.attributes["exception.message"], "error") self.assertEqual(span.attributes["error.type"], "500") def test_error_both_semconv(self): with self.assertRaises(ValueError): Client().get("/error/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET ^error/" if DJANGO_2_2 else "GET") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(span.attributes["http.method"], "GET") self.assertEqual( span.attributes["http.url"], "http://testserver/error/", ) if DJANGO_2_2: self.assertEqual(span.attributes["http.route"], "^error/") self.assertEqual(span.attributes["http.scheme"], "http") self.assertEqual(span.attributes["http.status_code"], 500) self.assertEqual(span.attributes["http.request.method"], "GET") if DJANGO_2_2: self.assertEqual(span.attributes["http.route"], "^error/") self.assertEqual(span.attributes["url.scheme"], "http") self.assertEqual(span.attributes["http.response.status_code"], 500) self.assertEqual(len(span.events), 1) event = span.events[0] self.assertEqual(event.name, "exception") self.assertEqual(event.attributes["exception.type"], "ValueError") self.assertEqual(event.attributes["exception.message"], "error") self.assertEqual(span.attributes["error.type"], "500") def test_exclude_lists(self): client = Client() client.get("/excluded_arg/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) client.get("/excluded_arg/125") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) client.get("/excluded_noarg/") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) client.get("/excluded_noarg2/") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) def test_exclude_lists_through_instrument(self): _django_instrumentor.uninstrument() _django_instrumentor.instrument(excluded_urls="excluded_explicit") client = Client() client.get("/excluded_explicit") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) client.get("/excluded_arg/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) def test_span_name(self): # test no query_string Client().get("/span_name/1234/") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertEqual( span.name, "GET ^span_name/([0-9]{4})/$" if DJANGO_2_2 else "GET", ) def test_span_name_for_query_string(self): """ request not have query string """ Client().get("/span_name/1234/?query=test") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertEqual( span.name, "GET ^span_name/([0-9]{4})/$" if DJANGO_2_2 else "GET", ) def test_span_name_404(self): Client().get("/span_name/1234567890/") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertEqual(span.name, "GET") def test_nonstandard_http_method_span_name(self): Client().request(REQUEST_METHOD="NONSTANDARD", PATH_INFO="/span_name/1234/") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertEqual(span.name, "HTTP") self.assertEqual(span.attributes["http.method"], "_OTHER") def test_nonstandard_http_method_span_name_new_semconv(self): Client().request(REQUEST_METHOD="NONSTANDARD", PATH_INFO="/span_name/1234/") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertEqual(span.name, "HTTP") self.assertEqual(span.attributes["http.request.method"], "_OTHER") self.assertEqual(span.attributes["http.request.method_original"], "NONSTANDARD") def test_nonstandard_http_method_span_name_both_semconv(self): Client().request(REQUEST_METHOD="NONSTANDARD", PATH_INFO="/span_name/1234/") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertEqual(span.name, "HTTP") self.assertEqual(span.attributes["http.method"], "_OTHER") self.assertEqual(span.attributes["http.request.method"], "_OTHER") self.assertEqual(span.attributes["http.request.method_original"], "NONSTANDARD") def test_traced_request_attrs(self): Client().get("/span_name/1234/", CONTENT_TYPE="test/ct") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertEqual(span.attributes["path_info"], "/span_name/1234/") self.assertEqual(span.attributes["content_type"], "test/ct") self.assertNotIn("non_existing_variable", span.attributes) def test_hooks(self): request_hook_args = () response_hook_args = () def request_hook(span, request): nonlocal request_hook_args request_hook_args = (span, request) def response_hook(span, request, response): nonlocal response_hook_args response_hook_args = (span, request, response) response["hook-header"] = "set by hook" _DjangoMiddleware._otel_request_hook = request_hook _DjangoMiddleware._otel_response_hook = response_hook response = Client().get("/span_name/1234/") _DjangoMiddleware._otel_request_hook = _DjangoMiddleware._otel_response_hook = None self.assertEqual(response["hook-header"], "set by hook") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertEqual(span.attributes["path_info"], "/span_name/1234/") self.assertEqual(len(request_hook_args), 2) self.assertEqual(request_hook_args[0].name, span.name) self.assertIsInstance(request_hook_args[0], Span) self.assertIsInstance(request_hook_args[1], HttpRequest) self.assertEqual(len(response_hook_args), 3) self.assertEqual(request_hook_args[0], response_hook_args[0]) self.assertIsInstance(response_hook_args[1], HttpRequest) self.assertIsInstance(response_hook_args[2], HttpResponse) self.assertEqual(response_hook_args[2], response) def test_request_hook_exception(self): def request_hook(span, request): # pylint: disable=broad-exception-raised raise Exception("request hook exception") _DjangoMiddleware._otel_request_hook = request_hook Client().get("/span_name/1234/") _DjangoMiddleware._otel_request_hook = None # ensure that span ended finished_spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(finished_spans), 1) def test_response_hook_exception(self): def response_hook(span, request, response): # pylint: disable=broad-exception-raised raise Exception("response hook exception") _DjangoMiddleware._otel_response_hook = response_hook Client().get("/span_name/1234/") _DjangoMiddleware._otel_response_hook = None # ensure that span ended finished_spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(finished_spans), 1) def test_trace_parent(self): id_generator = RandomIdGenerator() trace_id = format_trace_id(id_generator.generate_trace_id()) span_id = format_span_id(id_generator.generate_span_id()) traceparent_value = f"00-{trace_id}-{span_id}-01" Client().get( "/span_name/1234/", HTTP_TRACEPARENT=traceparent_value, ) span = self.memory_exporter.get_finished_spans()[0] self.assertEqual( trace_id, format_trace_id(span.get_span_context().trace_id), ) self.assertIsNotNone(span.parent) self.assertEqual( trace_id, format_trace_id(span.parent.trace_id), ) self.assertEqual( span_id, format_span_id(span.parent.span_id), ) self.memory_exporter.clear() def test_trace_response_headers(self): response = Client().get("/span_name/1234/") self.assertFalse(response.has_header("Server-Timing")) self.memory_exporter.clear() set_global_response_propagator(TraceResponsePropagator()) response = Client().get("/span_name/1234/") self.assertTraceResponseHeaderMatchesSpan( response, self.memory_exporter.get_finished_spans()[0], ) self.memory_exporter.clear() def test_uninstrument(self): Client().get("/route/2020/template/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.memory_exporter.clear() _django_instrumentor.uninstrument() Client().get("/route/2020/template/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) # pylint: disable=too-many-locals def test_wsgi_metrics(self): _expected_metric_names = [ "http.server.active_requests", "http.server.duration", ] expected_duration_attributes = { "http.method": "GET", "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "testserver", "net.host.port": 80, "http.status_code": 200, "http.target": "^span_name/([0-9]{4})/$", } expected_requests_count_attributes = { "http.method": "GET", "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "testserver", } start = default_timer() for _ in range(3): response = Client().get("/span_name/1234/") self.assertEqual(response.status_code, 200) duration = max(round((default_timer() - start) * 1000), 0) metrics = self.get_sorted_metrics(SCOPE) number_data_point_seen = False histrogram_data_point_seen = False self.assertTrue(len(metrics) != 0) for metric in metrics: self.assertIn(metric.name, _expected_metric_names) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 3) histrogram_data_point_seen = True self.assertAlmostEqual(duration, point.sum, delta=100) self.assertDictEqual( expected_duration_attributes, dict(point.attributes), ) if isinstance(point, NumberDataPoint): number_data_point_seen = True self.assertEqual(point.value, 0) self.assertDictEqual( expected_requests_count_attributes, dict(point.attributes), ) self.assertTrue(histrogram_data_point_seen and number_data_point_seen) # pylint: disable=too-many-locals def test_wsgi_metrics_new_semconv(self): _expected_metric_names = [ "http.server.active_requests", "http.server.request.duration", ] expected_duration_attributes = { "http.request.method": "GET", "url.scheme": "http", "network.protocol.version": "1.1", "http.response.status_code": 200, "http.route": "^span_name/([0-9]{4})/$", } expected_requests_count_attributes = { "http.request.method": "GET", "url.scheme": "http", } start = default_timer() for _ in range(3): response = Client().get("/span_name/1234/") self.assertEqual(response.status_code, 200) duration_s = default_timer() - start metrics = self.get_sorted_metrics(SCOPE) number_data_point_seen = False histrogram_data_point_seen = False self.assertTrue(len(metrics) != 0) for metric in metrics: self.assertIn(metric.name, _expected_metric_names) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 3) histrogram_data_point_seen = True self.assertAlmostEqual(duration_s, point.sum, places=1) self.assertDictEqual( expected_duration_attributes, dict(point.attributes), ) self.assertEqual( point.explicit_bounds, HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) if isinstance(point, NumberDataPoint): number_data_point_seen = True self.assertEqual(point.value, 0) self.assertDictEqual( expected_requests_count_attributes, dict(point.attributes), ) self.assertTrue(histrogram_data_point_seen and number_data_point_seen) # pylint: disable=too-many-locals # pylint: disable=too-many-nested-blocks def test_wsgi_metrics_both_semconv(self): _expected_metric_names = [ "http.server.duration", "http.server.active_requests", "http.server.request.duration", ] expected_duration_attributes_old = { "http.method": "GET", "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "testserver", "net.host.port": 80, "http.status_code": 200, "http.target": "^span_name/([0-9]{4})/$", } expected_duration_attributes_new = { "http.request.method": "GET", "url.scheme": "http", "network.protocol.version": "1.1", "http.response.status_code": 200, "http.route": "^span_name/([0-9]{4})/$", } expected_requests_count_attributes = { "http.method": "GET", "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "testserver", "http.request.method": "GET", "url.scheme": "http", } start = default_timer() for _ in range(3): response = Client().get("/span_name/1234/") self.assertEqual(response.status_code, 200) duration_s = max(default_timer() - start, 0) duration = max(round(duration_s * 1000), 0) metrics = self.get_sorted_metrics(SCOPE) number_data_point_seen = False histrogram_data_point_seen = False self.assertTrue(len(metrics) != 0) for metric in metrics: self.assertIn(metric.name, _expected_metric_names) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 3) histrogram_data_point_seen = True if metric.name == "http.server.request.duration": self.assertAlmostEqual(duration_s, point.sum, places=1) self.assertDictEqual( expected_duration_attributes_new, dict(point.attributes), ) self.assertEqual( point.explicit_bounds, HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) elif metric.name == "http.server.duration": self.assertAlmostEqual(duration, point.sum, delta=100) self.assertDictEqual( expected_duration_attributes_old, dict(point.attributes), ) self.assertEqual( point.explicit_bounds, HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, ) if isinstance(point, NumberDataPoint): number_data_point_seen = True self.assertEqual(point.value, 0) self.assertDictEqual( expected_requests_count_attributes, dict(point.attributes), ) self.assertTrue(histrogram_data_point_seen and number_data_point_seen) def test_wsgi_metrics_unistrument(self): Client().get("/span_name/1234/") _django_instrumentor.uninstrument() Client().get("/span_name/1234/") metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(1, point.count) if isinstance(point, NumberDataPoint): self.assertEqual(0, point.value) class TestMiddlewareWithTracerProvider(WsgiTestBase): @classmethod def setUpClass(cls): conf.settings.configure(ROOT_URLCONF=modules[__name__]) super().setUpClass() def setUp(self): super().setUp() setup_test_environment() resource = resources.Resource.create({"resource-key": "resource-value"}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result self.exporter = exporter self.tracer_provider = tracer_provider _django_instrumentor.instrument(tracer_provider=tracer_provider) def tearDown(self): super().tearDown() teardown_test_environment() _django_instrumentor.uninstrument() @classmethod def tearDownClass(cls): super().tearDownClass() conf.settings = conf.LazySettings() def test_tracer_provider_traced(self): Client().post("/traced/") spans = self.exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.resource.attributes["resource-key"], "resource-value") def test_django_with_wsgi_instrumented(self): tracer = self.tracer_provider.get_tracer(__name__) with tracer.start_as_current_span("test", kind=SpanKind.SERVER) as parent_span: Client().get("/span_name/1234/") span_list = self.exporter.get_finished_spans() print(span_list) self.assertEqual(span_list[0].attributes["http.status_code"], 200) self.assertEqual(trace.SpanKind.INTERNAL, span_list[0].kind) self.assertEqual( parent_span.get_span_context().span_id, span_list[0].parent.span_id, ) @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", }, ) class TestMiddlewareWsgiWithCustomHeaders(WsgiTestBase): @classmethod def setUpClass(cls): conf.settings.configure(ROOT_URLCONF=modules[__name__]) super().setUpClass() def setUp(self): super().setUp() setup_test_environment() tracer_provider, exporter = self.create_tracer_provider() self.exporter = exporter _django_instrumentor.instrument(tracer_provider=tracer_provider) def tearDown(self): super().tearDown() teardown_test_environment() _django_instrumentor.uninstrument() @classmethod def tearDownClass(cls): super().tearDownClass() conf.settings = conf.LazySettings() def test_http_custom_request_headers_in_span_attributes(self): expected = { "http.request.header.custom_test_header_1": ("test-header-value-1",), "http.request.header.custom_test_header_2": ("test-header-value-2",), "http.request.header.regex_test_header_1": ("Regex Test Value 1",), "http.request.header.regex_test_header_2": ("RegexTestValue2,RegexTestValue3",), "http.request.header.my_secret_header": ("[REDACTED]",), } Client( HTTP_CUSTOM_TEST_HEADER_1="test-header-value-1", HTTP_CUSTOM_TEST_HEADER_2="test-header-value-2", HTTP_REGEX_TEST_HEADER_1="Regex Test Value 1", HTTP_REGEX_TEST_HEADER_2="RegexTestValue2,RegexTestValue3", HTTP_MY_SECRET_HEADER="My Secret Value", ).get("/traced/") spans = self.exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) self.memory_exporter.clear() def test_http_custom_request_headers_not_in_span_attributes(self): not_expected = { "http.request.header.custom_test_header_2": ("test-header-value-2",), } Client(HTTP_CUSTOM_TEST_HEADER_1="test-header-value-1").get("/traced/") spans = self.exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.kind, SpanKind.SERVER) for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) self.memory_exporter.clear() def test_http_custom_response_headers_in_span_attributes(self): expected = { "http.response.header.custom_test_header_1": ("test-header-value-1",), "http.response.header.custom_test_header_2": ("test-header-value-2",), "http.response.header.my_custom_regex_header_1": ("my-custom-regex-value-1,my-custom-regex-value-2",), "http.response.header.my_custom_regex_header_2": ("my-custom-regex-value-3,my-custom-regex-value-4",), "http.response.header.my_secret_header": ("[REDACTED]",), } Client().get("/traced_custom_header/") spans = self.exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) self.memory_exporter.clear() def test_http_custom_response_headers_not_in_span_attributes(self): not_expected = { "http.response.header.custom_test_header_3": ("test-header-value-3",), } Client().get("/traced_custom_header/") spans = self.exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.kind, SpanKind.SERVER) for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) self.memory_exporter.clear() class TestMiddlewareSpanActivationTiming(WsgiTestBase): """Test span activation timing relative to metrics recording.""" @classmethod def setUpClass(cls): conf.settings.configure(ROOT_URLCONF=modules[__name__]) super().setUpClass() def setUp(self): super().setUp() setup_test_environment() _django_instrumentor.instrument() def tearDown(self): super().tearDown() teardown_test_environment() _django_instrumentor.uninstrument() @classmethod def tearDownClass(cls): super().tearDownClass() conf.settings = conf.LazySettings() def test_span_ended_after_metrics_recorded(self): """Span activation exits after metrics recording.""" Client().get("/traced/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) # Span properly finished self.assertIsNotNone(spans[0].end_time) # Metrics recorded metrics_list = self.memory_metrics_reader.get_metrics_data() histogram_found = any( "duration" in metric.name for rm in metrics_list.resource_metrics for sm in rm.scope_metrics for metric in sm.metrics ) self.assertTrue(histogram_found) def test_metrics_recorded_with_exception(self): """Metrics recorded even when request raises exception.""" with self.assertRaises(ValueError): Client().get("/error/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].status.status_code, StatusCode.ERROR) # Metrics still recorded metrics_list = self.memory_metrics_reader.get_metrics_data() histogram_found = any( "duration" in metric.name for rm in metrics_list.resource_metrics for sm in rm.scope_metrics for metric in sm.metrics ) self.assertTrue(histogram_found) test_middleware_asgi.py000066400000000000000000000741541527070072300402340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=E0611 from sys import modules from unittest.mock import Mock, patch import pytest from django import VERSION, conf from django.http import HttpRequest, HttpResponse from django.test import SimpleTestCase from django.test.utils import setup_test_environment, teardown_test_environment from opentelemetry import trace as trace_api from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.django import ( DjangoInstrumentor, _DjangoMiddleware, ) from opentelemetry.instrumentation.propagators import ( TraceResponsePropagator, set_global_response_propagator, ) from opentelemetry.sdk import resources from opentelemetry.sdk.trace import Span from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_METHOD, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_URL, ) from opentelemetry.semconv.attributes.client_attributes import CLIENT_ADDRESS from opentelemetry.semconv.attributes.exception_attributes import ( EXCEPTION_MESSAGE, EXCEPTION_TYPE, ) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_REQUEST_METHOD_ORIGINAL, HTTP_RESPONSE_STATUS_CODE, HTTP_ROUTE, ) from opentelemetry.semconv.attributes.network_attributes import ( NETWORK_PROTOCOL_VERSION, ) from opentelemetry.semconv.attributes.server_attributes import SERVER_PORT from opentelemetry.semconv.attributes.url_attributes import URL_SCHEME from opentelemetry.test.test_base import TestBase from opentelemetry.trace import ( SpanKind, StatusCode, format_span_id, format_trace_id, ) from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, get_excluded_urls, get_traced_request_attrs, ) # pylint: disable=import-error from .views import ( async_error, async_excluded, async_excluded_noarg, async_excluded_noarg2, async_route_span_name, async_traced, async_traced_template, async_with_custom_header, ) DJANGO_2_0 = VERSION >= (2, 0) DJANGO_3_1 = VERSION >= (3, 1) if DJANGO_2_0: from django.urls import re_path else: from django.conf.urls import url as re_path urlpatterns = [ re_path(r"^traced/", async_traced), re_path(r"^traced_custom_header/", async_with_custom_header), re_path(r"^route/(?P[0-9]{4})/template/$", async_traced_template), re_path(r"^error/", async_error), re_path(r"^excluded_arg/", async_excluded), re_path(r"^excluded_noarg/", async_excluded_noarg), re_path(r"^excluded_noarg2/", async_excluded_noarg2), re_path(r"^span_name/([0-9]{4})/$", async_route_span_name), ] _django_instrumentor = DjangoInstrumentor() @pytest.mark.skipif(not DJANGO_3_1, reason="AsyncClient implemented since Django 3.1") # pylint: disable=too-many-public-methods class TestMiddlewareAsgi(SimpleTestCase, TestBase): @classmethod def setUpClass(cls): conf.settings.configure(ROOT_URLCONF=modules[__name__]) super().setUpClass() def setUp(self): super().setUp() setup_test_environment() test_name = "" if hasattr(self, "_testMethodName"): test_name = self._testMethodName sem_conv_mode = "default" if "new_semconv" in test_name: sem_conv_mode = "http" elif "both_semconv" in test_name: sem_conv_mode = "http/dup" self.env_patch = patch.dict( "os.environ", { "OTEL_PYTHON_DJANGO_EXCLUDED_URLS": "http://testserver/excluded_arg/123,excluded_noarg", "OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS": "path_info,content_type,non_existing_variable", OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) _OpenTelemetrySemanticConventionStability._initialized = False self.env_patch.start() _django_instrumentor.instrument() self.exclude_patch = patch( "opentelemetry.instrumentation.django.middleware.otel_middleware._DjangoMiddleware._excluded_urls", get_excluded_urls("DJANGO"), ) self.traced_patch = patch( "opentelemetry.instrumentation.django.middleware.otel_middleware._DjangoMiddleware._traced_request_attrs", get_traced_request_attrs("DJANGO"), ) self.exclude_patch.start() self.traced_patch.start() def tearDown(self): super().tearDown() self.env_patch.stop() self.exclude_patch.stop() self.traced_patch.stop() teardown_test_environment() _django_instrumentor.uninstrument() @classmethod def tearDownClass(cls): super().tearDownClass() conf.settings = conf.LazySettings() async def test_templated_route_get(self): await self.async_client.get("/route/2020/template/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET ^route/(?P[0-9]{4})/template/$") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes[HTTP_METHOD], "GET") self.assertEqual( span.attributes[HTTP_URL], "http://testserver/route/2020/template/", ) self.assertEqual( span.attributes[HTTP_ROUTE], "^route/(?P[0-9]{4})/template/$", ) self.assertEqual(span.attributes[HTTP_SCHEME], "http") self.assertEqual(span.attributes[HTTP_STATUS_CODE], 200) async def test_templated_route_get_new_semconv(self): await self.async_client.get("/route/2020/template/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET ^route/(?P[0-9]{4})/template/$") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") self.assertEqual(span.attributes[SERVER_PORT], 80) self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") self.assertEqual( span.attributes[HTTP_ROUTE], "^route/(?P[0-9]{4})/template/$", ) self.assertEqual(span.attributes[URL_SCHEME], "http") self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) async def test_templated_route_get_both_semconv(self): await self.async_client.get("/route/2020/template/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET ^route/(?P[0-9]{4})/template/$") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes[HTTP_METHOD], "GET") self.assertEqual( span.attributes[HTTP_URL], "http://testserver/route/2020/template/", ) self.assertEqual(span.attributes[HTTP_SCHEME], "http") self.assertEqual(span.attributes[HTTP_STATUS_CODE], 200) self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") self.assertEqual(span.attributes[SERVER_PORT], 80) self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") self.assertEqual( span.attributes[HTTP_ROUTE], "^route/(?P[0-9]{4})/template/$", ) self.assertEqual(span.attributes[URL_SCHEME], "http") self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) async def test_traced_get(self): await self.async_client.get("/traced/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET ^traced/") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes[HTTP_METHOD], "GET") self.assertEqual( span.attributes[HTTP_URL], "http://testserver/traced/", ) self.assertEqual(span.attributes[HTTP_ROUTE], "^traced/") self.assertEqual(span.attributes[HTTP_SCHEME], "http") self.assertEqual(span.attributes[HTTP_STATUS_CODE], 200) async def test_traced_get_new_semconv(self): await self.async_client.get("/traced/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET ^traced/") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") self.assertEqual(span.attributes[URL_SCHEME], "http") self.assertEqual(span.attributes[SERVER_PORT], 80) self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") self.assertEqual(span.attributes[HTTP_ROUTE], "^traced/") self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) async def test_traced_get_both_semconv(self): await self.async_client.get("/traced/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET ^traced/") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes[HTTP_METHOD], "GET") self.assertEqual( span.attributes[HTTP_URL], "http://testserver/traced/", ) self.assertEqual(span.attributes[HTTP_SCHEME], "http") self.assertEqual(span.attributes[HTTP_STATUS_CODE], 200) self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") self.assertEqual(span.attributes[URL_SCHEME], "http") self.assertEqual(span.attributes[SERVER_PORT], 80) self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") self.assertEqual(span.attributes[HTTP_ROUTE], "^traced/") self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) async def test_not_recording(self): mock_tracer = Mock() mock_span = Mock() mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span with patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer await self.async_client.get("/traced/") self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) async def test_traced_post(self): await self.async_client.post("/traced/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "POST ^traced/") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes[HTTP_METHOD], "POST") self.assertEqual( span.attributes[HTTP_URL], "http://testserver/traced/", ) self.assertEqual(span.attributes[HTTP_ROUTE], "^traced/") self.assertEqual(span.attributes[HTTP_SCHEME], "http") self.assertEqual(span.attributes[HTTP_STATUS_CODE], 200) async def test_traced_post_new_semconv(self): await self.async_client.post("/traced/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "POST ^traced/") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "POST") self.assertEqual(span.attributes[URL_SCHEME], "http") self.assertEqual(span.attributes[SERVER_PORT], 80) self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") self.assertEqual(span.attributes[HTTP_ROUTE], "^traced/") self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) async def test_traced_post_both_semconv(self): await self.async_client.post("/traced/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "POST ^traced/") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.attributes[HTTP_METHOD], "POST") self.assertEqual( span.attributes[HTTP_URL], "http://testserver/traced/", ) self.assertEqual(span.attributes[HTTP_SCHEME], "http") self.assertEqual(span.attributes[HTTP_STATUS_CODE], 200) self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "POST") self.assertEqual(span.attributes[URL_SCHEME], "http") self.assertEqual(span.attributes[SERVER_PORT], 80) self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") self.assertEqual(span.attributes[HTTP_ROUTE], "^traced/") self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) async def test_error(self): with self.assertRaises(ValueError): await self.async_client.get("/error/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET ^error/") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(span.attributes[HTTP_METHOD], "GET") self.assertEqual( span.attributes[HTTP_URL], "http://testserver/error/", ) self.assertEqual(span.attributes[HTTP_ROUTE], "^error/") self.assertEqual(span.attributes[HTTP_SCHEME], "http") self.assertEqual(span.attributes[HTTP_STATUS_CODE], 500) self.assertEqual(len(span.events), 1) event = span.events[0] self.assertEqual(event.name, "exception") self.assertEqual(event.attributes[EXCEPTION_TYPE], "ValueError") self.assertEqual(event.attributes[EXCEPTION_MESSAGE], "error") async def test_error_new_semconv(self): with self.assertRaises(ValueError): await self.async_client.get("/error/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET ^error/") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") self.assertEqual(span.attributes[HTTP_ROUTE], "^error/") self.assertEqual(span.attributes[URL_SCHEME], "http") self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 500) self.assertEqual(len(span.events), 1) event = span.events[0] self.assertEqual(event.name, "exception") self.assertEqual(event.attributes[EXCEPTION_TYPE], "ValueError") self.assertEqual(event.attributes[EXCEPTION_MESSAGE], "error") async def test_error_both_semconv(self): with self.assertRaises(ValueError): await self.async_client.get("/error/") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET ^error/") self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(span.attributes[HTTP_METHOD], "GET") self.assertEqual( span.attributes[HTTP_URL], "http://testserver/error/", ) self.assertEqual(span.attributes[HTTP_ROUTE], "^error/") self.assertEqual(span.attributes[HTTP_SCHEME], "http") self.assertEqual(span.attributes[HTTP_STATUS_CODE], 500) self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") self.assertEqual(span.attributes[HTTP_ROUTE], "^error/") self.assertEqual(span.attributes[URL_SCHEME], "http") self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 500) self.assertEqual(len(span.events), 1) event = span.events[0] self.assertEqual(event.name, "exception") self.assertEqual(event.attributes[EXCEPTION_TYPE], "ValueError") self.assertEqual(event.attributes[EXCEPTION_MESSAGE], "error") async def test_exclude_lists(self): await self.async_client.get("/excluded_arg/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) await self.async_client.get("/excluded_arg/125") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) await self.async_client.get("/excluded_noarg/") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) await self.async_client.get("/excluded_noarg2/") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) async def test_span_name(self): # test no query_string await self.async_client.get("/span_name/1234/") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertEqual(span.name, "GET ^span_name/([0-9]{4})/$") async def test_span_name_for_query_string(self): """ request not have query string """ await self.async_client.get("/span_name/1234/?query=test") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertEqual(span.name, "GET ^span_name/([0-9]{4})/$") async def test_span_name_404(self): await self.async_client.get("/span_name/1234567890/") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertEqual(span.name, "GET") async def test_nonstandard_http_method_span_name(self): await self.async_client.request(method="NONSTANDARD", path="/span_name/1234/") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertEqual(span.name, "HTTP") async def test_nonstandard_http_method_span_name_new_semconv(self): await self.async_client.request(method="NONSTANDARD", path="/span_name/1234/") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertEqual(span.name, "HTTP") self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "_OTHER") self.assertEqual(span.attributes[HTTP_REQUEST_METHOD_ORIGINAL], "NONSTANDARD") async def test_nonstandard_http_method_span_name_both_semconv(self): await self.async_client.request(method="NONSTANDARD", path="/span_name/1234/") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertEqual(span.name, "HTTP") self.assertEqual(span.attributes[HTTP_METHOD], "_OTHER") self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "_OTHER") self.assertEqual(span.attributes[HTTP_REQUEST_METHOD_ORIGINAL], "NONSTANDARD") async def test_traced_request_attrs(self): await self.async_client.get("/span_name/1234/", CONTENT_TYPE="test/ct") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertEqual(span.attributes["path_info"], "/span_name/1234/") self.assertEqual(span.attributes["content_type"], "test/ct") self.assertNotIn("non_existing_variable", span.attributes) async def test_hooks(self): request_hook_args = () response_hook_args = () def request_hook(span, request): nonlocal request_hook_args request_hook_args = (span, request) def response_hook(span, request, response): nonlocal response_hook_args response_hook_args = (span, request, response) response["hook-header"] = "set by hook" _DjangoMiddleware._otel_request_hook = request_hook _DjangoMiddleware._otel_response_hook = response_hook response = await self.async_client.get("/span_name/1234/") _DjangoMiddleware._otel_request_hook = _DjangoMiddleware._otel_response_hook = None self.assertEqual(response["hook-header"], "set by hook") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertEqual(span.attributes["path_info"], "/span_name/1234/") self.assertEqual(len(request_hook_args), 2) self.assertEqual(request_hook_args[0].name, span.name) self.assertIsInstance(request_hook_args[0], Span) self.assertIsInstance(request_hook_args[1], HttpRequest) self.assertEqual(len(response_hook_args), 3) self.assertEqual(request_hook_args[0], response_hook_args[0]) self.assertIsInstance(response_hook_args[1], HttpRequest) self.assertIsInstance(response_hook_args[2], HttpResponse) self.assertEqual(response_hook_args[2], response) async def test_trace_parent(self): id_generator = RandomIdGenerator() trace_id = format_trace_id(id_generator.generate_trace_id()) span_id = format_span_id(id_generator.generate_span_id()) traceparent_value = f"00-{trace_id}-{span_id}-01" await self.async_client.get( "/span_name/1234/", traceparent=traceparent_value, ) span = self.memory_exporter.get_finished_spans()[0] self.assertEqual( trace_id, format_trace_id(span.get_span_context().trace_id), ) self.assertIsNotNone(span.parent) self.assertEqual( trace_id, format_trace_id(span.parent.trace_id), ) self.assertEqual( span_id, format_span_id(span.parent.span_id), ) self.memory_exporter.clear() async def test_trace_response_headers(self): response = await self.async_client.get("/span_name/1234/") self.assertFalse(response.has_header("Server-Timing")) self.memory_exporter.clear() set_global_response_propagator(TraceResponsePropagator()) response = await self.async_client.get("/span_name/1234/") span = self.memory_exporter.get_finished_spans()[0] self.assertTrue(response.has_header("traceresponse")) self.assertEqual( response["Access-Control-Expose-Headers"], "traceresponse", ) trace_id = format_trace_id(span.get_span_context().trace_id) span_id = format_span_id(span.get_span_context().span_id) trace_flags = span.get_span_context().trace_flags self.assertEqual( response["traceresponse"], f"00-{trace_id}-{span_id}-{trace_flags:02x}", ) self.memory_exporter.clear() class TestMiddlewareAsgiWithTracerProvider(SimpleTestCase, TestBase): @classmethod def setUpClass(cls): conf.settings.configure(ROOT_URLCONF=modules[__name__]) super().setUpClass() def setUp(self): super().setUp() setup_test_environment() resource = resources.Resource.create({"resource-key": "resource-value"}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result self.exporter = exporter _django_instrumentor.instrument(tracer_provider=tracer_provider) def tearDown(self): super().tearDown() teardown_test_environment() _django_instrumentor.uninstrument() @classmethod def tearDownClass(cls): super().tearDownClass() conf.settings = conf.LazySettings() async def test_tracer_provider_traced(self): await self.async_client.post("/traced/") spans = self.exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.resource.attributes["resource-key"], "resource-value") async def test_no_op_tracer_provider(self): _django_instrumentor.uninstrument() _django_instrumentor.instrument(tracer_provider=trace_api.NoOpTracerProvider()) await self.async_client.post("/traced/") spans = self.exporter.get_finished_spans() self.assertEqual(len(spans), 0) @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", }, ) class TestMiddlewareAsgiWithCustomHeaders(SimpleTestCase, TestBase): @classmethod def setUpClass(cls): conf.settings.configure(ROOT_URLCONF=modules[__name__]) super().setUpClass() def setUp(self): super().setUp() setup_test_environment() tracer_provider, exporter = self.create_tracer_provider() self.exporter = exporter _django_instrumentor.instrument(tracer_provider=tracer_provider) def tearDown(self): super().tearDown() teardown_test_environment() _django_instrumentor.uninstrument() @classmethod def tearDownClass(cls): super().tearDownClass() conf.settings = conf.LazySettings() async def test_http_custom_request_headers_in_span_attributes(self): expected = { "http.request.header.custom_test_header_1": ("test-header-value-1",), "http.request.header.custom_test_header_2": ("test-header-value-2",), "http.request.header.regex_test_header_1": ("Regex Test Value 1",), "http.request.header.regex_test_header_2": ("RegexTestValue2,RegexTestValue3",), "http.request.header.my_secret_header": ("[REDACTED]",), } await self.async_client.get( "/traced/", **{ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", "Regex-Test-Header-1": "Regex Test Value 1", "regex-test-header-2": "RegexTestValue2,RegexTestValue3", "My-Secret-Header": "My Secret Value", }, ) spans = self.exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) self.memory_exporter.clear() async def test_http_custom_request_headers_not_in_span_attributes(self): not_expected = { "http.request.header.custom_test_header_2": ("test-header-value-2",), } await self.async_client.get( "/traced/", **{ "custom-test-header-1": "test-header-value-1", }, ) spans = self.exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.kind, SpanKind.SERVER) for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) self.memory_exporter.clear() async def test_http_custom_response_headers_in_span_attributes(self): expected = { "http.response.header.custom_test_header_1": ("test-header-value-1",), "http.response.header.custom_test_header_2": ("test-header-value-2",), "http.response.header.my_custom_regex_header_1": ("my-custom-regex-value-1,my-custom-regex-value-2",), "http.response.header.my_custom_regex_header_2": ("my-custom-regex-value-3,my-custom-regex-value-4",), "http.response.header.my_secret_header": ("[REDACTED]",), } await self.async_client.get("/traced_custom_header/") spans = self.exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) self.memory_exporter.clear() async def test_http_custom_response_headers_not_in_span_attributes(self): not_expected = { "http.response.header.custom_test_header_3": ("test-header-value-3",), } await self.async_client.get("/traced_custom_header/") spans = self.exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.kind, SpanKind.SERVER) for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) self.memory_exporter.clear() test_sqlcommenter.py000066400000000000000000000154371527070072300376240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=no-name-in-module from unittest.mock import MagicMock, patch import pytest from django import VERSION, conf from django.http import HttpResponse from django.test.utils import setup_test_environment, teardown_test_environment from opentelemetry.instrumentation.django import DjangoInstrumentor from opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware import ( SqlCommenter, _QueryWrapper, ) from opentelemetry.test.wsgitestutil import WsgiTestBase DJANGO_2_0 = VERSION >= (2, 0) _django_instrumentor = DjangoInstrumentor() class TestMiddleware(WsgiTestBase): @classmethod def setUpClass(cls): conf.settings.configure( SQLCOMMENTER_WITH_FRAMEWORK=False, SQLCOMMENTER_WITH_DB_DRIVER=False, ) super().setUpClass() def setUp(self): super().setUp() setup_test_environment() _django_instrumentor.instrument(is_sql_commentor_enabled=True) def tearDown(self): super().tearDown() teardown_test_environment() _django_instrumentor.uninstrument() @classmethod def tearDownClass(cls): super().tearDownClass() conf.settings = conf.LazySettings() @patch("opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware.SqlCommenter") def test_middleware_added(self, sqlcommenter_middleware): instance = sqlcommenter_middleware.return_value instance.get_response = HttpResponse() if DJANGO_2_0: middleware = conf.settings.MIDDLEWARE else: middleware = conf.settings.MIDDLEWARE_CLASSES self.assertTrue( "opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware.SqlCommenter" in middleware ) @patch("opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware.SqlCommenter") def test_middleware_added_at_position(self, sqlcommenter_middleware): _django_instrumentor.uninstrument() if DJANGO_2_0: middleware = conf.settings.MIDDLEWARE else: middleware = conf.settings.MIDDLEWARE_CLASSES # adding two dummy middlewares temprory_middelware = "django.utils.deprecation.MiddlewareMixin" middleware.append(temprory_middelware) middleware.append(temprory_middelware) middleware_position = 1 _django_instrumentor.instrument( is_sql_commentor_enabled=True, middleware_position=middleware_position, ) instance = sqlcommenter_middleware.return_value instance.get_response = HttpResponse() self.assertEqual( middleware[middleware_position], "opentelemetry.instrumentation.django.middleware.otel_middleware._DjangoMiddleware", ) self.assertEqual( middleware[middleware_position + 1], "opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware.SqlCommenter", ) @patch("opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware._get_opentelemetry_values") def test_query_wrapper(self, trace_capture): requests_mock = MagicMock() requests_mock.resolver_match.view_name = "view" requests_mock.resolver_match.route = "route" requests_mock.resolver_match.app_name = "app" trace_capture.return_value = { "traceparent": "*traceparent='00-000000000000000000000000deadbeef-000000000000beef-00" } qw_instance = _QueryWrapper(requests_mock) execute_mock_obj = MagicMock() qw_instance( execute_mock_obj, "Select 1;", MagicMock("test"), MagicMock("test1"), MagicMock(), ) output_sql = execute_mock_obj.call_args[0][0] self.assertEqual( output_sql, "Select 1 /*app_name='app',controller='view',route='route',traceparent='%%2Atraceparent%%3D%%2700-0000000" "00000000000000000deadbeef-000000000000beef-00'*/;", ) @patch("opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware._get_opentelemetry_values") def test_query_wrapper_non_string_queries(self, trace_capture): """Test that non-string queries and psycopg2 composable objects are handled correctly.""" requests_mock = MagicMock() requests_mock.resolver_match.view_name = "view" requests_mock.resolver_match.route = "route" requests_mock.resolver_match.app_name = "app" trace_capture.return_value = { "traceparent": "*traceparent='00-000000000000000000000000deadbeef-000000000000beef-00" } qw_instance = _QueryWrapper(requests_mock) execute_mock_obj = MagicMock() input_query = MagicMock(as_string=lambda conn: "SELECT 2") expected_query_start = "SELECT 2" qw_instance( execute_mock_obj, input_query, MagicMock("test"), MagicMock("test1"), MagicMock(), ) output_sql = execute_mock_obj.call_args[0][0] self.assertTrue( str(output_sql).startswith(str(expected_query_start)), f"Query should start with {expected_query_start!r}, got {output_sql!r}", ) @patch("opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware._QueryWrapper") def test_multiple_connection_support(self, query_wrapper): if not DJANGO_2_0: pytest.skip() requests_mock = MagicMock() get_response = MagicMock() sql_instance = SqlCommenter(get_response) sql_instance(requests_mock) # check if query_wrapper is added to the context for 2 databases self.assertEqual(query_wrapper.call_count, 2) @patch("opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware._get_opentelemetry_values") def test_empty_sql(self, trace_capture): requests_mock = MagicMock() requests_mock.resolver_match.view_name = "view" requests_mock.resolver_match.route = "route" requests_mock.resolver_match.app_name = "app" trace_capture.return_value = { "traceparent": "*traceparent='00-000000000000000000000000deadbeef-000000000000beef-00" } qw_instance = _QueryWrapper(requests_mock) execute_mock_obj = MagicMock() qw_instance( execute_mock_obj, "", MagicMock("test"), MagicMock("test1"), MagicMock(), ) output_sql = execute_mock_obj.call_args[0][0] self.assertEqual( output_sql, " /*app_name='app',controller='view',route='route',traceparent='%%2Atraceparent%%3D%%2700-0000000" "00000000000000000deadbeef-000000000000beef-00'*/", ) views.py000066400000000000000000000042161527070072300352020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-django/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from django.http import HttpResponse def traced(request): # pylint: disable=unused-argument return HttpResponse() def traced_template(request, year): # pylint: disable=unused-argument return HttpResponse() def error(request): # pylint: disable=unused-argument raise ValueError("error") def excluded(request): # pylint: disable=unused-argument return HttpResponse() def excluded_noarg(request): # pylint: disable=unused-argument return HttpResponse() def excluded_noarg2(request): # pylint: disable=unused-argument return HttpResponse() def route_span_name(request, *args, **kwargs): # pylint: disable=unused-argument return HttpResponse() def response_with_custom_header(request): response = HttpResponse() response["custom-test-header-1"] = "test-header-value-1" response["custom-test-header-2"] = "test-header-value-2" response["my-custom-regex-header-1"] = "my-custom-regex-value-1,my-custom-regex-value-2" response["my-custom-regex-header-2"] = "my-custom-regex-value-3,my-custom-regex-value-4" response["my-secret-header"] = "my-secret-value" return response async def async_traced(request): # pylint: disable=unused-argument return HttpResponse() async def async_traced_template(request, year): # pylint: disable=unused-argument return HttpResponse() async def async_error(request): # pylint: disable=unused-argument raise ValueError("error") async def async_excluded(request): # pylint: disable=unused-argument return HttpResponse() async def async_excluded_noarg(request): # pylint: disable=unused-argument return HttpResponse() async def async_excluded_noarg2(request): # pylint: disable=unused-argument return HttpResponse() async def async_route_span_name(request, *args, **kwargs): # pylint: disable=unused-argument return HttpResponse() async def async_with_custom_header(request): response = HttpResponse() response.headers["custom-test-header-1"] = "test-header-value-1" response.headers["custom-test-header-2"] = "test-header-value-2" return response opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-exceptions/000077500000000000000000000000001527070072300333245ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-exceptions/LICENSE000066400000000000000000000261351527070072300343400ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000016221527070072300347350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-exceptionsOpenTelemetry unhandled exceptions instrumentation ================================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-exceptions.svg :target: https://pypi.org/project/opentelemetry-instrumentation-exceptions/ Installation ------------ :: pip install opentelemetry-instrumentation-exceptions Usage ----- .. code-block:: python from opentelemetry.instrumentation.exceptions import ( UnhandledExceptionInstrumentor, ) UnhandledExceptionInstrumentor().instrument() This instrumentation captures uncaught process exceptions, uncaught thread exceptions, and unhandled asyncio task exceptions and emits them as OpenTelemetry logs. References ---------- * `OpenTelemetry Python Contrib repository `_ * `OpenTelemetry Project `_ pyproject.toml000066400000000000000000000031721527070072300361640ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-exceptions[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-exceptions" dynamic = ["version"] description = "OpenTelemetry unhandled exceptions instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api >= 1.42.0.dev, < 2.0.0", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", ] [project.optional-dependencies] instruments = [] [project.entry-points.opentelemetry_instrumentor] exceptions = "opentelemetry.instrumentation.exceptions:UnhandledExceptionInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-exceptions" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/exceptions/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-exceptions/src/000077500000000000000000000000001527070072300341135ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300367305ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-exceptions/srcinstrumentation/000077500000000000000000000000001527070072300421735ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-exceptions/src/opentelemetryexceptions/000077500000000000000000000000001527070072300443545ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-exceptions/src/opentelemetry/instrumentation__init__.py000066400000000000000000000124131527070072300464660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-exceptions/src/opentelemetry/instrumentation/exceptions# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Instrument uncaught exceptions to emit OpenTelemetry logs. Usage ----- .. code-block:: python from opentelemetry.instrumentation.exceptions import ( UnhandledExceptionInstrumentor, ) UnhandledExceptionInstrumentor().instrument() This instrumentation captures uncaught process exceptions, uncaught thread exceptions, and unhandled asyncio task exceptions and emits them as OpenTelemetry logs. """ from __future__ import annotations import asyncio import sys import threading from collections.abc import Collection from types import TracebackType from typing import Any from wrapt import ( wrap_function_wrapper, # type: ignore[reportUnknownVariableType] ) from opentelemetry._logs import Logger, SeverityNumber, get_logger from opentelemetry.instrumentation.exceptions.package import _instruments from opentelemetry.instrumentation.exceptions.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap from opentelemetry.semconv.schemas import Schemas class UnhandledExceptionInstrumentor(BaseInstrumentor): """Emit logs for uncaught exceptions and unhandled asyncio exceptions.""" def __init__(self): super().__init__() self._logger: Logger | None = None # pylint: disable-next=no-self-use def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs: Any): self._logger = get_logger( __name__, __version__, kwargs.get("logger_provider"), schema_url=Schemas.V1_37_0.value, ) self._install_sys_hook() self._install_threading_hook() self._install_asyncio_hook() def _uninstrument(self, **kwargs: Any): self._restore_sys_hook() self._restore_threading_hook() self._restore_asyncio_hook() self._logger = None def _install_sys_hook(self) -> None: wrap_function_wrapper( sys, "excepthook", self._wrap_sys_excepthook, ) @staticmethod def _restore_sys_hook() -> None: unwrap(sys, "excepthook") def _install_threading_hook(self) -> None: wrap_function_wrapper( threading, "excepthook", self._wrap_threading_excepthook, ) @staticmethod def _restore_threading_hook() -> None: unwrap(threading, "excepthook") def _install_asyncio_hook(self) -> None: wrap_function_wrapper( asyncio.BaseEventLoop, "call_exception_handler", self._wrap_asyncio_call_exception_handler, ) @staticmethod def _restore_asyncio_hook() -> None: unwrap(asyncio.BaseEventLoop, "call_exception_handler") def _emit_exception( self, exc: BaseException, *, severity_text: str, severity_number: SeverityNumber, event_name: str, ) -> None: # BaseException includes process-control signals like KeyboardInterrupt. if not isinstance(exc, Exception) or self._logger is None: return try: self._logger.emit( event_name=event_name, body=str(exc), severity_text=severity_text, severity_number=severity_number, exception=exc, ) # Logging must never replace the original unhandled exception path. # pylint: disable-next=broad-exception-caught except Exception: # pragma: no cover pass def _wrap_sys_excepthook( self, wrapped, instance, args: tuple[type[BaseException], BaseException, TracebackType | None], kwargs: dict[str, Any], ) -> None: _, exc, _ = args self._emit_exception( exc, severity_text="FATAL", severity_number=SeverityNumber.FATAL, event_name=type(exc).__name__, ) wrapped(*args, **kwargs) def _wrap_threading_excepthook( self, wrapped, instance, args: tuple[threading.ExceptHookArgs], kwargs: dict[str, Any], ) -> None: (hook_args,) = args self._emit_exception( hook_args.exc_value, severity_text="ERROR", severity_number=SeverityNumber.ERROR, event_name=hook_args.exc_type.__name__, ) wrapped(*args, **kwargs) def _wrap_asyncio_call_exception_handler( self, wrapped, instance: asyncio.AbstractEventLoop, args: tuple[dict[str, Any]], kwargs: dict[str, Any], ) -> None: context = args[0] if args else kwargs.get("context") if context: exc = context.get("exception") if isinstance(exc, BaseException): message = context.get("message") self._emit_exception( exc, severity_text="ERROR", severity_number=SeverityNumber.ERROR, event_name=str(message) if message else type(exc).__name__, ) wrapped(*args, **kwargs) __all__ = ["UnhandledExceptionInstrumentor"] package.py000066400000000000000000000001721527070072300463210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-exceptions/src/opentelemetry/instrumentation/exceptions# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = () _supports_metrics = False version.py000066400000000000000000000001441527070072300464120ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-exceptions/src/opentelemetry/instrumentation/exceptions# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000004201527070072300375020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-exceptionsasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-exceptions opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-exceptions/tests/000077500000000000000000000000001527070072300344665ustar00rootroot00000000000000test_exceptions.py000066400000000000000000000247231527070072300402110ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-exceptions/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import asyncio import sys import threading from collections.abc import Generator import pytest import opentelemetry._logs._internal from opentelemetry._logs import ( SeverityNumber, get_logger_provider, set_logger_provider, ) from opentelemetry.instrumentation.exceptions import ( UnhandledExceptionInstrumentor, ) from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import ( InMemoryLogRecordExporter, SimpleLogRecordProcessor, ) from opentelemetry.semconv.attributes import exception_attributes from opentelemetry.util._once import Once # pylint: disable=redefined-outer-name @pytest.fixture def log_exporter() -> Generator[InMemoryLogRecordExporter, None, None]: snapshot = get_logger_provider() opentelemetry._logs._internal._LOGGER_PROVIDER_SET_ONCE = Once() provider = LoggerProvider() exporter = InMemoryLogRecordExporter() provider.add_log_record_processor(SimpleLogRecordProcessor(exporter)) set_logger_provider(provider) try: yield exporter finally: opentelemetry._logs._internal._LOGGER_PROVIDER_SET_ONCE = Once() set_logger_provider(snapshot) @pytest.fixture def instrumentor() -> Generator[UnhandledExceptionInstrumentor, None, None]: inst = UnhandledExceptionInstrumentor() try: yield inst finally: inst.uninstrument() def _raised_value_error() -> ValueError: try: raise ValueError("boom") except ValueError as exc: return exc def _finished_log(log_exporter: InMemoryLogRecordExporter): logs = log_exporter.get_finished_logs() assert len(logs) == 1 return logs[0].log_record def test_sys_excepthook_emits_log( log_exporter: InMemoryLogRecordExporter, monkeypatch: pytest.MonkeyPatch, instrumentor: UnhandledExceptionInstrumentor, ) -> None: called = {"value": False} def stub_excepthook(exc_type, exc, tb) -> None: called["value"] = True monkeypatch.setattr(sys, "excepthook", stub_excepthook) instrumentor.instrument() exc = _raised_value_error() sys.excepthook(type(exc), exc, exc.__traceback__) log_record = _finished_log(log_exporter) assert log_record.severity_text == "FATAL" assert log_record.severity_number == SeverityNumber.FATAL assert log_record.event_name == "ValueError" assert log_record.body == "boom" assert called["value"] is True attributes = log_record.attributes assert attributes[exception_attributes.EXCEPTION_TYPE] == "ValueError" assert isinstance(attributes[exception_attributes.EXCEPTION_TYPE], str) assert attributes[exception_attributes.EXCEPTION_MESSAGE] == "boom" assert isinstance(attributes[exception_attributes.EXCEPTION_MESSAGE], str) assert "ValueError: boom" in attributes[exception_attributes.EXCEPTION_STACKTRACE] assert isinstance(attributes[exception_attributes.EXCEPTION_STACKTRACE], str) def test_threading_excepthook_emits_log( log_exporter: InMemoryLogRecordExporter, monkeypatch: pytest.MonkeyPatch, instrumentor: UnhandledExceptionInstrumentor, ) -> None: called = {"value": False} def stub_threading_excepthook(args: threading.ExceptHookArgs) -> None: called["value"] = True monkeypatch.setattr(threading, "excepthook", stub_threading_excepthook) instrumentor.instrument() exc = _raised_value_error() args = threading.ExceptHookArgs((type(exc), exc, exc.__traceback__, threading.current_thread())) threading.excepthook(args) log_record = _finished_log(log_exporter) assert log_record.severity_text == "ERROR" assert log_record.severity_number == SeverityNumber.ERROR assert log_record.event_name == "ValueError" assert log_record.body == "boom" assert called["value"] is True attributes = log_record.attributes assert attributes[exception_attributes.EXCEPTION_TYPE] == "ValueError" assert attributes[exception_attributes.EXCEPTION_MESSAGE] == "boom" assert "ValueError: boom" in attributes[exception_attributes.EXCEPTION_STACKTRACE] def test_asyncio_unhandled_exception_emits_log( log_exporter: InMemoryLogRecordExporter, monkeypatch: pytest.MonkeyPatch, instrumentor: UnhandledExceptionInstrumentor, ) -> None: called = {"value": False} original_handler = asyncio.BaseEventLoop.call_exception_handler def stub_call_exception_handler(loop, context) -> None: called["value"] = True original_handler(loop, context) monkeypatch.setattr( asyncio.BaseEventLoop, "call_exception_handler", stub_call_exception_handler, ) instrumentor.instrument() loop = asyncio.new_event_loop() loop.set_exception_handler(lambda _loop, _context: None) try: exc = _raised_value_error() loop.call_exception_handler({"exception": exc, "message": "task boom"}) finally: loop.close() log_record = _finished_log(log_exporter) assert log_record.severity_text == "ERROR" assert log_record.severity_number == SeverityNumber.ERROR assert log_record.event_name == "task boom" assert log_record.body == "boom" assert called["value"] is True attributes = log_record.attributes assert attributes[exception_attributes.EXCEPTION_TYPE] == "ValueError" assert attributes[exception_attributes.EXCEPTION_MESSAGE] == "boom" assert "ValueError: boom" in attributes[exception_attributes.EXCEPTION_STACKTRACE] def test_asyncio_unhandled_exception_called_with_kwarg_emits_log( log_exporter: InMemoryLogRecordExporter, monkeypatch: pytest.MonkeyPatch, instrumentor: UnhandledExceptionInstrumentor, ) -> None: called = {"value": False} original_handler = asyncio.BaseEventLoop.call_exception_handler def stub_call_exception_handler(loop, context) -> None: called["value"] = True original_handler(loop, context) monkeypatch.setattr( asyncio.BaseEventLoop, "call_exception_handler", stub_call_exception_handler, ) instrumentor.instrument() loop = asyncio.new_event_loop() loop.set_exception_handler(lambda _loop, _context: None) try: exc = _raised_value_error() loop.call_exception_handler(context={"exception": exc, "message": "task boom"}) finally: loop.close() log_record = _finished_log(log_exporter) assert log_record.severity_text == "ERROR" assert log_record.severity_number == SeverityNumber.ERROR assert log_record.event_name == "task boom" assert log_record.body == "boom" assert called["value"] is True attributes = log_record.attributes assert attributes[exception_attributes.EXCEPTION_TYPE] == "ValueError" assert attributes[exception_attributes.EXCEPTION_MESSAGE] == "boom" assert "ValueError: boom" in attributes[exception_attributes.EXCEPTION_STACKTRACE] def test_asyncio_unhandled_exception_invalid_call( log_exporter: InMemoryLogRecordExporter, monkeypatch: pytest.MonkeyPatch, instrumentor: UnhandledExceptionInstrumentor, ) -> None: called = {"value": False} original_handler = asyncio.BaseEventLoop.call_exception_handler def stub_call_exception_handler(loop, context) -> None: called["value"] = True original_handler(loop, context) monkeypatch.setattr( asyncio.BaseEventLoop, "call_exception_handler", stub_call_exception_handler, ) instrumentor.instrument() loop = asyncio.new_event_loop() loop.set_exception_handler(lambda _loop, _context: None) try: _raised_value_error() with pytest.raises(TypeError): loop.call_exception_handler() finally: loop.close() logs = log_exporter.get_finished_logs() assert not logs @pytest.mark.parametrize( ("message", "expected_event_name"), ((None, "ValueError"), (42, "42")), ) def test_asyncio_event_name_falls_back_or_coerces( log_exporter: InMemoryLogRecordExporter, monkeypatch: pytest.MonkeyPatch, instrumentor: UnhandledExceptionInstrumentor, message: object, expected_event_name: str, ) -> None: monkeypatch.setattr( asyncio.BaseEventLoop, "call_exception_handler", lambda _loop, _context: None, ) instrumentor.instrument() loop = asyncio.new_event_loop() loop.set_exception_handler(lambda _loop, _context: None) try: exc = _raised_value_error() loop.call_exception_handler({"exception": exc, "message": message}) finally: loop.close() assert _finished_log(log_exporter).event_name == expected_event_name def test_base_exceptions_are_not_emitted( log_exporter: InMemoryLogRecordExporter, monkeypatch: pytest.MonkeyPatch, instrumentor: UnhandledExceptionInstrumentor, ) -> None: called = {"value": False} def stub_excepthook(exc_type, exc, tb) -> None: called["value"] = True monkeypatch.setattr(sys, "excepthook", stub_excepthook) instrumentor.instrument() exc = KeyboardInterrupt() sys.excepthook(type(exc), exc, exc.__traceback__) assert not log_exporter.get_finished_logs() assert called["value"] is True def test_double_instrument_does_not_emit_duplicate_logs( log_exporter: InMemoryLogRecordExporter, monkeypatch: pytest.MonkeyPatch, instrumentor: UnhandledExceptionInstrumentor, ) -> None: monkeypatch.setattr(sys, "excepthook", lambda _exc_type, _exc, _tb: None) instrumentor.instrument() instrumentor.instrument() exc = _raised_value_error() sys.excepthook(type(exc), exc, exc.__traceback__) assert len(log_exporter.get_finished_logs()) == 1 def test_uninstrument_restores_hooks( monkeypatch: pytest.MonkeyPatch, ) -> None: instrumentor = UnhandledExceptionInstrumentor() def original_sys(exc_type, exc, tb) -> None: del exc_type, exc, tb def original_threading(args: threading.ExceptHookArgs) -> None: del args def original_asyncio(loop, context) -> None: del loop, context monkeypatch.setattr(sys, "excepthook", original_sys) monkeypatch.setattr(threading, "excepthook", original_threading) monkeypatch.setattr( asyncio.BaseEventLoop, "call_exception_handler", original_asyncio, ) instrumentor.instrument(logger_provider=LoggerProvider()) instrumentor.uninstrument() assert sys.excepthook is original_sys assert threading.excepthook is original_threading assert asyncio.BaseEventLoop.call_exception_handler is original_asyncio opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falcon/000077500000000000000000000000001527070072300324055ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falcon/LICENSE000066400000000000000000000261351527070072300334210ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falcon/README.rst000066400000000000000000000053341527070072300341010ustar00rootroot00000000000000OpenTelemetry Falcon Tracing ============================ |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-falcon.svg :target: https://pypi.org/project/opentelemetry-instrumentation-falcon/ This library builds on the OpenTelemetry WSGI middleware to track web requests in Falcon applications. Installation ------------ :: pip install opentelemetry-instrumentation-falcon Configuration ------------- Exclude lists ************* To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_FALCON_EXCLUDED_URLS`` (or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude. For example, :: export OTEL_PYTHON_FALCON_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. Request attributes ******************** To extract certain attributes from Falcon's request object and use them as span attributes, set the environment variable ``OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS`` to a comma delimited list of request attribute names. For example, :: export OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS='query_string,uri_template' will extract path_info and content_type attributes from every traced request and add them as span attritbues. Falcon Request object reference: https://falcon.readthedocs.io/en/stable/api/request_and_response.html#id1 Usage ----- .. code-block:: python import falcon from opentelemetry.instrumentation.falcon import FalconInstrumentor FalconInstrumentor().instrument() app = falcon.App() class HelloWorldResource(object): def on_get(self, req, resp): resp.text = 'Hello World' app.add_route('/hello', HelloWorldResource()) Request/Response hooks ********************** The instrumentation supports specifying request and response hooks. These are functions that get called back by the instrumentation right after a Span is created for a request and right before the span is finished while processing a response. The hooks can be configured as follows: .. code-block:: python from opentelemetry.instrumentation.falcon import FalconInstrumentor def request_hook(span, req): pass def response_hook(span, req, resp): pass FalconInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook) References ---------- * `OpenTelemetry Falcon Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000033201527070072300352400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falcon[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-falcon" dynamic = ["version"] description = "Falcon instrumentation for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-instrumentation-wsgi == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-util-http == 0.66b2", "packaging >= 20.0", ] [project.optional-dependencies] instruments = [ "falcon >= 1.4.1, < 5.0.0", ] [project.entry-points.opentelemetry_instrumentor] falcon = "opentelemetry.instrumentation.falcon:FalconInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-falcon" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/falcon/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falcon/src/000077500000000000000000000000001527070072300331745ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300360115ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falcon/srcinstrumentation/000077500000000000000000000000001527070072300412545ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetryfalcon/000077500000000000000000000000001527070072300425165ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation__init__.py000066400000000000000000000513221527070072300446320ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This library builds on the OpenTelemetry WSGI middleware to track web requests in Falcon applications. In addition to opentelemetry-instrumentation-wsgi, it supports falcon-specific features such as: * The Falcon resource and method name is used as the Span name. * The ``falcon.resource`` Span attribute is set so the matched resource. * Errors from Falcon resources are properly caught and recorded. Configuration ------------- Exclude lists ************* To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_FALCON_EXCLUDED_URLS`` (or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the URLs. For example, :: export OTEL_PYTHON_FALCON_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. Request attributes ******************** To extract attributes from Falcon's request object and use them as span attributes, set the environment variable ``OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS`` to a comma delimited list of request attribute names. For example, :: export OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS='query_string,uri_template' will extract the ``query_string`` and ``uri_template`` attributes from every traced request and add them as span attributes. Falcon Request object reference: https://falcon.readthedocs.io/en/stable/api/request_and_response.html#id1 Usage ----- .. code-block:: python import falcon from opentelemetry.instrumentation.falcon import FalconInstrumentor FalconInstrumentor().instrument() app = falcon.App() class HelloWorldResource(object): def on_get(self, req, resp): resp.text = "Hello World" app.add_route("/hello", HelloWorldResource()) Request and Response hooks *************************** This instrumentation supports request and response hooks. These are functions that get called right after a span is created for a request and right before the span is finished for the response. The hooks can be configured as follows: .. code-block:: python from opentelemetry.instrumentation.falcon import FalconInstrumentor def request_hook(span, req): pass def response_hook(span, req, resp): pass FalconInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook) Capture HTTP request and response headers ***************************************** You can configure the agent to capture specified HTTP headers as span attributes, according to the `semantic convention `_. Request headers *************** To capture HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. Request header names in Falcon are case-insensitive and ``-`` characters are replaced by ``_``. So, giving the header name as ``CUStom_Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" Would match all request headers that start with ``Accept`` and ``X-``. To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*" The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a single item list containing all the header values. For example: ``http.request.header.custom_request_header = [","]`` Response headers **************** To capture HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. Response header names in Falcon are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" Would match all response headers that start with ``Content`` and ``X-``. To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*" The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a single item list containing all the header values. For example: ``http.response.header.custom_response_header = [","]`` Sanitizing headers ****************** In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be matched in a case-insensitive manner. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. Note: The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. API --- """ from collections.abc import Collection from logging import getLogger from sys import exc_info from time import time_ns from timeit import default_timer import falcon from packaging import version as package_version import opentelemetry.instrumentation.wsgi as otel_wsgi from opentelemetry import context, trace from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, _get_schema_url, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _report_new, _report_old, _StabilityMode, ) from opentelemetry.instrumentation.falcon.package import _instruments from opentelemetry.instrumentation.falcon.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.propagators import ( FuncSetter, get_global_response_propagator, ) from opentelemetry.instrumentation.utils import ( _start_internal_or_server_span, extract_attributes_from_object, ) from opentelemetry.metrics import get_meter from opentelemetry.semconv.attributes.http_attributes import ( HTTP_ROUTE, ) from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.semconv.metrics.http_metrics import ( HTTP_SERVER_REQUEST_DURATION, ) from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs _logger = getLogger(__name__) _ENVIRON_STARTTIME_KEY = "opentelemetry-falcon.starttime_key" _ENVIRON_SPAN_KEY = "opentelemetry-falcon.span_key" _ENVIRON_REQ_ATTRS = "opentelemetry-falcon.req_attrs" _ENVIRON_ACTIVATION_KEY = "opentelemetry-falcon.activation_key" _ENVIRON_TOKEN = "opentelemetry-falcon.token" _ENVIRON_EXC = "opentelemetry-falcon.exc" _response_propagation_setter = FuncSetter(falcon.Response.append_header) _parsed_falcon_version = package_version.parse(falcon.__version__) if _parsed_falcon_version >= package_version.parse("3.0.0"): # Falcon 3 _instrument_app = "App" _falcon_version = 3 elif _parsed_falcon_version >= package_version.parse("2.0.0"): # Falcon 2 _instrument_app = "API" _falcon_version = 2 else: # Falcon 1 _instrument_app = "API" _falcon_version = 1 class _InstrumentedFalconAPI(getattr(falcon, _instrument_app)): _instrumented_falcon_apps = set() def __init__(self, *args, **kwargs): otel_opts = kwargs.pop("_otel_opts", {}) self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) # inject trace middleware self._middlewares_list = kwargs.pop("middleware", []) if self._middlewares_list is None: self._middlewares_list = [] tracer_provider = otel_opts.pop("tracer_provider", None) meter_provider = otel_opts.pop("meter_provider", None) if not isinstance(self._middlewares_list, (list, tuple)): self._middlewares_list = [self._middlewares_list] self._otel_tracer = trace.get_tracer( __name__, __version__, tracer_provider, schema_url=_get_schema_url(self._sem_conv_opt_in_mode), ) self._otel_meter = get_meter( __name__, __version__, meter_provider, schema_url=_get_schema_url(self._sem_conv_opt_in_mode), ) self.duration_histogram_old = None if _report_old(self._sem_conv_opt_in_mode): self.duration_histogram_old = self._otel_meter.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", description="Measures the duration of inbound HTTP requests.", ) self.duration_histogram_new = None if _report_new(self._sem_conv_opt_in_mode): self.duration_histogram_new = self._otel_meter.create_histogram( name=HTTP_SERVER_REQUEST_DURATION, description="Duration of HTTP server requests.", unit="s", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) self.active_requests_counter = self._otel_meter.create_up_down_counter( name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, unit="requests", description="measures the number of concurrent HTTP requests that are currently in-flight", ) trace_middleware = _TraceMiddleware( self._otel_tracer, otel_opts.pop("traced_request_attributes", get_traced_request_attrs("FALCON")), otel_opts.pop("request_hook", None), otel_opts.pop("response_hook", None), self._sem_conv_opt_in_mode, ) self._middlewares_list.insert(0, trace_middleware) kwargs["middleware"] = self._middlewares_list self._otel_excluded_urls = get_excluded_urls("FALCON") self._is_instrumented_by_opentelemetry = True _InstrumentedFalconAPI._instrumented_falcon_apps.add(self) super().__init__(*args, **kwargs) def __del__(self): if self in _InstrumentedFalconAPI._instrumented_falcon_apps: _InstrumentedFalconAPI._instrumented_falcon_apps.remove(self) def _handle_exception(self, *args): # Falcon 3 does not execute middleware within the context # of the exception so we capture the exception here and # save it into the env dict if not self._is_instrumented_by_opentelemetry: return super()._handle_exception(*args) if _falcon_version == 1: _, req, _, _ = args # ex, req, resp, params else: req, _, _, _ = args # req, resp, ex, params _, exc, _ = exc_info() req.env[_ENVIRON_EXC] = exc return super()._handle_exception(*args) def __call__(self, env, start_response): # pylint: disable=unnecessary-dunder-call # pylint: disable=too-many-locals # pylint: disable=too-many-branches if self._otel_excluded_urls.url_disabled(env.get("PATH_INFO", "/")): return super().__call__(env, start_response) if not self._is_instrumented_by_opentelemetry: return super().__call__(env, start_response) start_time = time_ns() attributes = otel_wsgi.collect_request_attributes(env, self._sem_conv_opt_in_mode) span, token = _start_internal_or_server_span( tracer=self._otel_tracer, span_name=otel_wsgi.get_default_span_name(env), start_time=start_time, context_carrier=env, context_getter=otel_wsgi.wsgi_getter, attributes=attributes, ) active_requests_count_attrs = otel_wsgi._parse_active_request_count_attrs( attributes, self._sem_conv_opt_in_mode ) self.active_requests_counter.add(1, active_requests_count_attrs) if span.is_recording(): if span.is_recording() and span.kind == trace.SpanKind.SERVER: custom_attributes = otel_wsgi.collect_custom_request_headers_attributes(env) if len(custom_attributes) > 0: span.set_attributes(custom_attributes) activation = trace.use_span(span, end_on_exit=True) activation.__enter__() env[_ENVIRON_SPAN_KEY] = span env[_ENVIRON_ACTIVATION_KEY] = activation env[_ENVIRON_REQ_ATTRS] = attributes exception = None def _start_response(status, response_headers, *args, **kwargs): response = start_response(status, response_headers, *args, **kwargs) return response start = default_timer() try: return super().__call__(env, _start_response) except Exception as exc: exception = exc raise finally: duration_s = default_timer() - start if self.duration_histogram_old: duration_attrs = otel_wsgi._parse_duration_attrs(attributes, _StabilityMode.DEFAULT) self.duration_histogram_old.record(max(round(duration_s * 1000), 0), duration_attrs) if self.duration_histogram_new: duration_attrs = otel_wsgi._parse_duration_attrs(attributes, _StabilityMode.HTTP) self.duration_histogram_new.record(max(duration_s, 0), duration_attrs) self.active_requests_counter.add(-1, active_requests_count_attrs) if exception is None: activation.__exit__(None, None, None) else: activation.__exit__( type(exception), exception, getattr(exception, "__traceback__", None), ) if token is not None: context.detach(token) class _TraceMiddleware: # pylint:disable=R0201,W0613 def __init__( self, tracer=None, traced_request_attrs=None, request_hook=None, response_hook=None, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ): self.tracer = tracer self._traced_request_attrs = traced_request_attrs self._request_hook = request_hook self._response_hook = response_hook self._sem_conv_opt_in_mode = sem_conv_opt_in_mode def process_request(self, req, resp): span = req.env.get(_ENVIRON_SPAN_KEY) if span and self._request_hook: self._request_hook(span, req) if not span or not span.is_recording(): return attributes = extract_attributes_from_object(req, self._traced_request_attrs) for key, value in attributes.items(): span.set_attribute(key, value) def process_resource(self, req, resp, resource, params): span = req.env.get(_ENVIRON_SPAN_KEY) if not span or not span.is_recording(): return resource_name = resource.__class__.__name__ span.set_attribute("falcon.resource", resource_name) def process_response(self, req, resp, resource, req_succeeded=None): # pylint:disable=R0201,R0912 span = req.env.get(_ENVIRON_SPAN_KEY) req_attrs = req.env.get(_ENVIRON_REQ_ATTRS) if not span: return status = resp.status if resource is None: status = falcon.HTTP_404 else: exc_type, exc = None, None if _ENVIRON_EXC in req.env: exc = req.env[_ENVIRON_EXC] exc_type = type(exc) if exc_type and not req_succeeded: if "HTTPNotFound" in exc_type.__name__: status = falcon.HTTP_404 elif isinstance(exc, (falcon.HTTPError, falcon.HTTPStatus)): try: if _falcon_version > 2: status = falcon.code_to_http_status(exc.status) else: status = exc.status except ValueError: status = falcon.HTTP_500 else: status = falcon.HTTP_500 # Falcon 1 does not support response headers. So # send an empty dict. response_headers = {} if _falcon_version > 1: response_headers = resp.headers otel_wsgi.add_response_attributes( span, status, response_headers, req_attrs, self._sem_conv_opt_in_mode, ) if _report_new(self._sem_conv_opt_in_mode) and req.uri_template and req_attrs is not None: req_attrs[HTTP_ROUTE] = req.uri_template try: if span.is_recording() and span.kind == trace.SpanKind.SERVER: # Check if low-cardinality route is available as per semantic-conventions if req.uri_template: span.update_name(f"{req.method} {req.uri_template}") span.set_attribute(HTTP_ROUTE, req.uri_template) else: span.update_name(f"{req.method}") custom_attributes = otel_wsgi.collect_custom_response_headers_attributes(response_headers.items()) if len(custom_attributes) > 0: span.set_attributes(custom_attributes) except ValueError: pass propagator = get_global_response_propagator() if propagator: propagator.inject(resp, setter=_response_propagation_setter) if self._response_hook: self._response_hook(span, req, resp) class FalconInstrumentor(BaseInstrumentor): # pylint: disable=protected-access """An instrumentor for falcon.API See `BaseInstrumentor` """ def instrumentation_dependencies(self) -> Collection[str]: return _instruments # pylint:disable=no-self-use def _remove_instrumented_middleware(self, app): if hasattr(app, "_is_instrumented_by_opentelemetry") and app._is_instrumented_by_opentelemetry: if _falcon_version == 3: app._unprepared_middleware = [ x for x in app._unprepared_middleware if not isinstance(x, _TraceMiddleware) ] app._middleware = app._prepare_middleware( app._unprepared_middleware, independent_middleware=app._independent_middleware, ) else: app._middlewares_list = [x for x in app._middlewares_list if not isinstance(x, _TraceMiddleware)] # pylint: disable=no-member app._middleware = falcon.api_helpers.prepare_middleware( app._middlewares_list, independent_middleware=app._independent_middleware, ) app._is_instrumented_by_opentelemetry = False def _instrument(self, **opts): self._original_falcon_api = getattr(falcon, _instrument_app) class FalconAPI(_InstrumentedFalconAPI): def __init__(self, *args, **kwargs): kwargs["_otel_opts"] = opts super().__init__(*args, **kwargs) setattr(falcon, _instrument_app, FalconAPI) def _uninstrument(self, **kwargs): for app in _InstrumentedFalconAPI._instrumented_falcon_apps: self._remove_instrumented_middleware(app) _InstrumentedFalconAPI._instrumented_falcon_apps.clear() setattr(falcon, _instrument_app, self._original_falcon_api) package.py000066400000000000000000000002641527070072300444650ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("falcon >= 1.4.1, < 5.0.0",) _supports_metrics = True _semconv_status = "migration" version.py000066400000000000000000000001441527070072300445540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements-0.txt000066400000000000000000000006241527070072300367260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falconasgiref==3.8.1 Deprecated==1.2.14 falcon==1.4.1 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 python-mimeparse==1.6.0 six==1.16.0 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-wsgi -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-falcon test-requirements-1.txt000066400000000000000000000005601527070072300367260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falconasgiref==3.8.1 Deprecated==1.2.14 falcon==2.0.0 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-wsgi -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-falcon test-requirements-2.txt000066400000000000000000000005601527070072300367270ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falconasgiref==3.8.1 Deprecated==1.2.14 falcon==3.1.1 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-wsgi -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-falcon test-requirements-3.txt000066400000000000000000000005601527070072300367300ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falconasgiref==3.8.1 Deprecated==1.2.14 falcon==3.1.3 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-wsgi -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-falcon test-requirements-4.txt000066400000000000000000000005601527070072300367310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falconasgiref==3.8.1 Deprecated==1.2.14 falcon==4.0.2 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-wsgi -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-falcon opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falcon/tests/000077500000000000000000000000001527070072300335475ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300355670ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falcon/testsapp.py000066400000000000000000000052331527070072300346250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falcon/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import falcon from packaging import version as package_version # pylint:disable=R0201,W0613,E0602 _parsed_falcon_version = package_version.parse(falcon.__version__) class HelloWorldResource: def _handle_request(self, _, resp): # pylint: disable=no-member resp.status = falcon.HTTP_201 if _parsed_falcon_version < package_version.parse("3.0.0"): # Falcon 1 and Falcon 2 resp.body = "Hello World" else: resp.text = "Hello World" def on_get(self, req, resp): self._handle_request(req, resp) def on_put(self, req, resp): self._handle_request(req, resp) def on_patch(self, req, resp): self._handle_request(req, resp) def on_post(self, req, resp): self._handle_request(req, resp) def on_delete(self, req, resp): self._handle_request(req, resp) def on_head(self, req, resp): self._handle_request(req, resp) class ErrorResource: def on_get(self, req, resp): print(non_existent_var) # noqa class CustomResponseHeaderResource: def on_get(self, _, resp): # pylint: disable=no-member resp.status = falcon.HTTP_201 resp.set_header("content-type", "text/plain; charset=utf-8") resp.set_header("content-length", "0") resp.set_header("my-custom-header", "my-custom-value-1,my-custom-header-2") resp.set_header("dont-capture-me", "test-value") resp.set_header( "my-custom-regex-header-1", "my-custom-regex-value-1,my-custom-regex-value-2", ) resp.set_header( "My-Custom-Regex-Header-2", "my-custom-regex-value-3,my-custom-regex-value-4", ) resp.set_header("my-secret-header", "my-secret-value") class UserResource: def on_get(self, req, resp, user_id): # pylint: disable=no-member resp.status = falcon.HTTP_200 if _parsed_falcon_version < package_version.parse("3.0.0"): # Falcon 1 and Falcon 2 resp.body = f"Hello user {user_id}" else: resp.text = f"Hello user {user_id}" def make_app(): if _parsed_falcon_version < package_version.parse("3.0.0"): # Falcon 1 and Falcon 2 app = falcon.API() else: # Falcon 3 app = falcon.App() app.add_route("/hello", HelloWorldResource()) app.add_route("/ping", HelloWorldResource()) app.add_route("/error", ErrorResource()) app.add_route("/test_custom_response_headers", CustomResponseHeaderResource()) app.add_route("/user/{user_id}", UserResource()) return app test_falcon.py000066400000000000000000000774521527070072300363620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-falcon/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # from timeit import default_timer from unittest.mock import Mock, patch import pytest from falcon import __version__ as _falcon_version from falcon import testing from packaging import version as package_version from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, _server_active_requests_count_attrs_new, _server_active_requests_count_attrs_old, _server_duration_attrs_new, _server_duration_attrs_old, ) from opentelemetry.instrumentation.falcon import FalconInstrumentor from opentelemetry.instrumentation.propagators import ( TraceResponsePropagator, get_global_response_propagator, set_global_response_propagator, ) from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, NumberDataPoint, ) from opentelemetry.sdk.resources import Resource from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_FLAVOR, HTTP_HOST, HTTP_METHOD, HTTP_SCHEME, HTTP_SERVER_NAME, HTTP_STATUS_CODE, HTTP_TARGET, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_HOST_PORT, NET_PEER_IP, NET_PEER_PORT, ) from opentelemetry.semconv.attributes.client_attributes import ( CLIENT_PORT, ) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, HTTP_ROUTE, ) from opentelemetry.semconv.attributes.network_attributes import ( NETWORK_PROTOCOL_VERSION, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.semconv.attributes.url_attributes import ( URL_PATH, URL_SCHEME, ) from opentelemetry.test.test_base import TestBase from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.trace import StatusCode from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, ) from .app import make_app _expected_metric_names = [ "http.server.active_requests", "http.server.duration", ] _recommended_attrs = { "http.server.active_requests": _server_active_requests_count_attrs_new + _server_active_requests_count_attrs_old, "http.server.duration": _server_duration_attrs_new + _server_duration_attrs_old, } _recommended_metrics_attrs_old = { "http.server.active_requests": _server_active_requests_count_attrs_old, "http.server.duration": _server_duration_attrs_old, } _recommended_metrics_attrs_new = { "http.server.active_requests": _server_active_requests_count_attrs_new, "http.server.request.duration": _server_duration_attrs_new, } _server_active_requests_count_attrs_both = _server_active_requests_count_attrs_old _server_active_requests_count_attrs_both.extend(_server_active_requests_count_attrs_new) _recommended_metrics_attrs_both = { "http.server.active_requests": _server_active_requests_count_attrs_both, "http.server.duration": _server_duration_attrs_old, "http.server.request.duration": _server_duration_attrs_new, } _parsed_falcon_version = package_version.parse(_falcon_version) SCOPE = "opentelemetry.instrumentation.falcon" class TestFalconBase(TestBase): def setUp(self): super().setUp() test_name = "" if hasattr(self, "_testMethodName"): test_name = self._testMethodName sem_conv_mode = "default" if "new_semconv" in test_name: sem_conv_mode = "http" elif "both_semconv" in test_name: sem_conv_mode = "http/dup" self.env_patch = patch.dict( "os.environ", { "OTEL_PYTHON_FALCON_EXCLUDED_URLS": "ping", "OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS": "query_string", OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) _OpenTelemetrySemanticConventionStability._initialized = False self.env_patch.start() FalconInstrumentor().instrument( request_hook=getattr(self, "request_hook", None), response_hook=getattr(self, "response_hook", None), ) self.app = make_app() @property def _has_fixed_http_target(self): # In falcon<3.1.2, HTTP_TARGET is always set to / in TestClient # In falcon>=3.1.2, HTTP_TARGET is set to unencoded path by default # https://github.com/falconry/falcon/blob/69cdcd6edd2ee33f4ac9f7793e1cc3c4f99da692/falcon/testing/helpers.py#L1153-1156 return _parsed_falcon_version < package_version.parse("3.1.2") def client(self): return testing.TestClient(self.app) def tearDown(self): super().tearDown() with self.disable_logging(): FalconInstrumentor().uninstrument() self.env_patch.stop() # pylint: disable=too-many-public-methods class TestFalconInstrumentation(TestFalconBase, WsgiTestBase): def test_get(self): self._test_method("GET") def test_get_new_semconv(self): self._test_method("GET", old_semconv=False, new_semconv=True) def test_get_both_semconv(self): self._test_method("GET", old_semconv=True, new_semconv=True) def test_post(self): self._test_method("POST") def test_post_new_semconv(self): self._test_method("POST", old_semconv=False, new_semconv=True) def test_post_both_semconv(self): self._test_method("POST", old_semconv=True, new_semconv=True) def test_patch(self): self._test_method("PATCH") def test_patch_new_semconv(self): self._test_method("PATCH", old_semconv=False, new_semconv=True) def test_patch_both_semconv(self): self._test_method("PATCH", old_semconv=True, new_semconv=True) def test_put(self): self._test_method("PUT") def test_put_new_semconv(self): self._test_method("PUT", old_semconv=False, new_semconv=True) def test_put_both_semconv(self): self._test_method("PUT", old_semconv=True, new_semconv=True) def test_delete(self): self._test_method("DELETE") def test_delete_new_semconv(self): self._test_method("DELETE", old_semconv=False, new_semconv=True) def test_delete_both_semconv(self): self._test_method("DELETE", old_semconv=True, new_semconv=True) def test_head(self): self._test_method("HEAD") def test_head_new_semconv(self): self._test_method("HEAD", old_semconv=False, new_semconv=True) def test_head_both_semconv(self): self._test_method("HEAD", old_semconv=True, new_semconv=True) def _test_method(self, method, old_semconv=True, new_semconv=False): self.client().simulate_request(method=method, path="/hello") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, f"{method} /hello") self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual( span.status.description, None, ) expected_attributes = {} expected_attributes_old = { HTTP_METHOD: method, HTTP_SERVER_NAME: "falconframework.org", HTTP_SCHEME: "http", NET_HOST_PORT: 80, HTTP_HOST: "falconframework.org", HTTP_TARGET: "/" if self._has_fixed_http_target else "/hello", NET_PEER_PORT: 65133, HTTP_FLAVOR: "1.1", "falcon.resource": "HelloWorldResource", HTTP_STATUS_CODE: 201, HTTP_ROUTE: "/hello", } expected_attributes_new = { HTTP_REQUEST_METHOD: method, SERVER_ADDRESS: "falconframework.org", URL_SCHEME: "http", SERVER_PORT: 80, URL_PATH: "/hello", CLIENT_PORT: 65133, NETWORK_PROTOCOL_VERSION: "1.1", "falcon.resource": "HelloWorldResource", HTTP_RESPONSE_STATUS_CODE: 201, HTTP_ROUTE: "/hello", } if old_semconv: expected_attributes.update(expected_attributes_old) if new_semconv: expected_attributes.update(expected_attributes_new) self.assertSpanHasAttributes(span, expected_attributes) # In falcon<3, NET_PEER_IP is always set by default to 127.0.0.1 # In falcon>=3, NET_PEER_IP is not set to anything by default # https://github.com/falconry/falcon/blob/5233d0abed977d9dab78ebadf305f5abe2eef07c/falcon/testing/helpers.py#L1168-L1172 if NET_PEER_IP in span.attributes: self.assertEqual(span.attributes[NET_PEER_IP], "127.0.0.1") self.memory_exporter.clear() def test_404(self): self.client().simulate_get("/does-not-exist") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET") self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertSpanHasAttributes( span, { HTTP_METHOD: "GET", HTTP_SERVER_NAME: "falconframework.org", HTTP_SCHEME: "http", NET_HOST_PORT: 80, HTTP_HOST: "falconframework.org", HTTP_TARGET: "/" if self._has_fixed_http_target else "/does-not-exist", NET_PEER_PORT: 65133, HTTP_FLAVOR: "1.1", HTTP_STATUS_CODE: 404, }, ) # In falcon<3, NET_PEER_IP is always set by default to 127.0.0.1 # In falcon>=3, NET_PEER_IP is not set to anything by default # https://github.com/falconry/falcon/blob/5233d0abed977d9dab78ebadf305f5abe2eef07c/falcon/testing/helpers.py#L1168-L1172 if NET_PEER_IP in span.attributes: self.assertEqual(span.attributes[NET_PEER_IP], "127.0.0.1") def test_500(self): try: self.client().simulate_get("/error") except NameError: pass spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET /error") self.assertFalse(span.status.is_ok) self.assertEqual(span.status.status_code, StatusCode.ERROR) _parsed_falcon_version = package_version.parse(_falcon_version) if _parsed_falcon_version < package_version.parse("3.0.0"): self.assertEqual( span.status.description, "NameError: name 'non_existent_var' is not defined", ) else: self.assertEqual(span.status.description, None) self.assertSpanHasAttributes( span, { HTTP_METHOD: "GET", HTTP_SERVER_NAME: "falconframework.org", HTTP_SCHEME: "http", NET_HOST_PORT: 80, HTTP_HOST: "falconframework.org", HTTP_TARGET: "/" if self._has_fixed_http_target else "/error", NET_PEER_PORT: 65133, HTTP_FLAVOR: "1.1", HTTP_STATUS_CODE: 500, HTTP_ROUTE: "/error", }, ) # In falcon<3, NET_PEER_IP is always set by default to 127.0.0.1 # In falcon>=3, NET_PEER_IP is not set to anything by default # https://github.com/falconry/falcon/blob/5233d0abed977d9dab78ebadf305f5abe2eef07c/falcon/testing/helpers.py#L1168-L1172 if NET_PEER_IP in span.attributes: self.assertEqual(span.attributes[NET_PEER_IP], "127.0.0.1") def test_url_template_new_semconv(self): self.client().simulate_get("/user/123") spans = self.memory_exporter.get_finished_spans() metrics_list = self.memory_metrics_reader.get_metrics_data() self.assertEqual(len(spans), 1) self.assertTrue(len(metrics_list.resource_metrics) != 0) span = spans[0] self.assertEqual(span.name, "GET /user/{user_id}") self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual( span.status.description, None, ) self.assertSpanHasAttributes( span, { HTTP_REQUEST_METHOD: "GET", SERVER_ADDRESS: "falconframework.org", URL_SCHEME: "http", SERVER_PORT: 80, URL_PATH: "/user/123", CLIENT_PORT: 65133, NETWORK_PROTOCOL_VERSION: "1.1", "falcon.resource": "UserResource", HTTP_RESPONSE_STATUS_CODE: 200, HTTP_ROUTE: "/user/{user_id}", }, ) for resource_metric in metrics_list.resource_metrics: for scope_metric in resource_metric.scope_metrics: for metric in scope_metric.metrics: if metric.name == "http.server.request.duration": data_points = list(metric.data.data_points) for point in data_points: self.assertIn( "http.route", point.attributes, ) def test_url_template(self): self.client().simulate_get("/user/123") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET /user/{user_id}") self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual( span.status.description, None, ) self.assertSpanHasAttributes( span, { HTTP_METHOD: "GET", HTTP_SERVER_NAME: "falconframework.org", HTTP_SCHEME: "http", NET_HOST_PORT: 80, HTTP_HOST: "falconframework.org", HTTP_TARGET: "/" if self._has_fixed_http_target else "/user/123", NET_PEER_PORT: 65133, HTTP_FLAVOR: "1.1", "falcon.resource": "UserResource", HTTP_STATUS_CODE: 200, HTTP_ROUTE: "/user/{user_id}", }, ) def test_uninstrument(self): self.client().simulate_get(path="/hello") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.memory_exporter.clear() FalconInstrumentor().uninstrument() self.app = make_app() self.client().simulate_get(path="/hello") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_no_op_tracer_provider(self): FalconInstrumentor().uninstrument() FalconInstrumentor().instrument(tracer_provider=trace.NoOpTracerProvider()) self.memory_exporter.clear() self.client().simulate_get(path="/hello") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_exclude_lists(self): self.client().simulate_get(path="/ping") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) self.client().simulate_get(path="/hello") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) def test_traced_request_attributes(self): self.client().simulate_get(path="/hello", query_string="q=abc") span = self.memory_exporter.get_finished_spans()[0] self.assertIn("query_string", span.attributes) self.assertEqual(span.attributes["query_string"], "q=abc") self.assertNotIn("not_available_attr", span.attributes) def test_trace_response(self): orig = get_global_response_propagator() set_global_response_propagator(TraceResponsePropagator()) response = self.client().simulate_get(path="/hello", query_string="q=abc") self.assertTraceResponseHeaderMatchesSpan(response.headers, self.memory_exporter.get_finished_spans()[0]) set_global_response_propagator(orig) def test_traced_not_recording(self): mock_tracer = Mock() mock_span = Mock() mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span with patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer self.client().simulate_get(path="/hello", query_string="q=abc") self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) metrics = self.get_sorted_metrics(SCOPE) self.assertTrue(len(metrics) != 0) for metric in metrics: data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) if isinstance(point, NumberDataPoint): self.assertEqual(point.value, 0) for attr in point.attributes: self.assertIn( attr, _recommended_metrics_attrs_old[metric.name], ) def test_uninstrument_after_instrument(self): self.client().simulate_get(path="/hello") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) FalconInstrumentor().uninstrument() self.memory_exporter.clear() self.client().simulate_get(path="/hello") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_falcon_metrics(self): self.client().simulate_get("/hello/756") self.client().simulate_get("/hello/756") self.client().simulate_get("/hello/756") metrics = self.get_sorted_metrics(SCOPE) number_data_point_seen = False histogram_data_point_seen = False self.assertTrue(len(metrics) != 0) for metric in metrics: self.assertIn(metric.name, _expected_metric_names) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 3) histogram_data_point_seen = True if isinstance(point, NumberDataPoint): number_data_point_seen = True for attr in point.attributes: self.assertIn(attr, _recommended_attrs[metric.name]) self.assertTrue(number_data_point_seen and histogram_data_point_seen) def test_falcon_metric_values_new_semconv(self): number_data_point_seen = False histogram_data_point_seen = False start = default_timer() self.client().simulate_get("/hello/756") duration = max(default_timer() - start, 0) metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) histogram_data_point_seen = True self.assertAlmostEqual(duration, point.sum, delta=10) self.assertEqual( point.explicit_bounds, HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) if isinstance(point, NumberDataPoint): self.assertEqual(point.value, 0) number_data_point_seen = True for attr in point.attributes: self.assertIn( attr, _recommended_metrics_attrs_new[metric.name], ) self.assertTrue(number_data_point_seen and histogram_data_point_seen) def test_falcon_metric_values_both_semconv(self): number_data_point_seen = False histogram_data_point_seen = False start = default_timer() self.client().simulate_get("/hello/756") duration_s = default_timer() - start metrics = self.get_sorted_metrics(SCOPE) # pylint: disable=too-many-nested-blocks for metric in metrics: if metric.unit == "ms": self.assertEqual(metric.name, "http.server.duration") elif metric.unit == "s": self.assertEqual(metric.name, "http.server.request.duration") else: self.assertEqual(metric.name, "http.server.active_requests") data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) if metric.unit == "ms": self.assertAlmostEqual( max(round(duration_s * 1000), 0), point.sum, delta=10, ) elif metric.unit == "s": self.assertAlmostEqual(max(duration_s, 0), point.sum, delta=10) self.assertEqual( point.explicit_bounds, HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) histogram_data_point_seen = True if isinstance(point, NumberDataPoint): self.assertEqual(point.value, 0) number_data_point_seen = True for attr in point.attributes: self.assertIn( attr, _recommended_metrics_attrs_both[metric.name], ) self.assertTrue(number_data_point_seen and histogram_data_point_seen) def test_falcon_metric_values(self): number_data_point_seen = False histogram_data_point_seen = False start = default_timer() self.client().simulate_get("/hello/756") duration = max(round((default_timer() - start) * 1000), 0) metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) histogram_data_point_seen = True self.assertAlmostEqual(duration, point.sum, delta=10) if isinstance(point, NumberDataPoint): self.assertEqual(point.value, 0) number_data_point_seen = True for attr in point.attributes: self.assertIn( attr, _recommended_metrics_attrs_old[metric.name], ) self.assertTrue(number_data_point_seen and histogram_data_point_seen) def test_metric_uninstrument(self): self.client().simulate_request(method="POST", path="/hello/756") FalconInstrumentor().uninstrument() self.client().simulate_request(method="POST", path="/hello/756") metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) class TestFalconInstrumentationWithTracerProvider(TestBase): def setUp(self): super().setUp() resource = Resource.create({"resource-key": "resource-value"}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result self.exporter = exporter FalconInstrumentor().instrument(tracer_provider=tracer_provider) self.app = make_app() def client(self): return testing.TestClient(self.app) def tearDown(self): super().tearDown() with self.disable_logging(): FalconInstrumentor().uninstrument() def test_traced_request(self): self.client().simulate_request(method="GET", path="/hello") spans = self.exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.resource.attributes["resource-key"], "resource-value") self.exporter.clear() class TestFalconInstrumentationHooks(TestFalconBase): # pylint: disable=no-self-use def request_hook(self, span, req): span.set_attribute("request_hook_attr", "value from hook") def response_hook(self, span, req, resp): span.update_name("set from hook") def test_hooks(self): self.client().simulate_get(path="/hello", query_string="q=abc") span = self.memory_exporter.get_finished_spans()[0] self.assertEqual(span.name, "set from hook") self.assertIn("request_hook_attr", span.attributes) self.assertEqual(span.attributes["request_hook_attr"], "value from hook") class TestFalconInstrumentationWrappedWithOtherFramework(TestFalconBase): def test_mark_span_internal_in_presence_of_span_from_other_framework(self): tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER) as parent_span: self.client().simulate_request(method="GET", path="/hello") span = self.memory_exporter.get_finished_spans()[0] assert span.status.is_ok self.assertEqual(trace.SpanKind.INTERNAL, span.kind) self.assertEqual(span.parent.span_id, parent_span.get_span_context().span_id) @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,invalid-header,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", }, ) class TestCustomRequestResponseHeaders(TestFalconBase): def test_custom_request_header_added_in_server_span(self): headers = { "Custom-Test-Header-1": "Test Value 1", "Custom-Test-Header-2": "TestValue2,TestValue3", "Custom-Test-Header-3": "TestValue4", "Regex-Test-Header-1": "Regex Test Value 1", "regex-test-header-2": "RegexTestValue2,RegexTestValue3", "My-Secret-Header": "My Secret Value", } self.client().simulate_request(method="GET", path="/hello", headers=headers) span = self.memory_exporter.get_finished_spans()[0] assert span.status.is_ok expected = { "http.request.header.custom_test_header_1": ("Test Value 1",), "http.request.header.custom_test_header_2": ("TestValue2,TestValue3",), "http.request.header.regex_test_header_1": ("Regex Test Value 1",), "http.request.header.regex_test_header_2": ("RegexTestValue2,RegexTestValue3",), "http.request.header.my_secret_header": ("[REDACTED]",), } not_expected = { "http.request.header.custom_test_header_3": ("TestValue4",), } self.assertEqual(span.kind, trace.SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) def test_custom_request_header_not_added_in_internal_span(self): tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER): headers = { "Custom-Test-Header-1": "Test Value 1", "Custom-Test-Header-2": "TestValue2,TestValue3", "Regex-Test-Header-1": "Regex Test Value 1", "regex-test-header-2": "RegexTestValue2,RegexTestValue3", "My-Secret-Header": "My Secret Value", } self.client().simulate_request(method="GET", path="/hello", headers=headers) span = self.memory_exporter.get_finished_spans()[0] assert span.status.is_ok not_expected = { "http.request.header.custom_test_header_1": ("Test Value 1",), "http.request.header.custom_test_header_2": ("TestValue2,TestValue3",), "http.request.header.regex_test_header_1": ("Regex Test Value 1",), "http.request.header.regex_test_header_2": ("RegexTestValue2,RegexTestValue3",), "http.request.header.my_secret_header": ("[REDACTED]",), } self.assertEqual(span.kind, trace.SpanKind.INTERNAL) for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) @pytest.mark.skipif( condition=_parsed_falcon_version < package_version.parse("2.0.0"), reason="falcon<2 does not implement custom response headers", ) def test_custom_response_header_added_in_server_span(self): self.client().simulate_request(method="GET", path="/test_custom_response_headers") span = self.memory_exporter.get_finished_spans()[0] assert span.status.is_ok expected = { "http.response.header.content_type": ("text/plain; charset=utf-8",), "http.response.header.content_length": ("0",), "http.response.header.my_custom_header": ("my-custom-value-1,my-custom-header-2",), "http.response.header.my_custom_regex_header_1": ("my-custom-regex-value-1,my-custom-regex-value-2",), "http.response.header.my_custom_regex_header_2": ("my-custom-regex-value-3,my-custom-regex-value-4",), "http.response.header.my_secret_header": ("[REDACTED]",), } not_expected = {"http.response.header.dont_capture_me": ("test-value",)} self.assertEqual(span.kind, trace.SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) @pytest.mark.skipif( condition=_parsed_falcon_version < package_version.parse("2.0.0"), reason="falcon<2 does not implement custom response headers", ) def test_custom_response_header_not_added_in_internal_span(self): tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER): self.client().simulate_request(method="GET", path="/test_custom_response_headers") span = self.memory_exporter.get_finished_spans()[0] assert span.status.is_ok not_expected = { "http.response.header.content_type": ("text/plain; charset=utf-8",), "http.response.header.content_length": ("0",), "http.response.header.my_custom_header": ("my-custom-value-1,my-custom-header-2",), "http.response.header.my_custom_regex_header_1": ("my-custom-regex-value-1,my-custom-regex-value-2",), "http.response.header.my_custom_regex_header_2": ("my-custom-regex-value-3,my-custom-regex-value-4",), "http.response.header.my_secret_header": ("[REDACTED]",), } self.assertEqual(span.kind, trace.SpanKind.INTERNAL) for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi/000077500000000000000000000000001527070072300325725ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi/LICENSE000066400000000000000000000261351527070072300336060ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi/README.rst000066400000000000000000000014161527070072300342630ustar00rootroot00000000000000OpenTelemetry FastAPI Instrumentation ======================================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-fastapi.svg :target: https://pypi.org/project/opentelemetry-instrumentation-fastapi/ This library provides automatic and manual instrumentation of FastAPI web frameworks, instrumenting http requests served by applications utilizing the framework. auto-instrumentation using the opentelemetry-instrumentation package is also supported. Installation ------------ :: pip install opentelemetry-instrumentation-fastapi References ---------- * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000032651527070072300354350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-fastapi" dynamic = ["version"] description = "OpenTelemetry FastAPI Instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-instrumentation-asgi == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-util-http == 0.66b2", ] [project.optional-dependencies] instruments = [ "fastapi ~= 0.92", ] [project.entry-points.opentelemetry_instrumentor] fastapi = "opentelemetry.instrumentation.fastapi:FastAPIInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-fastapi" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/fastapi/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi/src/000077500000000000000000000000001527070072300333615ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300361765ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi/srcinstrumentation/000077500000000000000000000000001527070072300414415ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetryfastapi/000077500000000000000000000000001527070072300430705ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation__init__.py000066400000000000000000000647031527070072300452130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Usage ----- .. code-block:: python import fastapi from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor app = fastapi.FastAPI() @app.get("/foobar") async def foobar(): return {"message": "hello world"} FastAPIInstrumentor.instrument_app(app) Auto-instrumentation ******************** FastAPI can also be instrumented without code changes by using the ``opentelemetry-instrument`` command from the `opentelemetry-instrumentation `_ package. For installation, runtime command, exporter configuration, and environment variables, see `Python zero-code instrumentation `_. Install ``opentelemetry-instrumentation-fastapi`` in the same environment so the FastAPI integration is available to auto-instrumentation. FastAPI-specific options, such as excluded URLs and HTTP header capture, are documented below. Trace propagation ***************** FastAPI instrumentation uses the underlying ASGI instrumentation, which extracts incoming context from request headers using the configured global OpenTelemetry propagator. WebSocket connections are instrumented as ASGI ``websocket`` scopes, with context extracted from the handshake headers. Propagation data inside WebSocket message payloads is not parsed automatically. For more information, see `Python context propagation `_ and `Python distro configuration `_. Configuration ------------- Exclude lists ************* To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_FASTAPI_EXCLUDED_URLS`` (or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the URLs. For example, :: export OTEL_PYTHON_FASTAPI_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. You can also pass comma delimited regexes directly to the ``instrument_app`` method: .. code-block:: python FastAPIInstrumentor.instrument_app(app, excluded_urls="client/.*/info,healthcheck") Request/Response hooks ********************** This instrumentation supports request and response hooks. These are functions that get called right after a span is created for a request and right before the span is finished for the response. - The server request hook is passed a server span and ASGI scope object for every incoming request. - The client request hook is called with the internal span, and ASGI scope and event when the method ``receive`` is called. - The client response hook is called with the internal span, and ASGI scope and event when the method ``send`` is called. .. code-block:: python from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from opentelemetry.trace import Span from starlette.types import Message, Scope def server_request_hook(span: Span, scope: Scope): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_request_hook", "some-value") def client_request_hook(span: Span, scope: Scope, message: Message): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_client_request_hook", "some-value") def client_response_hook(span: Span, scope: Scope, message: Message): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_response_hook", "some-value") FastAPIInstrumentor().instrument( server_request_hook=server_request_hook, client_request_hook=client_request_hook, client_response_hook=client_response_hook, ) Capture HTTP request and response headers ***************************************** You can configure the agent to capture specified HTTP headers as span attributes, according to the `semantic conventions `_. Request headers *************** To capture HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names, or pass the ``http_capture_headers_server_request`` keyword argument to the ``instrument_app`` method. For example using the environment variable, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. Request header names in FastAPI are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" Would match all request headers that start with ``Accept`` and ``X-``. To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*" The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a single item list containing all the header values. For example: ``http.request.header.custom_request_header = ["", ""]`` Response headers **************** To capture HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names, or pass the ``http_capture_headers_server_response`` keyword argument to the ``instrument_app`` method. For example using the environment variable, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. Response header names in FastAPI are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" Would match all response headers that start with ``Content`` and ``X-``. To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*" The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a list containing the header values. For example: ``http.response.header.custom_response_header = ["", ""]`` Sanitizing headers ****************** In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` to a comma delimited list of HTTP header names to be sanitized, or pass the ``http_capture_headers_sanitize_fields`` keyword argument to the ``instrument_app`` method. Regexes may be used, and all header names will be matched in a case-insensitive manner. For example using the environment variable, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. Note: The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. API --- """ from __future__ import annotations import functools import logging import types from collections.abc import Collection, Iterator, Sequence from typing import TYPE_CHECKING, Literal from weakref import WeakSet as _WeakSet import fastapi from starlette.applications import Starlette from starlette.background import BackgroundTask from starlette.middleware.errors import ServerErrorMiddleware from starlette.routing import BaseRoute, Match, Route from starlette.types import ASGIApp, Receive, Scope, Send from typing_extensions import Unpack try: # FastAPI >= 0.137.2 exposes a public helper that flattens routes added via # include_router() (nested under _IncludedRouter in 0.137) into matchable # RouteContext objects. Older versions don't have it; see _flatten_routes. from fastapi.routing import iter_route_contexts except ImportError: iter_route_contexts = None from opentelemetry.instrumentation._semconv import ( _get_schema_url, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _StabilityMode, ) from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.instrumentation.asgi.types import ( ClientRequestHook, ClientResponseHook, ServerRequestHook, ) from opentelemetry.instrumentation.fastapi.package import _instruments from opentelemetry.instrumentation.fastapi.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.metrics import MeterProvider, get_meter from opentelemetry.semconv.attributes.http_attributes import HTTP_ROUTE from opentelemetry.trace import TracerProvider, get_current_span, get_tracer from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util.http import ( get_excluded_urls, parse_excluded_urls, sanitize_method, ) if TYPE_CHECKING: from typing import TypedDict from fastapi.routing import RouteContext, _EffectiveRouteContext class _InstrumentKwargs(TypedDict, total=False): server_request_hook: ServerRequestHook client_request_hook: ClientRequestHook client_response_hook: ClientResponseHook tracer_provider: TracerProvider | None meter_provider: MeterProvider | None excluded_urls: str | None http_capture_headers_server_request: list[str] | None http_capture_headers_server_response: list[str] | None http_capture_headers_sanitize_fields: list[str] | None exclude_spans: list[Literal["receive", "send"]] | None class _UninstrumentKwargs(TypedDict, total=False): pass _excluded_urls_from_env = get_excluded_urls("FASTAPI") _logger = logging.getLogger(__name__) class FastAPIInstrumentor(BaseInstrumentor): """An instrumentor for FastAPI See `BaseInstrumentor` """ _original_fastapi = None @staticmethod def instrument_app( # pylint: disable=too-many-locals app: fastapi.FastAPI, server_request_hook: ServerRequestHook = None, client_request_hook: ClientRequestHook = None, client_response_hook: ClientResponseHook = None, tracer_provider: TracerProvider | None = None, meter_provider: MeterProvider | None = None, excluded_urls: str | None = None, http_capture_headers_server_request: list[str] | None = None, http_capture_headers_server_response: list[str] | None = None, http_capture_headers_sanitize_fields: list[str] | None = None, exclude_spans: list[Literal["receive", "send"]] | None = None, ) -> None: """Instrument an uninstrumented FastAPI application. Args: app: The fastapi ASGI application callable to forward requests to. server_request_hook: Optional callback which is called with the server span and ASGI scope object for every incoming request. client_request_hook: Optional callback which is called with the internal span, and ASGI scope and event which are sent as dictionaries for when the method receive is called. client_response_hook: Optional callback which is called with the internal span, and ASGI scope and event which are sent as dictionaries for when the method send is called. tracer_provider: The optional tracer provider to use. If omitted the current globally configured one is used. meter_provider: The optional meter provider to use. If omitted the current globally configured one is used. excluded_urls: Optional comma delimited string of regexes to match URLs that should not be traced. http_capture_headers_server_request: Optional list of HTTP headers to capture from the request. http_capture_headers_server_response: Optional list of HTTP headers to capture from the response. http_capture_headers_sanitize_fields: Optional list of HTTP headers to sanitize. exclude_spans: Optionally exclude HTTP `send` and/or `receive` spans from the trace. """ if not hasattr(app, "_is_instrumented_by_opentelemetry"): app._is_instrumented_by_opentelemetry = False # pyright: ignore[reportAttributeAccessIssue] if not getattr(app, "_is_instrumented_by_opentelemetry", False): # initialize semantic conventions opt-in if needed _OpenTelemetrySemanticConventionStability._initialize() sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) parsed_excluded_urls = ( _excluded_urls_from_env if excluded_urls is None else parse_excluded_urls(excluded_urls) ) tracer = get_tracer( __name__, __version__, tracer_provider, schema_url=_get_schema_url(sem_conv_opt_in_mode), ) meter = get_meter( __name__, __version__, meter_provider, schema_url=_get_schema_url(sem_conv_opt_in_mode), ) original_build_middleware_stack = app.build_middleware_stack def build_middleware_stack(self: Starlette) -> ASGIApp: # Define an additional middleware for exception handling # Normally, `opentelemetry.trace.use_span` covers the recording of # exceptions into the active span, but `OpenTelemetryMiddleware` # ends the span too early before the exception can be recorded. class ExceptionHandlerMiddleware: def __init__(self, app: ASGIApp) -> None: self.app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: try: await self.app(scope, receive, send) except Exception as exc: # pylint: disable=broad-exception-caught span = get_current_span() if span.is_recording(): span.record_exception(exc) span.set_status( Status( status_code=StatusCode.ERROR, description=f"{type(exc).__name__}: {exc}", ) ) raise # For every possible use case of error handling, exception # handling, trace availability in exception handlers and # automatic exception recording to work, we need to make a # series of wrapping and re-wrapping middlewares. # First, grab the original middleware stack from Starlette. It # comprises a stack of # `ServerErrorMiddleware` -> [user defined middlewares] -> `ExceptionMiddleware` inner_server_error_middleware = original_build_middleware_stack() if not isinstance(inner_server_error_middleware, ServerErrorMiddleware): # Oops, something changed about how Starlette creates middleware stacks _logger.error( "Skipping FastAPI instrumentation due to unexpected middleware stack: expected %s, got %s", ServerErrorMiddleware.__name__, type(inner_server_error_middleware), ) return inner_server_error_middleware # We take [user defined middlewares] -> `ExceptionHandlerMiddleware` # out of the outermost `ServerErrorMiddleware` and instead pass # it to our own `ExceptionHandlerMiddleware` exception_middleware = ExceptionHandlerMiddleware(inner_server_error_middleware.app) # Now, we create a new `ServerErrorMiddleware` that wraps # `ExceptionHandlerMiddleware` but otherwise uses the same # original `handler` and debug setting. The end result is a # middleware stack that's identical to the original stack except # all user middlewares are covered by our # `ExceptionHandlerMiddleware`. error_middleware = ServerErrorMiddleware( app=exception_middleware, handler=inner_server_error_middleware.handler, debug=inner_server_error_middleware.debug, ) # Finally, we wrap the stack above in our actual OTEL # middleware. As a result, an active tracing context exists for # every use case of user-defined error and exception handlers as # well as automatic recording of exceptions in active spans. otel_middleware = OpenTelemetryMiddleware( error_middleware, excluded_urls=parsed_excluded_urls, default_span_details=_get_default_span_details, server_request_hook=server_request_hook, client_request_hook=client_request_hook, client_response_hook=client_response_hook, # Pass in tracer/meter to get __name__and __version__ of fastapi instrumentation tracer=tracer, meter=meter, http_capture_headers_server_request=http_capture_headers_server_request, http_capture_headers_server_response=http_capture_headers_server_response, http_capture_headers_sanitize_fields=http_capture_headers_sanitize_fields, exclude_spans=exclude_spans, ) # Ultimately, wrap everything in another default # `ServerErrorMiddleware` (w/o user handlers) so that any # exceptions raised in `OpenTelemetryMiddleware` are handled. # # This should not happen unless there is a bug in # OpenTelemetryMiddleware, but if there is we don't want that to # impact the user's application just because we wrapped the # middlewares in this order. return ServerErrorMiddleware( app=otel_middleware, ) app._original_build_middleware_stack = ( # pyright: ignore[reportAttributeAccessIssue] original_build_middleware_stack ) app.build_middleware_stack = types.MethodType( functools.wraps(app.build_middleware_stack)(build_middleware_stack), app, ) if not hasattr(BackgroundTask, "_otel_original_call"): original_background_task_call = BackgroundTask.__call__ BackgroundTask._otel_original_call = ( # pyright: ignore[reportAttributeAccessIssue] original_background_task_call ) async def traced_call(self: BackgroundTask) -> None: span_name = f"BackgroundTask {getattr(self.func, '__name__', self.func.__class__.__name__)}" with tracer.start_as_current_span(span_name): return await original_background_task_call(self) BackgroundTask.__call__ = traced_call app._is_instrumented_by_opentelemetry = True # pyright: ignore[reportAttributeAccessIssue] if app not in _InstrumentedFastAPI._instrumented_fastapi_apps: _InstrumentedFastAPI._instrumented_fastapi_apps.add(app) else: _logger.warning("Attempting to instrument FastAPI app while already instrumented") @staticmethod def uninstrument_app(app: fastapi.FastAPI) -> None: original_build_middleware_stack = getattr(app, "_original_build_middleware_stack", None) if original_build_middleware_stack: app.build_middleware_stack = original_build_middleware_stack del app._original_build_middleware_stack # pyright: ignore[reportAttributeAccessIssue] app.middleware_stack = app.build_middleware_stack() if hasattr(BackgroundTask, "_otel_original_call"): BackgroundTask.__call__ = BackgroundTask._otel_original_call # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType] del BackgroundTask._otel_original_call # pyright: ignore[reportAttributeAccessIssue] app._is_instrumented_by_opentelemetry = False # pyright: ignore[reportAttributeAccessIssue] # Remove the app from the set of instrumented apps to avoid calling uninstrument twice # if the instrumentation is later disabled or such # Use discard to avoid KeyError if already GC'ed _InstrumentedFastAPI._instrumented_fastapi_apps.discard(app) def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs: Unpack[_InstrumentKwargs]) -> None: self._original_fastapi = fastapi.FastAPI _InstrumentedFastAPI._instrument_kwargs = kwargs fastapi.FastAPI = _InstrumentedFastAPI def _uninstrument(self, **kwargs: Unpack[_UninstrumentKwargs]) -> None: # Create a copy of the set to avoid RuntimeError during iteration instances_to_uninstrument = list(_InstrumentedFastAPI._instrumented_fastapi_apps) for instance in instances_to_uninstrument: self.uninstrument_app(instance) _InstrumentedFastAPI._instrumented_fastapi_apps.clear() fastapi.FastAPI = self._original_fastapi class _InstrumentedFastAPI(fastapi.FastAPI): _instrument_kwargs: _InstrumentKwargs = {} # Track instrumented app instances using weak references to avoid GC leaks _instrumented_fastapi_apps: _WeakSet[fastapi.FastAPI] = _WeakSet() _sem_conv_opt_in_mode = _StabilityMode.DEFAULT def __init__(self, *args: object, **kwargs: object) -> None: super().__init__(*args, **kwargs) FastAPIInstrumentor.instrument_app(self, **_InstrumentedFastAPI._instrument_kwargs) _InstrumentedFastAPI._instrumented_fastapi_apps.add(self) def _flatten_routes( routes: Sequence[BaseRoute], ) -> Iterator[BaseRoute | RouteContext | _EffectiveRouteContext]: """ Yield the matchable routes from an app's route list. FastAPI 0.137 nests routes added via include_router() under _IncludedRouter tree nodes, which expose no ``path`` attribute. They have to be flattened into their effective route contexts (each providing matches() and the full templated path) before they can be matched against a scope. FastAPI >= 0.137.2 provides the public iter_route_contexts() for this, which also wraps plain routes uniformly. On older versions fall back to the private _IncludedRouter.effective_route_contexts(), and on FastAPI < 0.137 (no _IncludedRouter) the routes are already matchable as-is. """ if iter_route_contexts is not None: yield from iter_route_contexts(routes) return for starlette_route in routes: if hasattr(starlette_route, "effective_route_contexts"): yield from starlette_route.effective_route_contexts() # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType] else: yield starlette_route def _get_route_details(scope: Scope) -> str | None: """ Function to retrieve Starlette route from scope. TODO: there is currently no way to retrieve http.route from a starlette application from scope. See: https://github.com/encode/starlette/pull/804 Args: scope: A Starlette scope Returns: A string containing the route or None """ app: fastapi.FastAPI = scope["app"] route: str | None = None for starlette_route in _flatten_routes(app.routes): match, _ = ( Route.matches(starlette_route, scope) if isinstance(starlette_route, Route) else starlette_route.matches(scope) ) if match == Match.FULL: try: route = starlette_route.path # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType, reportUnknownVariableType] except AttributeError: # Routes added via host routing have no path attribute. route = scope.get("path") break if match == Match.PARTIAL: route = starlette_route.path # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType, reportUnknownVariableType] return route # pyright: ignore[reportUnknownVariableType] def _get_default_span_details(scope: Scope) -> tuple[str, dict[str, str]]: """ Callback to retrieve span name and attributes from scope. Args: scope: A Starlette scope Returns: A tuple of span name and attributes """ route = _get_route_details(scope) raw_method: str = scope.get("method", "") method = sanitize_method(raw_method.strip()) attributes: dict[str, str] = {} if method == "_OTHER": method = "HTTP" if route: attributes[HTTP_ROUTE] = route if method and route: # http span_name = f"{method} {route}" elif route: # websocket span_name = route else: # fallback span_name = method return span_name, attributes package.py000066400000000000000000000002531527070072300450350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("fastapi ~= 0.92",) _supports_metrics = True _semconv_status = "migration" py.typed000066400000000000000000000000001527070072300445550ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapiversion.py000066400000000000000000000001441527070072300451260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.in000066400000000000000000000005411527070072300365430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi# Used to generate test-requirements.{oldest,latest}.txt with uv pip compile pytest~=7.4 requests~=2.33.1 urllib3~=2.7.0 httpx~=0.28 fastapi~=0.119.1 wrapt>=1.16.0 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-asgi -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-fastapi[instruments] test-requirements.latest.txt000066400000000000000000000064111527070072300402510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi# This file was autogenerated by uv via the following command: # uv pip compile --python 3.10 --universal --no-emit-package opentelemetry-api --no-emit-package opentelemetry-semantic-conventions -c dev-requirements.txt instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in -o instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.latest.txt -e instrumentation/opentelemetry-instrumentation-asgi # via # -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in # opentelemetry-instrumentation-fastapi -e instrumentation/opentelemetry-instrumentation-fastapi # via -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in -e opentelemetry-instrumentation # via # -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in # opentelemetry-instrumentation-asgi # opentelemetry-instrumentation-fastapi -e util/opentelemetry-util-http # via # -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in # opentelemetry-instrumentation-asgi # opentelemetry-instrumentation-fastapi annotated-doc==0.0.4 # via fastapi annotated-types==0.7.0 # via pydantic anyio==4.14.0 # via # httpx # starlette asgiref==3.11.1 # via opentelemetry-instrumentation-asgi certifi==2026.6.17 # via # httpcore # httpx # requests charset-normalizer==3.4.7 # via requests colorama==0.4.6 ; sys_platform == 'win32' # via pytest exceptiongroup==1.3.1 ; python_full_version < '3.11' # via # anyio # pytest fastapi==0.137.2 # via opentelemetry-instrumentation-fastapi h11==0.16.0 # via httpcore httpcore==1.0.9 # via httpx httpx==0.28.1 # via -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in idna==3.18 # via # anyio # httpx # requests iniconfig==2.3.0 # via pytest packaging==26.2 # via # opentelemetry-instrumentation # pytest pluggy==1.6.0 # via pytest pydantic==2.12.5 # via fastapi pydantic-core==2.41.5 # via # -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in # pydantic pytest==7.4.4 # via # -c dev-requirements.txt # -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in requests==2.33.1 # via # -c dev-requirements.txt # -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in starlette==1.3.1 # via fastapi tomli==2.4.1 ; python_full_version < '3.11' # via pytest typing-extensions==4.15.0 # via # anyio # asgiref # exceptiongroup # fastapi # opentelemetry-api # opentelemetry-semantic-conventions # pydantic # pydantic-core # starlette # typing-inspection typing-inspection==0.4.2 # via # fastapi # pydantic urllib3==2.7.0 # via # -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in # requests wrapt==2.2.1 # via # -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in # opentelemetry-instrumentation # The following packages were excluded from the output: # opentelemetry-api # opentelemetry-semantic-conventions test-requirements.oldest.txt000066400000000000000000000065021527070072300402500ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi# This file was autogenerated by uv via the following command: # uv pip compile --python 3.10 --universal --resolution lowest-direct --no-emit-package opentelemetry-api --no-emit-package opentelemetry-semantic-conventions -c dev-requirements.txt instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in -o instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.oldest.txt -e instrumentation/opentelemetry-instrumentation-asgi # via # -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in # opentelemetry-instrumentation-fastapi -e instrumentation/opentelemetry-instrumentation-fastapi # via -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in -e opentelemetry-instrumentation # via # -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in # opentelemetry-instrumentation-asgi # opentelemetry-instrumentation-fastapi -e util/opentelemetry-util-http # via # -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in # opentelemetry-instrumentation-asgi # opentelemetry-instrumentation-fastapi annotated-types==0.7.0 # via pydantic anyio==4.14.0 # via # httpx # starlette asgiref==3.0.0 # via opentelemetry-instrumentation-asgi async-timeout==3.0.1 # via asgiref certifi==2026.6.17 # via # httpcore # httpx # requests charset-normalizer==3.4.7 # via requests colorama==0.4.6 ; sys_platform == 'win32' # via pytest exceptiongroup==1.3.1 ; python_full_version < '3.11' # via # anyio # pytest fastapi==0.119.1 # via opentelemetry-instrumentation-fastapi h11==0.16.0 # via httpcore httpcore==1.0.9 # via httpx httpx==0.28.0 # via -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in idna==3.18 # via # anyio # httpx # requests iniconfig==2.3.0 # via pytest packaging==18.0 # via # opentelemetry-instrumentation # pytest pluggy==1.6.0 # via pytest pydantic==2.12.5 # via fastapi pydantic-core==2.41.5 # via # -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in # pydantic pyparsing==3.3.2 # via packaging pytest==7.4.4 # via # -c dev-requirements.txt # -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in requests==2.33.1 # via # -c dev-requirements.txt # -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in six==1.17.0 # via packaging starlette==0.48.0 # via fastapi tomli==2.4.1 ; python_full_version < '3.11' # via pytest typing-extensions==4.15.0 # via # anyio # exceptiongroup # fastapi # opentelemetry-api # opentelemetry-semantic-conventions # pydantic # pydantic-core # starlette # typing-inspection typing-inspection==0.4.2 # via pydantic urllib3==2.7.0 # via # -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in # requests wrapt==1.16.0 # via # -r instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in # opentelemetry-instrumentation # The following packages were excluded from the output: # opentelemetry-api # opentelemetry-semantic-conventions opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi/tests/000077500000000000000000000000001527070072300337345ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300357540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi/teststest_fastapi_instrumentation.py000066400000000000000000002733221527070072300422510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines import gc as _gc import logging import unittest import weakref as _weakref from contextlib import ExitStack from timeit import default_timer from typing import Any, Final, cast from unittest.mock import Mock, call, patch import fastapi import pytest from fastapi.background import BackgroundTasks from fastapi.middleware.asyncexitstack import AsyncExitStackMiddleware from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware from fastapi.responses import JSONResponse, PlainTextResponse from fastapi.routing import APIRoute from fastapi.testclient import TestClient from starlette.background import BackgroundTask from starlette.routing import BaseRoute, Match from starlette.types import Receive, Scope, Send import opentelemetry.instrumentation.fastapi as otel_fastapi from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, _server_active_requests_count_attrs_new, _server_active_requests_count_attrs_old, _server_duration_attrs_new, _server_duration_attrs_old, ) from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.instrumentation.auto_instrumentation._load import ( _load_instrumentors, ) from opentelemetry.instrumentation.dependencies import DependencyConflict from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, NumberDataPoint, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_FLAVOR, HTTP_HOST, HTTP_METHOD, HTTP_SCHEME, HTTP_SERVER_NAME, HTTP_STATUS_CODE, HTTP_TARGET, HTTP_URL, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_HOST_PORT, ) from opentelemetry.semconv.attributes.exception_attributes import ( EXCEPTION_TYPE, ) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, HTTP_ROUTE, ) from opentelemetry.semconv.attributes.network_attributes import ( NETWORK_PROTOCOL_VERSION, ) from opentelemetry.semconv.attributes.url_attributes import URL_SCHEME from opentelemetry.test.globals_test import reset_trace_globals from opentelemetry.test.test_base import TestBase from opentelemetry.trace.status import StatusCode from opentelemetry.util._importlib_metadata import entry_points from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, get_excluded_urls, ) _expected_metric_names_old = [ "http.server.active_requests", "http.server.duration", "http.server.response.size", "http.server.request.size", ] _expected_metric_names_new = [ "http.server.active_requests", "http.server.request.duration", "http.server.response.body.size", "http.server.request.body.size", ] _expected_metric_names_both = _expected_metric_names_old _expected_metric_names_both.extend(_expected_metric_names_new) _recommended_attrs_old = { "http.server.active_requests": _server_active_requests_count_attrs_old, "http.server.duration": { *_server_duration_attrs_old, HTTP_TARGET, }, "http.server.response.size": { *_server_duration_attrs_old, HTTP_TARGET, }, "http.server.request.size": { *_server_duration_attrs_old, HTTP_TARGET, }, } _recommended_attrs_new = { "http.server.active_requests": _server_active_requests_count_attrs_new, "http.server.request.duration": _server_duration_attrs_new, "http.server.response.body.size": _server_duration_attrs_new, "http.server.request.body.size": _server_duration_attrs_new, } _recommended_attrs_both = _recommended_attrs_old.copy() _recommended_attrs_both.update(_recommended_attrs_new) _recommended_attrs_both["http.server.active_requests"].extend(_server_active_requests_count_attrs_old) class CustomMiddleware: def __init__(self, app: fastapi.FastAPI) -> None: self.app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: scope["nonstandard_field"] = "here" await self.app(scope, receive, send) class CustomRoute(APIRoute): def matches(self, scope: Scope) -> tuple[Match, Scope]: assert "nonstandard_field" in scope return super().matches(scope) SCOPE = "opentelemetry.instrumentation.fastapi" class TestBaseFastAPI(TestBase): def _create_app(self): app = self._create_fastapi_app() self._instrumentor.instrument_app( app=app, server_request_hook=getattr(self, "server_request_hook", None), client_request_hook=getattr(self, "client_request_hook", None), client_response_hook=getattr(self, "client_response_hook", None), ) return app def _create_app_explicit_excluded_urls(self): app = self._create_fastapi_app() to_exclude = "/user/123,/foobar" self._instrumentor.instrument_app( app, excluded_urls=to_exclude, server_request_hook=getattr(self, "server_request_hook", None), client_request_hook=getattr(self, "client_request_hook", None), client_response_hook=getattr(self, "client_response_hook", None), ) return app @classmethod def setUpClass(cls): if cls is TestBaseFastAPI: raise unittest.SkipTest(f"{cls.__name__} is an abstract base class") super().setUpClass() def setUp(self): super().setUp() test_name = "" if hasattr(self, "_testMethodName"): test_name = self._testMethodName sem_conv_mode = "default" if "new_semconv" in test_name: sem_conv_mode = "http" elif "both_semconv" in test_name: sem_conv_mode = "http/dup" self.env_patch = patch.dict( "os.environ", { "OTEL_PYTHON_FASTAPI_EXCLUDED_URLS": "/exclude/123,healthzz", OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) _OpenTelemetrySemanticConventionStability._initialized = False self.env_patch.start() self.exclude_patch = patch( "opentelemetry.instrumentation.fastapi._excluded_urls_from_env", get_excluded_urls("FASTAPI"), ) self.exclude_patch.start() self._instrumentor = otel_fastapi.FastAPIInstrumentor() self._app = self._create_app() self._app.add_middleware(HTTPSRedirectMiddleware) self._app.add_middleware(CustomMiddleware) self._client = TestClient(self._app, base_url="https://testserver:443") # run the lifespan, initialize the middleware stack # this is more in-line with what happens in a real application when the server starts up self._exit_stack = ExitStack() self._exit_stack.enter_context(self._client) def tearDown(self): self._exit_stack.close() super().tearDown() self.env_patch.stop() self.exclude_patch.stop() with self.disable_logging(): self._instrumentor.uninstrument() self._instrumentor.uninstrument_app(self._app) @staticmethod def _create_fastapi_app(): app = fastapi.FastAPI() sub_app = fastapi.FastAPI() custom_router = fastapi.APIRouter(route_class=CustomRoute) @sub_app.get("/home") async def _home(): return {"message": "sub hi"} @app.get("/foobar") async def _foobar(): return {"message": "hello world"} @app.get("/user/{username}") async def _user(username: str): return {"message": username} @app.get("/exclude/{param}") async def _exclude(param: str): return {"message": param} @app.get("/healthzz") async def _health(): return {"message": "ok"} @app.get("/error") async def _error(): raise UnhandledException("This is an unhandled exception") @custom_router.get("/success") async def _(): return None app.include_router(custom_router, prefix="/custom-router") app.mount("/sub", app=sub_app) app.host("testserver2", sub_app) return app class UnhandledException(Exception): pass class TestBaseManualFastAPI(TestBaseFastAPI): @classmethod def setUpClass(cls): if cls is TestBaseManualFastAPI: raise unittest.SkipTest(f"{cls.__name__} is an abstract base class") super().setUpClass() def test_fastapi_unhandled_exception(self): """If the application has an unhandled error the instrumentation should capture that a 500 response is returned.""" try: resp = self._client.get("/error") assert resp.status_code == 500, ( resp.content ) # pragma: no cover, for debugging this test if an exception is _not_ raised except UnhandledException: pass else: self.fail("Expected UnhandledException") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) span = spans[0] assert span.name == "GET /error http send" assert span.attributes[HTTP_STATUS_CODE] == 500 span = spans[2] assert span.name == "GET /error" assert span.attributes[HTTP_TARGET] == "/error" def test_sub_app_fastapi_call(self): """ This test is to ensure that a span in case of a sub app targeted contains the correct server url As this test case covers manual instrumentation, we won't see any additional spans for the sub app. In this case all generated spans might suffice the requirements for the attributes already (as the testcase is not setting a root_path for the outer app here) """ self._client.get("/sub/home") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) for span in spans: # As we are only looking to the "outer" app, we would see only the "GET /sub" spans self.assertIn("GET /sub", span.name) # We now want to specifically test all spans including the # - HTTP_TARGET # - HTTP_URL # attributes to be populated with the expected values spans_with_http_attributes = [ span for span in spans if (HTTP_URL in span.attributes or HTTP_TARGET in span.attributes) ] # We expect only one span to have the HTTP attributes set (the SERVER span from the app itself) # the sub app is not instrumented with manual instrumentation tests. self.assertEqual(1, len(spans_with_http_attributes)) for span in spans_with_http_attributes: self.assertEqual("/sub/home", span.attributes[HTTP_TARGET]) self.assertEqual( "https://testserver/sub/home", span.attributes[HTTP_URL], ) def test_custom_api_router(self): """ This test is to ensure that custom API routers the OpenTelemetryMiddleware does not cause issues with custom API routers that depend on non-standard fields on the ASGI scope. """ resp: Final = self._client.get("/custom-router/success") spans: Final = self.memory_exporter.get_finished_spans() spans_with_http_attributes = [ span for span in spans if (HTTP_URL in span.attributes or HTTP_TARGET in span.attributes) ] self.assertEqual(200, resp.status_code) for span in spans_with_http_attributes: self.assertEqual("/custom-router/success", span.attributes[HTTP_TARGET]) self.assertEqual( "https://testserver/custom-router/success", span.attributes[HTTP_URL], ) def test_included_router_route_details(self): """ Regression test for https://github.com/open-telemetry/opentelemetry-python-contrib/issues/4699 FastAPI 0.137 added intermediate ``_IncludedRouter`` objects to ``app.routes`` that do not expose a ``path`` attribute. Resolving the route details for a request to an ``include_router``-added route must not raise ``AttributeError`` and must still report the templated route. """ app = fastapi.FastAPI() router = fastapi.APIRouter() @router.get("/items/{item_id}") async def _read_item(item_id: str): return {"item_id": item_id} app.include_router(router, prefix="/api") self._instrumentor.instrument_app(app) try: client = TestClient(app) resp = client.get("/api/items/123") self.assertEqual(200, resp.status_code) spans = self.memory_exporter.get_finished_spans() self.assertTrue(spans) server_span = spans[-1] self.assertEqual("/api/items/{item_id}", server_span.attributes[HTTP_ROUTE]) self.assertIn("GET /api/items/{item_id}", server_span.name) finally: self._instrumentor.uninstrument_app(app) def test_custom_route_preserves_templated_path(self): class CustomPathRoute(BaseRoute): path = "/widgets/{widget_id}" def matches(self, scope: Scope) -> tuple[Match, Scope]: return Match.FULL, scope def url_path_for(self, name: str, /, **path_params: Any) -> Any: raise NotImplementedError() async def handle(self, scope: Scope, receive: Receive, send: Send) -> None: raise NotImplementedError() app = fastapi.FastAPI() app.router.routes = [CustomPathRoute()] scope = cast( Scope, {"type": "http", "app": app, "path": "/widgets/42"}, ) with patch.object(otel_fastapi, "iter_route_contexts", None): self.assertEqual("/widgets/{widget_id}", otel_fastapi._get_route_details(scope)) def test_host_fastapi_call(self): client = TestClient(self._app, base_url="https://testserver2:443") client.get("/") spans = self.memory_exporter.get_finished_spans() spans_with_http_attributes = [ span for span in spans if (HTTP_URL in span.attributes or HTTP_TARGET in span.attributes) ] self.assertEqual(1, len(spans_with_http_attributes)) for span in spans_with_http_attributes: self.assertEqual("/", span.attributes[HTTP_TARGET]) self.assertEqual( "https://testserver2/", span.attributes[HTTP_URL], ) class TestBaseAutoFastAPI(TestBaseFastAPI): @classmethod def setUpClass(cls): if cls is TestBaseAutoFastAPI: raise unittest.SkipTest(f"{cls.__name__} is an abstract base class") super().setUpClass() def test_sub_app_fastapi_call(self): """ This test is to ensure that a span in case of a sub app targeted contains the correct server url As this test case covers auto instrumentation, we will see additional spans for the sub app. In this case all generated spans might suffice the requirements for the attributes already (as the testcase is not setting a root_path for the outer app here) """ self._client.get("/sub/home") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 6) for span in spans: # As we are only looking to the "outer" app, we would see only the "GET /sub" spans # -> the outer app is not aware of the sub_apps internal routes sub_in = "GET /sub" in span.name # The sub app spans are named GET /home as from the sub app perspective the request targets /home # -> the sub app is technically not aware of the /sub prefix home_in = "GET /home" in span.name # We expect the spans to be either from the outer app or the sub app self.assertTrue( sub_in or home_in, f"Span {span.name} does not have /sub or /home in its name", ) # We now want to specifically test all spans including the # - HTTP_TARGET # - HTTP_URL # attributes to be populated with the expected values spans_with_http_attributes = [ span for span in spans if (HTTP_URL in span.attributes or HTTP_TARGET in span.attributes) ] # We now expect spans with attributes from both the app and its sub app self.assertEqual(2, len(spans_with_http_attributes)) for span in spans_with_http_attributes: self.assertEqual("/sub/home", span.attributes[HTTP_TARGET]) self.assertEqual( "https://testserver/sub/home", span.attributes[HTTP_URL], ) # pylint: disable=too-many-public-methods class TestFastAPIManualInstrumentation(TestBaseManualFastAPI): def test_instrument_app_with_instrument(self): if not isinstance(self, TestAutoInstrumentation): self._instrumentor.instrument() self._client.get("/foobar") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) for span in spans: self.assertIn("GET /foobar", span.name) self.assertEqual( span.instrumentation_scope.name, SCOPE, ) def test_uninstrument_app(self): self._client.get("/foobar") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) self._instrumentor.uninstrument_app(self._app) self.assertFalse(isinstance(self._app.user_middleware[0].cls, OpenTelemetryMiddleware)) self._client = TestClient(self._app) resp = self._client.get("/foobar") self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) def test_uninstrument_app_after_instrument(self): if not isinstance(self, TestAutoInstrumentation): self._instrumentor.instrument() self._instrumentor.uninstrument_app(self._app) self._client.get("/foobar") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_basic_fastapi_call(self): self._client.get("/foobar") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) for span in spans: self.assertIn("GET /foobar", span.name) def test_background_task_span_parents_inner_spans(self): """Regression test for #4251: spans created inside a FastAPI BackgroundTask must be children of a dedicated background-task span instead of the already-closed request span.""" self.memory_exporter.clear() app = fastapi.FastAPI() self._instrumentor.instrument_app(app) tracer = self.tracer_provider.get_tracer(__name__) async def background_notify(): with tracer.start_as_current_span("inside-background-task"): pass @app.post("/checkout") async def checkout(background_tasks: BackgroundTasks): background_tasks.add_task(background_notify) return {"status": "processing"} with TestClient(app) as client: response = client.post("/checkout") self.assertEqual(200, response.status_code) spans = self.memory_exporter.get_finished_spans() request_span = next(span for span in spans if span.name == "POST /checkout") background_span = next(span for span in spans if span.name == "BackgroundTask background_notify") inner_span = next(span for span in spans if span.name == "inside-background-task") self.assertIsNotNone(background_span.parent) self.assertEqual( background_span.parent.span_id, request_span.context.span_id, ) self.assertIsNotNone(inner_span.parent) self.assertEqual( inner_span.parent.span_id, background_span.context.span_id, ) otel_fastapi.FastAPIInstrumentor().uninstrument_app(app) def test_fastapi_route_attribute_added(self): """Ensure that fastapi routes are used as the span name.""" self._client.get("/user/123") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) for span in spans: self.assertIn("GET /user/{username}", span.name) self.assertEqual(spans[-1].attributes[HTTP_ROUTE], "/user/{username}") # ensure that at least one attribute that is populated by # the asgi instrumentation is successfully feeding though. self.assertEqual(spans[-1].attributes[HTTP_FLAVOR], "1.1") def test_fastapi_excluded_urls(self): """Ensure that given fastapi routes are excluded.""" self._client.get("/exclude/123") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) self._client.get("/healthzz") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_fastapi_excluded_urls_not_env(self): """Ensure that given fastapi routes are excluded when passed explicitly (not in the environment)""" app = self._create_app_explicit_excluded_urls() client = TestClient(app) client.get("/user/123") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) client.get("/foobar") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_fastapi_metrics(self): self._client.get("/foobar") self._client.get("/foobar") self._client.get("/foobar") number_data_point_seen = False histogram_data_point_seen = False metrics = self.get_sorted_metrics(SCOPE) self.assertTrue(len(metrics) == 3) for metric in metrics: self.assertIn(metric.name, _expected_metric_names_old) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 3) histogram_data_point_seen = True if isinstance(point, NumberDataPoint): number_data_point_seen = True for attr in point.attributes: self.assertIn(attr, _recommended_attrs_old[metric.name]) self.assertTrue(number_data_point_seen and histogram_data_point_seen) def test_fastapi_metrics_new_semconv(self): self._client.get("/foobar") self._client.get("/foobar") self._client.get("/foobar") number_data_point_seen = False histogram_data_point_seen = False metrics = self.get_sorted_metrics(SCOPE) self.assertTrue(len(metrics) == 3) for metric in metrics: self.assertIn(metric.name, _expected_metric_names_new) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 3) histogram_data_point_seen = True if isinstance(point, NumberDataPoint): number_data_point_seen = True for attr in point.attributes: self.assertIn(attr, _recommended_attrs_new[metric.name]) self.assertTrue(number_data_point_seen and histogram_data_point_seen) def test_fastapi_metrics_both_semconv(self): self._client.get("/foobar") self._client.get("/foobar") self._client.get("/foobar") number_data_point_seen = False histogram_data_point_seen = False metrics = self.get_sorted_metrics(SCOPE) self.assertTrue(len(metrics) == 5) for metric in metrics: self.assertIn(metric.name, _expected_metric_names_both) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 3) histogram_data_point_seen = True if isinstance(point, NumberDataPoint): number_data_point_seen = True for attr in point.attributes: self.assertIn(attr, _recommended_attrs_both[metric.name]) self.assertTrue(number_data_point_seen and histogram_data_point_seen) def test_basic_metric_success(self): start = default_timer() self._client.get("/foobar") duration = max(round((default_timer() - start) * 1000), 0) expected_duration_attributes = { HTTP_METHOD: "GET", HTTP_HOST: "testserver:443", HTTP_SCHEME: "https", HTTP_FLAVOR: "1.1", HTTP_SERVER_NAME: "testserver", NET_HOST_PORT: 443, HTTP_STATUS_CODE: 200, HTTP_TARGET: "/foobar", } expected_requests_count_attributes = { HTTP_METHOD: "GET", HTTP_HOST: "testserver:443", HTTP_SCHEME: "https", HTTP_FLAVOR: "1.1", HTTP_SERVER_NAME: "testserver", } metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertDictEqual( expected_duration_attributes, dict(point.attributes), ) self.assertEqual(point.count, 1) self.assertAlmostEqual(duration, point.sum, delta=350) if isinstance(point, NumberDataPoint): self.assertDictEqual( expected_requests_count_attributes, dict(point.attributes), ) self.assertEqual(point.value, 0) def test_basic_metric_success_new_semconv(self): start = default_timer() self._client.get("/foobar") duration_s = max(default_timer() - start, 0) expected_duration_attributes = { HTTP_REQUEST_METHOD: "GET", URL_SCHEME: "https", NETWORK_PROTOCOL_VERSION: "1.1", HTTP_RESPONSE_STATUS_CODE: 200, HTTP_ROUTE: "/foobar", } expected_requests_count_attributes = { HTTP_REQUEST_METHOD: "GET", URL_SCHEME: "https", } metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertDictEqual( expected_duration_attributes, dict(point.attributes), ) self.assertEqual(point.count, 1) if metric.name == "http.server.request.duration": self.assertAlmostEqual(duration_s * 0.1, point.sum, places=1) elif metric.name in ( "http.server.response.body.size", "http.server.request.body.size", ): self.assertEqual(25, point.sum) if isinstance(point, NumberDataPoint): self.assertDictEqual( expected_requests_count_attributes, dict(point.attributes), ) self.assertEqual(point.value, 0) def test_basic_metric_success_both_semconv(self): start = default_timer() self._client.get("/foobar") duration = max(round((default_timer() - start) * 1000), 0) duration_s = max(default_timer() - start, 0) expected_duration_attributes_old = { HTTP_METHOD: "GET", HTTP_HOST: "testserver:443", HTTP_SCHEME: "https", HTTP_FLAVOR: "1.1", HTTP_SERVER_NAME: "testserver", NET_HOST_PORT: 443, HTTP_STATUS_CODE: 200, HTTP_TARGET: "/foobar", } expected_duration_attributes_new = { HTTP_REQUEST_METHOD: "GET", URL_SCHEME: "https", NETWORK_PROTOCOL_VERSION: "1.1", HTTP_RESPONSE_STATUS_CODE: 200, HTTP_ROUTE: "/foobar", } expected_requests_count_attributes = { HTTP_METHOD: "GET", HTTP_HOST: "testserver:443", HTTP_SCHEME: "https", HTTP_FLAVOR: "1.1", HTTP_SERVER_NAME: "testserver", HTTP_REQUEST_METHOD: "GET", URL_SCHEME: "https", } metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) self.assertAlmostEqual(duration, point.sum, delta=350) if metric.name == "http.server.request.duration": self.assertAlmostEqual(duration_s * 0.1, point.sum, places=1) self.assertDictEqual( expected_duration_attributes_new, dict(point.attributes), ) elif metric.name in ( "http.server.response.body.size", "http.server.request.body.size", ): self.assertEqual(25, point.sum) self.assertDictEqual( expected_duration_attributes_new, dict(point.attributes), ) elif metric.name == "http.server.duration": self.assertAlmostEqual(duration, point.sum, delta=350) self.assertDictEqual( expected_duration_attributes_old, dict(point.attributes), ) elif metric.name in ( "http.server.response.size", "http.server.request.size", ): self.assertEqual(25, point.sum) self.assertDictEqual( expected_duration_attributes_old, dict(point.attributes), ) if isinstance(point, NumberDataPoint): self.assertDictEqual( expected_requests_count_attributes, dict(point.attributes), ) self.assertEqual(point.value, 0) def test_basic_metric_nonstandard_http_method_success(self): start = default_timer() self._client.request("NONSTANDARD", "/foobar") duration = max(round((default_timer() - start) * 1000), 0) expected_duration_attributes = { HTTP_METHOD: "_OTHER", HTTP_HOST: "testserver:443", HTTP_SCHEME: "https", HTTP_FLAVOR: "1.1", HTTP_SERVER_NAME: "testserver", NET_HOST_PORT: 443, HTTP_STATUS_CODE: 405, HTTP_TARGET: "/foobar", } expected_requests_count_attributes = { HTTP_METHOD: "_OTHER", HTTP_HOST: "testserver:443", HTTP_SCHEME: "https", HTTP_FLAVOR: "1.1", HTTP_SERVER_NAME: "testserver", } metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertDictEqual( expected_duration_attributes, dict(point.attributes), ) self.assertEqual(point.count, 1) self.assertAlmostEqual(duration, point.sum, delta=350) if isinstance(point, NumberDataPoint): self.assertDictEqual( expected_requests_count_attributes, dict(point.attributes), ) self.assertEqual(point.value, 0) def test_basic_metric_nonstandard_http_method_success_new_semconv(self): start = default_timer() self._client.request("NONSTANDARD", "/foobar") duration_s = max(default_timer() - start, 0) expected_duration_attributes = { HTTP_REQUEST_METHOD: "_OTHER", URL_SCHEME: "https", NETWORK_PROTOCOL_VERSION: "1.1", HTTP_RESPONSE_STATUS_CODE: 405, HTTP_ROUTE: "/foobar", } expected_requests_count_attributes = { HTTP_REQUEST_METHOD: "_OTHER", URL_SCHEME: "https", } metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertDictEqual( expected_duration_attributes, dict(point.attributes), ) self.assertEqual(point.count, 1) if metric.name == "http.server.request.duration": self.assertAlmostEqual(duration_s * 0.1, point.sum, places=1) elif metric.name == "http.server.response.body.size": self.assertEqual(31, point.sum) elif metric.name == "http.server.request.body.size": self.assertEqual(25, point.sum) if isinstance(point, NumberDataPoint): self.assertDictEqual( expected_requests_count_attributes, dict(point.attributes), ) self.assertEqual(point.value, 0) def test_basic_metric_nonstandard_http_method_success_both_semconv(self): start = default_timer() self._client.request("NONSTANDARD", "/foobar") duration = max(round((default_timer() - start) * 1000), 0) duration_s = max(default_timer() - start, 0) expected_duration_attributes_old = { HTTP_METHOD: "_OTHER", HTTP_HOST: "testserver:443", HTTP_SCHEME: "https", HTTP_FLAVOR: "1.1", HTTP_SERVER_NAME: "testserver", NET_HOST_PORT: 443, HTTP_STATUS_CODE: 405, HTTP_TARGET: "/foobar", } expected_duration_attributes_new = { HTTP_REQUEST_METHOD: "_OTHER", URL_SCHEME: "https", NETWORK_PROTOCOL_VERSION: "1.1", HTTP_RESPONSE_STATUS_CODE: 405, HTTP_ROUTE: "/foobar", } expected_requests_count_attributes = { HTTP_METHOD: "_OTHER", HTTP_HOST: "testserver:443", HTTP_SCHEME: "https", HTTP_FLAVOR: "1.1", HTTP_SERVER_NAME: "testserver", HTTP_REQUEST_METHOD: "_OTHER", URL_SCHEME: "https", } metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) if metric.name == "http.server.request.duration": self.assertAlmostEqual(duration_s * 0.1, point.sum, places=1) self.assertDictEqual( expected_duration_attributes_new, dict(point.attributes), ) elif metric.name == "http.server.response.body.size": self.assertEqual(31, point.sum) self.assertDictEqual( expected_duration_attributes_new, dict(point.attributes), ) elif metric.name == "http.server.request.body.size": self.assertEqual(25, point.sum) self.assertDictEqual( expected_duration_attributes_new, dict(point.attributes), ) elif metric.name == "http.server.duration": self.assertAlmostEqual(duration, point.sum, delta=350) self.assertDictEqual( expected_duration_attributes_old, dict(point.attributes), ) elif metric.name == "http.server.response.size": self.assertEqual(31, point.sum) self.assertDictEqual( expected_duration_attributes_old, dict(point.attributes), ) elif metric.name == "http.server.request.size": self.assertEqual(25, point.sum) self.assertDictEqual( expected_duration_attributes_old, dict(point.attributes), ) if isinstance(point, NumberDataPoint): self.assertDictEqual( expected_requests_count_attributes, dict(point.attributes), ) self.assertEqual(point.value, 0) def test_basic_post_request_metric_success(self): start = default_timer() response = self._client.post( "/foobar", json={"foo": "bar"}, ) duration = max(round((default_timer() - start) * 1000), 0) response_size = int(response.headers.get("content-length")) request_size = int(response.request.headers.get("content-length")) metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) if metric.name == "http.server.duration": self.assertAlmostEqual(duration, point.sum, delta=350) elif metric.name == "http.server.response.size": self.assertEqual(response_size, point.sum) elif metric.name == "http.server.request.size": self.assertEqual(request_size, point.sum) if isinstance(point, NumberDataPoint): self.assertEqual(point.value, 0) def test_basic_post_request_metric_success_new_semconv(self): start = default_timer() response = self._client.post( "/foobar", json={"foo": "bar"}, ) duration_s = max(default_timer() - start, 0) response_size = int(response.headers.get("content-length")) request_size = int(response.request.headers.get("content-length")) metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) if metric.name == "http.server.request.duration": self.assertAlmostEqual(duration_s * 0.1, point.sum, places=1) elif metric.name == "http.server.response.body.size": self.assertEqual(response_size, point.sum) elif metric.name == "http.server.request.body.size": self.assertEqual(request_size, point.sum) if isinstance(point, NumberDataPoint): self.assertEqual(point.value, 0) def test_basic_post_request_metric_success_both_semconv(self): start = default_timer() response = self._client.post( "/foobar", json={"foo": "bar"}, ) duration = max(round((default_timer() - start) * 1000), 0) duration_s = max(default_timer() - start, 0) response_size = int(response.headers.get("content-length")) request_size = int(response.request.headers.get("content-length")) metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) if metric.name == "http.server.request.duration": self.assertAlmostEqual(duration_s * 0.1, point.sum, places=1) elif metric.name == "http.server.response.body.size": self.assertEqual(response_size, point.sum) elif metric.name == "http.server.request.body.size": self.assertEqual(request_size, point.sum) elif metric.name == "http.server.duration": self.assertAlmostEqual(duration, point.sum, delta=350) elif metric.name == "http.server.response.size": self.assertEqual(response_size, point.sum) elif metric.name == "http.server.request.size": self.assertEqual(request_size, point.sum) if isinstance(point, NumberDataPoint): self.assertEqual(point.value, 0) def test_uninstrument_app_restores_background_task_call(self): """Regression test for #4251: uninstrumentation must restore the original BackgroundTask.__call__ after FastAPI patches it.""" self.assertTrue(hasattr(BackgroundTask, "_otel_original_call")) self._instrumentor.uninstrument_app(self._app) self.assertFalse(hasattr(BackgroundTask, "_otel_original_call")) def test_background_task_span_not_duplicated_on_double_instrument_app( self, ): """Regression test for #4251: repeated instrument_app calls must not wrap BackgroundTask.__call__ multiple times or duplicate spans.""" self.memory_exporter.clear() app = fastapi.FastAPI() self._instrumentor.instrument_app(app) self._instrumentor.instrument_app(app) tracer = self.tracer_provider.get_tracer(__name__) async def background_notify(): with tracer.start_as_current_span("inside-background-task"): pass @app.post("/checkout") async def checkout(background_tasks: BackgroundTasks): background_tasks.add_task(background_notify) return {"status": "processing"} with TestClient(app) as client: response = client.post("/checkout") self.assertEqual(200, response.status_code) spans = self.memory_exporter.get_finished_spans() background_spans = [span for span in spans if span.name == "BackgroundTask background_notify"] inner_spans = [span for span in spans if span.name == "inside-background-task"] self.assertEqual(len(background_spans), 1) self.assertEqual(len(inner_spans), 1) otel_fastapi.FastAPIInstrumentor().uninstrument_app(app) def test_metric_uninstrument_app(self): self._client.get("/foobar") self._instrumentor.uninstrument_app(self._app) self._client.get("/foobar") metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) if isinstance(point, NumberDataPoint): self.assertEqual(point.value, 0) def test_metric_uninstrument(self): if not isinstance(self, TestAutoInstrumentation): self._instrumentor.instrument() self._client.get("/foobar") self._instrumentor.uninstrument() self._client.get("/foobar") metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) if isinstance(point, NumberDataPoint): self.assertEqual(point.value, 0) @staticmethod def _create_fastapi_app(): app = fastapi.FastAPI() sub_app = fastapi.FastAPI() custom_router = fastapi.APIRouter(route_class=CustomRoute) @sub_app.get("/home") async def _home(): return {"message": "sub hi"} @app.get("/foobar") async def _foobar(): return {"message": "hello world"} @app.get("/user/{username}") async def _user(username: str): return {"message": username} @app.get("/exclude/{param}") async def _exclude(param: str): return {"message": param} @app.get("/healthzz") async def _health(): return {"message": "ok"} @app.get("/error") async def _error(): raise UnhandledException("This is an unhandled exception") @custom_router.get("/success") async def _(): return None app.include_router(custom_router, prefix="/custom-router") app.mount("/sub", app=sub_app) return app class TestFastAPIManualInstrumentationHooks(TestBaseFastAPI): def _create_app(self): def server_request_hook(span: trace.Span, scope: dict[str, Any]): span.update_name("name from server hook") def client_request_hook( receive_span: trace.Span, scope: dict[str, Any], message: dict[str, Any], ): receive_span.update_name("name from client hook") receive_span.set_attribute("attr-from-request-hook", "set") def client_response_hook( send_span: trace.Span, scope: dict[str, Any], message: dict[str, Any], ): send_span.update_name("name from response hook") send_span.set_attribute("attr-from-response-hook", "value") self._instrumentor.instrument( server_request_hook=server_request_hook, client_request_hook=client_request_hook, client_response_hook=client_response_hook, ) app = self._create_fastapi_app() return app def test_hooks(self): self._client.get("/foobar") spans = cast( list[ReadableSpan], self.sorted_spans(self.memory_exporter.get_finished_spans()), ) self.assertEqual(len(spans), 3) server_span = spans[2] self.assertEqual(server_span.name, "name from server hook") response_spans = spans[:2] for span in response_spans: self.assertEqual(span.name, "name from response hook") self.assertSpanHasAttributes(span, {"attr-from-response-hook": "value"}) class TestAutoInstrumentation(TestBaseAutoFastAPI): """Test the auto-instrumented variant Extending the manual instrumentation as most test cases apply to both. """ def test_entry_point_exists(self): (ep,) = entry_points(group="opentelemetry_instrumentor") self.assertEqual(ep.name, "fastapi") @staticmethod def _instrumentation_loaded_successfully_call(): return call("Instrumented %s", "fastapi") @staticmethod def _instrumentation_failed_to_load_call(dependency_conflict): return call(dependency_conflict._format_message("fastapi")) @patch("opentelemetry.instrumentation.auto_instrumentation._load._logger") def test_instruments_with_fastapi_installed(self, mock_logger): mock_distro = Mock() mock_distro.load_instrumentor.return_value = None _load_instrumentors(mock_distro) self.assertEqual(len(mock_distro.load_instrumentor.call_args_list), 1) (ep,) = mock_distro.load_instrumentor.call_args.args self.assertEqual(ep.name, "fastapi") mock_logger.debug.assert_has_calls([self._instrumentation_loaded_successfully_call()]) @patch("opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts") @patch("opentelemetry.instrumentation.auto_instrumentation._load._logger") def test_instruments_with_old_fastapi_installed(self, mock_logger, mock_dep): # pylint: disable=no-self-use dependency_conflict = DependencyConflict("0.58", "0.57") mock_distro = Mock() mock_dep.return_value = dependency_conflict _load_instrumentors(mock_distro) mock_distro.load_instrumentor.assert_not_called() mock_logger.error.assert_has_calls([self._instrumentation_failed_to_load_call(dependency_conflict)]) @patch("opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts") @patch("opentelemetry.instrumentation.auto_instrumentation._load._logger") def test_instruments_without_fastapi_installed(self, mock_logger, mock_dep): # pylint: disable=no-self-use dependency_conflict = DependencyConflict("0.58", None) mock_distro = Mock() mock_dep.return_value = dependency_conflict _load_instrumentors(mock_distro) mock_distro.load_instrumentor.assert_not_called() mock_logger.debug.assert_has_calls([self._instrumentation_failed_to_load_call(dependency_conflict)]) def _create_app(self): # instrumentation is handled by the instrument call resource = Resource.create({"key1": "value1", "key2": "value2"}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result self.memory_exporter = exporter self._instrumentor.instrument(tracer_provider=tracer_provider) return self._create_fastapi_app() def _create_app_explicit_excluded_urls(self): resource = Resource.create({"key1": "value1", "key2": "value2"}) tracer_provider, exporter = self.create_tracer_provider(resource=resource) self.memory_exporter = exporter to_exclude = "/user/123,/foobar" self._instrumentor.uninstrument() # Disable previous instrumentation (setUp) self._instrumentor.instrument( tracer_provider=tracer_provider, excluded_urls=to_exclude, ) return self._create_fastapi_app() def test_request(self): self._client.get("/foobar") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) for span in spans: self.assertEqual(span.resource.attributes["key1"], "value1") self.assertEqual(span.resource.attributes["key2"], "value2") def test_mulitple_way_instrumentation(self): self._instrumentor.instrument_app(self._app) count = 0 app = self._app.middleware_stack while app is not None: if isinstance(app, OpenTelemetryMiddleware): count += 1 app = getattr(app, "app", None) self.assertEqual(count, 1) def test_uninstrument_after_instrument(self): app = self._create_fastapi_app() client = TestClient(app) client.get("/foobar") self._instrumentor.uninstrument() client.get("/foobar") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) def test_no_op_tracer_provider(self): self._instrumentor.uninstrument() self._instrumentor.instrument(tracer_provider=trace.NoOpTracerProvider()) app = self._create_fastapi_app() client = TestClient(app) client.get("/foobar") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def tearDown(self): self._instrumentor.uninstrument() super().tearDown() def test_sub_app_fastapi_call(self): """ !!! Attention: we need to override this testcase for the auto-instrumented variant The reason is, that with auto instrumentation, the sub app is instrumented as well and therefore we would see the spans for the sub app as well This test is to ensure that a span in case of a sub app targeted contains the correct server url """ self._client.get("/sub/home") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 6) for span in spans: # As we are only looking to the "outer" app, we would see only the "GET /sub" spans # -> the outer app is not aware of the sub_apps internal routes sub_in = "GET /sub" in span.name # The sub app spans are named GET /home as from the sub app perspective the request targets /home # -> the sub app is technically not aware of the /sub prefix home_in = "GET /home" in span.name # We expect the spans to be either from the outer app or the sub app self.assertTrue( sub_in or home_in, f"Span {span.name} does not have /sub or /home in its name", ) # We now want to specifically test all spans including the # - HTTP_TARGET # - HTTP_URL # attributes to be populated with the expected values spans_with_http_attributes = [ span for span in spans if (HTTP_URL in span.attributes or HTTP_TARGET in span.attributes) ] # We now expect spans with attributes from both the app and its sub app self.assertEqual(2, len(spans_with_http_attributes)) for span in spans_with_http_attributes: self.assertEqual("/sub/home", span.attributes[HTTP_TARGET]) self.assertEqual( "https://testserver/sub/home", span.attributes[HTTP_URL], ) class TestAutoInstrumentationHooks(TestBaseAutoFastAPI): """ Test the auto-instrumented variant for request and response hooks Extending the manual instrumentation to inherit defined hooks and since most test cases apply to both. """ def _create_app(self): # instrumentation is handled by the instrument call self._instrumentor.instrument( server_request_hook=getattr(self, "server_request_hook", None), client_request_hook=getattr(self, "client_request_hook", None), client_response_hook=getattr(self, "client_response_hook", None), ) return self._create_fastapi_app() def _create_app_explicit_excluded_urls(self): resource = Resource.create({"key1": "value1", "key2": "value2"}) tracer_provider, exporter = self.create_tracer_provider(resource=resource) self.memory_exporter = exporter to_exclude = "/user/123,/foobar" self._instrumentor.uninstrument() # Disable previous instrumentation (setUp) self._instrumentor.instrument( tracer_provider=tracer_provider, excluded_urls=to_exclude, server_request_hook=getattr(self, "server_request_hook", None), client_request_hook=getattr(self, "client_request_hook", None), client_response_hook=getattr(self, "client_response_hook", None), ) return self._create_fastapi_app() def tearDown(self): self._instrumentor.uninstrument() super().tearDown() def test_sub_app_fastapi_call(self): """ !!! Attention: we need to override this testcase for the auto-instrumented variant The reason is, that with auto instrumentation, the sub app is instrumented as well and therefore we would see the spans for the sub app as well This test is to ensure that a span in case of a sub app targeted contains the correct server url """ self._client.get("/sub/home") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 6) for span in spans: # As we are only looking to the "outer" app, we would see only the "GET /sub" spans # -> the outer app is not aware of the sub_apps internal routes sub_in = "GET /sub" in span.name # The sub app spans are named GET /home as from the sub app perspective the request targets /home # -> the sub app is technically not aware of the /sub prefix home_in = "GET /home" in span.name # We expect the spans to be either from the outer app or the sub app self.assertTrue( sub_in or home_in, f"Span {span.name} does not have /sub or /home in its name", ) # We now want to specifically test all spans including the # - HTTP_TARGET # - HTTP_URL # attributes to be populated with the expected values spans_with_http_attributes = [ span for span in spans if (HTTP_URL in span.attributes or HTTP_TARGET in span.attributes) ] # We now expect spans with attributes from both the app and its sub app self.assertEqual(2, len(spans_with_http_attributes)) for span in spans_with_http_attributes: self.assertEqual("/sub/home", span.attributes[HTTP_TARGET]) self.assertEqual( "https://testserver/sub/home", span.attributes[HTTP_URL], ) class TestAutoInstrumentationLogic(unittest.TestCase): def test_instrumentation(self): """Verify that instrumentation methods are instrumenting and removing as expected. """ instrumentor = otel_fastapi.FastAPIInstrumentor() original = fastapi.FastAPI instrumentor.instrument() try: instrumented = fastapi.FastAPI self.assertIsNot(original, instrumented) finally: instrumentor.uninstrument() should_be_original = fastapi.FastAPI self.assertIs(original, should_be_original) class TestWrappedApplication(TestBase): def setUp(self): super().setUp() self.app = fastapi.FastAPI() @self.app.get("/foobar") async def _(): return {"message": "hello world"} otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) self.client = TestClient(self.app) self.tracer = self.tracer_provider.get_tracer(__name__) def tearDown(self) -> None: super().tearDown() with self.disable_logging(): otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) def test_mark_span_internal_in_presence_of_span_from_other_framework(self): with self.tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER) as parent_span: resp = self.client.get("/foobar") self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() for span in span_list: print(str(span.__class__) + ": " + str(span.__dict__)) # there should be 4 spans - single SERVER "test" and three INTERNAL "FastAPI" self.assertEqual(trace.SpanKind.INTERNAL, span_list[0].kind) self.assertEqual(trace.SpanKind.INTERNAL, span_list[1].kind) # main INTERNAL span - child of test self.assertEqual(trace.SpanKind.INTERNAL, span_list[2].kind) self.assertEqual(parent_span.context.span_id, span_list[2].parent.span_id) # SERVER "test" self.assertEqual(trace.SpanKind.SERVER, span_list[3].kind) self.assertEqual(parent_span.context.span_id, span_list[3].context.span_id) class TestFastAPIGarbageCollection(unittest.TestCase): def test_fastapi_app_is_collected_after_instrument(self): app = fastapi.FastAPI() otel_fastapi.FastAPIInstrumentor().instrument_app(app) app_ref = _weakref.ref(app) del app _gc.collect() self.assertIsNone(app_ref()) @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", }, ) class TestHTTPAppWithCustomHeaders(TestBase): def setUp(self): super().setUp() self.app = self._create_app() otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) self.client = TestClient(self.app) def tearDown(self) -> None: super().tearDown() with self.disable_logging(): otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) @staticmethod def _create_app(): app = fastapi.FastAPI() @app.get("/foobar") async def _(): headers = { "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", "my-custom-regex-header-1": "my-custom-regex-value-1,my-custom-regex-value-2", "My-Custom-Regex-Header-2": "my-custom-regex-value-3,my-custom-regex-value-4", "My-Secret-Header": "My Secret Value", } content = {"message": "hello world"} return JSONResponse(content=content, headers=headers) return app def test_http_custom_request_headers_in_span_attributes(self): expected = { "http.request.header.custom_test_header_1": ("test-header-value-1",), "http.request.header.custom_test_header_2": ("test-header-value-2",), "http.request.header.regex_test_header_1": ("Regex Test Value 1",), "http.request.header.regex_test_header_2": ("RegexTestValue2,RegexTestValue3",), "http.request.header.my_secret_header": ("[REDACTED]",), } resp = self.client.get( "/foobar", headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", "Regex-Test-Header-1": "Regex Test Value 1", "regex-test-header-2": "RegexTestValue2,RegexTestValue3", "My-Secret-Header": "My Secret Value", }, ) self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] self.assertSpanHasAttributes(server_span, expected) def test_http_custom_request_headers_not_in_span_attributes(self): not_expected = { "http.request.header.custom_test_header_3": ("test-header-value-3",), } resp = self.client.get( "/foobar", headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", "Regex-Test-Header-1": "Regex Test Value 1", "regex-test-header-2": "RegexTestValue2,RegexTestValue3", "My-Secret-Header": "My Secret Value", }, ) self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] for key, _ in not_expected.items(): self.assertNotIn(key, server_span.attributes) def test_http_custom_response_headers_in_span_attributes(self): expected = { "http.response.header.custom_test_header_1": ("test-header-value-1",), "http.response.header.custom_test_header_2": ("test-header-value-2",), "http.response.header.my_custom_regex_header_1": ("my-custom-regex-value-1,my-custom-regex-value-2",), "http.response.header.my_custom_regex_header_2": ("my-custom-regex-value-3,my-custom-regex-value-4",), "http.response.header.my_secret_header": ("[REDACTED]",), } resp = self.client.get("/foobar") self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] self.assertSpanHasAttributes(server_span, expected) def test_http_custom_response_headers_not_in_span_attributes(self): not_expected = { "http.response.header.custom_test_header_3": ("test-header-value-3",), } resp = self.client.get("/foobar") self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] for key, _ in not_expected.items(): self.assertNotIn(key, server_span.attributes) class TestHTTPAppWithCustomHeadersParameters(TestBase): """Minimal tests here since the behavior of this logic is tested above and in the ASGI tests.""" def setUp(self): super().setUp() self.instrumentor = otel_fastapi.FastAPIInstrumentor() self.kwargs = { "http_capture_headers_server_request": ["a.*", "b.*"], "http_capture_headers_server_response": ["c.*", "d.*"], "http_capture_headers_sanitize_fields": [".*secret.*"], } self.app = None def tearDown(self) -> None: super().tearDown() with self.disable_logging(): if self.app: self.instrumentor.uninstrument_app(self.app) else: self.instrumentor.uninstrument() @staticmethod def _create_app(): app = fastapi.FastAPI() @app.get("/foobar") async def _(): headers = { "carrot": "bar", "date-secret": "yellow", "egg": "ham", } content = {"message": "hello world"} return JSONResponse(content=content, headers=headers) return app def test_http_custom_request_headers_in_span_attributes_app(self): self.app = self._create_app() self.instrumentor.instrument_app(self.app, **self.kwargs) resp = TestClient(self.app).get( "/foobar", headers={ "apple": "red", "banana-secret": "yellow", "fig": "green", }, ) self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] expected = { # apple should be included because it starts with a "http.request.header.apple": ("red",), # same with banana because it starts with b, # redacted because it contains "secret" "http.request.header.banana_secret": ("[REDACTED]",), } self.assertSpanHasAttributes(server_span, expected) self.assertNotIn("http.request.header.fig", server_span.attributes) def test_http_custom_request_headers_in_span_attributes_instr(self): """As above, but use instrument(), not instrument_app().""" self.instrumentor.instrument(**self.kwargs) resp = TestClient(self._create_app()).get( "/foobar", headers={ "apple": "red", "banana-secret": "yellow", "fig": "green", }, ) self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] expected = { # apple should be included because it starts with a "http.request.header.apple": ("red",), # same with banana because it starts with b, # redacted because it contains "secret" "http.request.header.banana_secret": ("[REDACTED]",), } self.assertSpanHasAttributes(server_span, expected) self.assertNotIn("http.request.header.fig", server_span.attributes) def test_http_custom_response_headers_in_span_attributes_app(self): self.app = self._create_app() self.instrumentor.instrument_app(self.app, **self.kwargs) resp = TestClient(self.app).get("/foobar") self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] expected = { "http.response.header.carrot": ("bar",), "http.response.header.date_secret": ("[REDACTED]",), } self.assertSpanHasAttributes(server_span, expected) self.assertNotIn("http.response.header.egg", server_span.attributes) def test_http_custom_response_headers_in_span_attributes_inst(self): """As above, but use instrument(), not instrument_app().""" self.instrumentor.instrument(**self.kwargs) resp = TestClient(self._create_app()).get("/foobar") self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] expected = { "http.response.header.carrot": ("bar",), "http.response.header.date_secret": ("[REDACTED]",), } self.assertSpanHasAttributes(server_span, expected) self.assertNotIn("http.response.header.egg", server_span.attributes) @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", }, ) class TestWebSocketAppWithCustomHeaders(TestBase): def setUp(self): super().setUp() self.app = self._create_app() otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) self.client = TestClient(self.app) def tearDown(self) -> None: super().tearDown() with self.disable_logging(): otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) @staticmethod def _create_app(): app = fastapi.FastAPI() @app.websocket("/foobar_web") async def _(websocket: fastapi.WebSocket): message = await websocket.receive() if message.get("type") == "websocket.connect": await websocket.send( { "type": "websocket.accept", "headers": [ (b"custom-test-header-1", b"test-header-value-1"), (b"custom-test-header-2", b"test-header-value-2"), (b"Regex-Test-Header-1", b"Regex Test Value 1"), ( b"regex-test-header-2", b"RegexTestValue2,RegexTestValue3", ), (b"My-Secret-Header", b"My Secret Value"), ], } ) await websocket.send_json({"message": "hello world"}) await websocket.close() if message.get("type") == "websocket.disconnect": pass return app def test_web_socket_custom_request_headers_in_span_attributes(self): expected = { "http.request.header.custom_test_header_1": ("test-header-value-1",), "http.request.header.custom_test_header_2": ("test-header-value-2",), } with self.client.websocket_connect( "/foobar_web", headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", }, ) as websocket: data = websocket.receive_json() self.assertEqual(data, {"message": "hello world"}) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 5) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] self.assertSpanHasAttributes(server_span, expected) @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", }, ) def test_web_socket_custom_request_headers_not_in_span_attributes(self): not_expected = { "http.request.header.custom_test_header_3": ("test-header-value-3",), } with self.client.websocket_connect( "/foobar_web", headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", }, ) as websocket: data = websocket.receive_json() self.assertEqual(data, {"message": "hello world"}) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 5) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] for key, _ in not_expected.items(): self.assertNotIn(key, server_span.attributes) def test_web_socket_custom_response_headers_in_span_attributes(self): expected = { "http.response.header.custom_test_header_1": ("test-header-value-1",), "http.response.header.custom_test_header_2": ("test-header-value-2",), } with self.client.websocket_connect("/foobar_web") as websocket: data = websocket.receive_json() self.assertEqual(data, {"message": "hello world"}) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 5) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] self.assertSpanHasAttributes(server_span, expected) def test_web_socket_custom_response_headers_not_in_span_attributes(self): not_expected = { "http.response.header.custom_test_header_3": ("test-header-value-3",), } with self.client.websocket_connect("/foobar_web") as websocket: data = websocket.receive_json() self.assertEqual(data, {"message": "hello world"}) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 5) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] for key, _ in not_expected.items(): self.assertNotIn(key, server_span.attributes) @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", }, ) class TestNonRecordingSpanWithCustomHeaders(TestBase): def setUp(self): super().setUp() self.app = fastapi.FastAPI() @self.app.get("/foobar") async def _(): return {"message": "hello world"} reset_trace_globals() tracer_provider = trace.NoOpTracerProvider() trace.set_tracer_provider(tracer_provider=tracer_provider) self._instrumentor = otel_fastapi.FastAPIInstrumentor() self._instrumentor.instrument_app(self.app) self.client = TestClient(self.app) def tearDown(self) -> None: super().tearDown() with self.disable_logging(): self._instrumentor.uninstrument_app(self.app) def test_custom_header_not_present_in_non_recording_span(self): resp = self.client.get( "/foobar", headers={ "custom-test-header-1": "test-header-value-1", }, ) self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) class TestTraceableExceptionHandling(TestBase): """Tests to ensure FastAPI exception handlers are only executed once and with a valid context""" def setUp(self): super().setUp() self.app = fastapi.FastAPI() otel_fastapi.FastAPIInstrumentor().instrument_app(self.app, exclude_spans=["receive", "send"]) self.client = TestClient(self.app) self.tracer = self.tracer_provider.get_tracer(__name__) self.executed = 0 self.request_trace_id = None self.error_trace_id = None def tearDown(self) -> None: super().tearDown() with self.disable_logging(): otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) def test_error_handler_context(self): """OTEL tracing contexts must be available during error handler execution, and handlers must only be executed once""" status_code = 501 @self.app.exception_handler(Exception) async def _(*_): self.error_trace_id = trace.get_current_span().get_span_context().trace_id self.executed += 1 return PlainTextResponse("", status_code) @self.app.get("/foobar") async def _foobar(): self.request_trace_id = trace.get_current_span().get_span_context().trace_id raise UnhandledException("Test Exception") try: self.client.get( "/foobar", ) except UnhandledException: pass self.assertIsNotNone(self.request_trace_id) self.assertEqual(self.request_trace_id, self.error_trace_id) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET /foobar") self.assertEqual(span.attributes.get(HTTP_STATUS_CODE), status_code) self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(len(span.events), 1) event = span.events[0] self.assertEqual(event.name, "exception") assert event.attributes is not None self.assertEqual( event.attributes.get(EXCEPTION_TYPE), f"{__name__}.UnhandledException", ) self.assertEqual(self.executed, 1) def test_exception_span_recording(self): """Exceptions are always recorded in the active span""" @self.app.get("/foobar") async def _(): raise UnhandledException("Test Exception") try: self.client.get( "/foobar", ) except UnhandledException: pass spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET /foobar") self.assertEqual(span.attributes.get(HTTP_STATUS_CODE), 500) self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(len(span.events), 1) event = span.events[0] self.assertEqual(event.name, "exception") assert event.attributes is not None self.assertEqual( event.attributes.get(EXCEPTION_TYPE), f"{__name__}.UnhandledException", ) def test_middleware_exceptions(self): """Exceptions from user middlewares are recorded in the active span""" @self.app.get("/foobar") async def _foobar(): return PlainTextResponse("Hello World") @self.app.middleware("http") async def _(*_): raise UnhandledException("Test Exception") try: self.client.get( "/foobar", ) except UnhandledException: pass spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET /foobar") self.assertEqual(span.attributes.get(HTTP_STATUS_CODE), 500) self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(len(span.events), 1) event = span.events[0] self.assertEqual(event.name, "exception") assert event.attributes is not None self.assertEqual( event.attributes.get(EXCEPTION_TYPE), f"{__name__}.UnhandledException", ) class TestFastAPIFallback(TestBaseFastAPI): @pytest.fixture(autouse=True) def inject_fixtures(self, caplog): self.caplog = caplog @staticmethod def _create_fastapi_app(): app = TestBaseFastAPI._create_fastapi_app() def build_middleware_stack(): # Return something that is NOT a ServerErrorMiddleware so the # instrumentation fallback path triggers, but still wrap the # router with AsyncExitStackMiddleware so that newer FastAPI # versions (which assert ``fastapi_middleware_astack`` exists in # the request scope) can service requests normally. return AsyncExitStackMiddleware(app.router) app.build_middleware_stack = build_middleware_stack return app def setUp(self): super().setUp() self.client = TestClient(self._app) def test_no_instrumentation(self): self.client.get( "/foobar", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) errors = [record for record in self.caplog.get_records("call") if record.levelno >= logging.ERROR] self.assertEqual(len(errors), 1) self.assertEqual( errors[0].getMessage(), "Skipping FastAPI instrumentation due to unexpected middleware stack: expected ServerErrorMiddleware, got ", ) class TestFastAPIHostHeaderURL(TestBaseManualFastAPI): """Test suite for Host header URL functionality in FastAPI instrumentation.""" def test_host_header_url_construction(self): """Test that URLs use Host header value instead of server IP when available.""" # Test with a custom Host header - should use the domain name resp = self._client.get("/foobar?param=value", headers={"host": "api.mycompany.com"}) self.assertEqual(200, resp.status_code) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) # Find the server span (the main span, not internal middleware spans) server_span = None for span in spans: if span.kind == trace.SpanKind.SERVER and HTTP_URL in span.attributes: server_span = span break self.assertIsNotNone(server_span, "Server span with HTTP_URL not found") # Verify the URL uses the Host header domain instead of testserver expected_url = "https://api.mycompany.com/foobar?param=value" actual_url = server_span.attributes[HTTP_URL] self.assertEqual(expected_url, actual_url) # Also verify that the server name attribute is set correctly self.assertEqual("api.mycompany.com", server_span.attributes.get("http.server_name")) def test_host_header_with_port_url_construction(self): """Test Host header URL construction when host includes port.""" resp = self._client.get("/user/123", headers={"host": "staging.myapp.com:8443"}) self.assertEqual(200, resp.status_code) spans = self.memory_exporter.get_finished_spans() server_span = next( (span for span in spans if span.kind == trace.SpanKind.SERVER and HTTP_URL in span.attributes), None, ) self.assertIsNotNone(server_span) # Should use the host header value with non-standard port included expected_url = "https://staging.myapp.com:8443/user/123" actual_url = server_span.attributes[HTTP_URL] self.assertEqual(expected_url, actual_url) def test_no_host_header_fallback_behavior(self): """Test fallback to server name when no Host header is present.""" # Make request without custom Host header - should use testserver (default TestClient base) resp = self._client.get("/foobar") self.assertEqual(200, resp.status_code) spans = self.memory_exporter.get_finished_spans() server_span = next( (span for span in spans if span.kind == trace.SpanKind.SERVER and HTTP_URL in span.attributes), None, ) self.assertIsNotNone(server_span) # Should fallback to testserver (TestClient default, standard port stripped) expected_url = "https://testserver/foobar" actual_url = server_span.attributes[HTTP_URL] self.assertEqual(expected_url, actual_url) def test_production_scenario_host_header(self): """Test a realistic production scenario with Host header.""" # Simulate a production request with public domain in Host header resp = self._client.get( "/foobar?limit=10&offset=20", headers={ "host": "prod-api.example.com", "user-agent": "ProductionClient/1.0", }, ) self.assertEqual(200, resp.status_code) # Valid route should return 200 spans = self.memory_exporter.get_finished_spans() server_span = next( (span for span in spans if span.kind == trace.SpanKind.SERVER and HTTP_URL in span.attributes), None, ) self.assertIsNotNone(server_span) # URL should use the production domain from Host header (AS-IS, no default port) expected_url = "https://prod-api.example.com/foobar?limit=10&offset=20" actual_url = server_span.attributes[HTTP_URL] self.assertEqual(expected_url, actual_url) # Verify other attributes are still correct self.assertEqual("GET", server_span.attributes[HTTP_METHOD]) self.assertEqual("/foobar", server_span.attributes[HTTP_TARGET]) self.assertEqual( "prod-api.example.com", server_span.attributes.get("http.server_name"), ) def test_host_header_with_special_characters(self): """Test Host header handling with special characters and edge cases.""" test_cases = [ ( "api-v2.test-domain.com", "https://api-v2.test-domain.com/foobar", ), ("localhost", "https://localhost/foobar"), ( "192.168.1.100", "https://192.168.1.100/foobar", ), # IP address as host ( "test.domain.co.uk", "https://test.domain.co.uk/foobar", ), # Multiple dots ] for host_value, expected_url in test_cases: with self.subTest(host=host_value): # Clear previous spans self.memory_exporter.clear() resp = self._client.get("/foobar", headers={"host": host_value}) self.assertEqual(200, resp.status_code) spans = self.memory_exporter.get_finished_spans() server_span = next( (span for span in spans if span.kind == trace.SpanKind.SERVER and HTTP_URL in span.attributes), None, ) self.assertIsNotNone(server_span) actual_url = server_span.attributes[HTTP_URL] self.assertEqual(expected_url, actual_url) def test_host_header_maintains_span_attributes(self): """Test that using Host header doesn't break other span attributes.""" resp = self._client.get( "/user/testuser?debug=true", headers={ "host": "api.testapp.com", "user-agent": "TestClient/1.0", }, ) self.assertEqual(200, resp.status_code) spans = self.memory_exporter.get_finished_spans() server_span = next( (span for span in spans if span.kind == trace.SpanKind.SERVER and HTTP_URL in span.attributes), None, ) self.assertIsNotNone(server_span) # Verify URL uses Host header self.assertEqual( "https://api.testapp.com/user/testuser?debug=true", server_span.attributes[HTTP_URL], ) # Verify all other attributes are still present and correct self.assertEqual("GET", server_span.attributes[HTTP_METHOD]) self.assertEqual("/user/testuser", server_span.attributes[HTTP_TARGET]) self.assertEqual("https", server_span.attributes[HTTP_SCHEME]) self.assertEqual("api.testapp.com", server_span.attributes.get("http.server_name")) self.assertEqual(200, server_span.attributes[HTTP_STATUS_CODE]) # Check that route attribute is still set correctly if HTTP_ROUTE in server_span.attributes: self.assertEqual("/user/{username}", server_span.attributes[HTTP_ROUTE]) class TestFastAPIHostHeaderURLNewSemconv(TestFastAPIHostHeaderURL): """Test Host header URL functionality with new semantic conventions.""" def test_host_header_url_new_semconv(self): """Test Host header URL construction with new semantic conventions. Note: With new semantic conventions, the URL is split into components (url.scheme, server.address, url.path, etc.) rather than a single http.url. Host header support may work differently with new semantic conventions. """ resp = self._client.get("/foobar?test=new_semconv", headers={"host": "newapi.example.com"}) self.assertEqual(200, resp.status_code) spans = self.memory_exporter.get_finished_spans() # With new semantic conventions, look for the main HTTP span with route information server_span = next( (span for span in spans if span.kind == trace.SpanKind.SERVER and "http.route" in span.attributes), None, ) self.assertIsNotNone(server_span) # Verify we have the new semantic convention attributes self.assertIn("url.scheme", server_span.attributes) self.assertIn("server.address", server_span.attributes) self.assertIn("url.path", server_span.attributes) self.assertEqual("https", server_span.attributes.get("url.scheme")) self.assertEqual("/foobar", server_span.attributes.get("url.path")) # Current behavior: Host header may not affect server.address in new semantic conventions # This test documents the current behavior rather than enforcing Host header usage server_address = server_span.attributes.get("server.address", "") self.assertIsNotNone(server_address, "testserver") # Should have some value class TestFastAPIHostHeaderURLBothSemconv(TestFastAPIHostHeaderURL): """Test Host header URL functionality with both old and new semantic conventions.""" def test_host_header_url_both_semconv(self): """Test Host header URL construction with both semantic conventions enabled.""" resp = self._client.get("/foobar?test=both_semconv", headers={"host": "dual.example.com"}) self.assertEqual(200, resp.status_code) spans = self.memory_exporter.get_finished_spans() server_span = next( (span for span in spans if span.kind == trace.SpanKind.SERVER and HTTP_URL in span.attributes), None, ) self.assertIsNotNone(server_span) # Should use Host header for URL construction regardless of semantic convention mode expected_url = "https://dual.example.com/foobar?test=both_semconv" actual_url = server_span.attributes[HTTP_URL] self.assertEqual(expected_url, actual_url) def test_fastapi_unhandled_exception(self): """Override inherited test - use the both_semconv version instead.""" self.skipTest("Use test_fastapi_unhandled_exception_both_semconv instead") def test_fastapi_unhandled_exception_both_semconv(self): """If the application has an unhandled error the instrumentation should capture that a 500 response is returned.""" try: resp = self._client.get("/error") assert resp.status_code == 500, ( resp.content ) # pragma: no cover, for debugging this test if an exception is _not_ raised except UnhandledException: pass else: self.fail("Expected UnhandledException") spans = self.memory_exporter.get_finished_spans() # With both semantic conventions enabled, we expect 3 spans: # 1. Server span (main HTTP span) # 2. ASGI receive span # 3. ASGI send span (for error response) self.assertEqual(len(spans), 3) # Find the server span (it should have HTTP attributes) server_spans = [ span for span in spans if span.kind == trace.SpanKind.SERVER and hasattr(span, "attributes") and span.attributes and HTTP_URL in span.attributes ] self.assertEqual( len(server_spans), 1, "Expected exactly one server span with HTTP_URL", ) server_span = server_spans[0] # Ensure server_span is not None assert server_span is not None self.assertEqual(server_span.name, "GET /error") test_fastapi_instrumentation_custom_headers.py000066400000000000000000000322721527070072300453330ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Mapping from unittest.mock import patch import fastapi from starlette.responses import JSONResponse from starlette.testclient import TestClient import opentelemetry.instrumentation.fastapi as otel_fastapi from opentelemetry import trace from opentelemetry.test.globals_test import reset_trace_globals from opentelemetry.test.test_base import TestBase from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, ) class MultiMapping(Mapping): def __init__(self, *items: tuple[str, str]): self._items = items def __len__(self): return len(self._items) def __getitem__(self, __key): raise NotImplementedError("use .items() instead") def __iter__(self): raise NotImplementedError("use .items() instead") def items(self): return self._items @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", }, ) class TestHTTPAppWithCustomHeaders(TestBase): def setUp(self): super().setUp() self.app = self._create_app() otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) self.client = TestClient(self.app) def tearDown(self) -> None: super().tearDown() with self.disable_logging(): otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) @staticmethod def _create_app(): app = fastapi.FastAPI() @app.get("/foobar") async def _(): headers = MultiMapping( ("custom-test-header-1", "test-header-value-1"), ("custom-test-header-2", "test-header-value-2"), ("my-custom-regex-header-1", "my-custom-regex-value-1"), ("my-custom-regex-header-1", "my-custom-regex-value-2"), ("My-Custom-Regex-Header-2", "my-custom-regex-value-3"), ("My-Custom-Regex-Header-2", "my-custom-regex-value-4"), ("My-Secret-Header", "My Secret Value"), ) content = {"message": "hello world"} return JSONResponse(content=content, headers=headers) return app def test_http_custom_request_headers_in_span_attributes(self): expected = { "http.request.header.custom_test_header_1": ("test-header-value-1",), "http.request.header.custom_test_header_2": ("test-header-value-2",), "http.request.header.regex_test_header_1": ("Regex Test Value 1",), "http.request.header.regex_test_header_2": ("RegexTestValue2,RegexTestValue3",), "http.request.header.my_secret_header": ("[REDACTED]",), } resp = self.client.get( "/foobar", headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", "Regex-Test-Header-1": "Regex Test Value 1", "regex-test-header-2": "RegexTestValue2,RegexTestValue3", "My-Secret-Header": "My Secret Value", }, ) self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] self.assertSpanHasAttributes(server_span, expected) def test_http_custom_request_headers_not_in_span_attributes(self): not_expected = { "http.request.header.custom_test_header_3": ("test-header-value-3",), } resp = self.client.get( "/foobar", headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", "Regex-Test-Header-1": "Regex Test Value 1", "regex-test-header-2": "RegexTestValue2,RegexTestValue3", "My-Secret-Header": "My Secret Value", }, ) self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] for key, _ in not_expected.items(): self.assertNotIn(key, server_span.attributes) def test_http_custom_response_headers_in_span_attributes(self): expected = { "http.response.header.custom_test_header_1": ("test-header-value-1",), "http.response.header.custom_test_header_2": ("test-header-value-2",), "http.response.header.my_custom_regex_header_1": ( "my-custom-regex-value-1", "my-custom-regex-value-2", ), "http.response.header.my_custom_regex_header_2": ( "my-custom-regex-value-3", "my-custom-regex-value-4", ), "http.response.header.my_secret_header": ("[REDACTED]",), } resp = self.client.get("/foobar") self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] self.assertSpanHasAttributes(server_span, expected) def test_http_custom_response_headers_not_in_span_attributes(self): not_expected = { "http.response.header.custom_test_header_3": ("test-header-value-3",), } resp = self.client.get("/foobar") self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] for key, _ in not_expected.items(): self.assertNotIn(key, server_span.attributes) @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", }, ) class TestWebSocketAppWithCustomHeaders(TestBase): def setUp(self): super().setUp() self.app = self._create_app() otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) self.client = TestClient(self.app) def tearDown(self) -> None: super().tearDown() with self.disable_logging(): otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) @staticmethod def _create_app(): app = fastapi.FastAPI() @app.websocket("/foobar_web") async def _(websocket: fastapi.WebSocket): message = await websocket.receive() if message.get("type") == "websocket.connect": await websocket.send( { "type": "websocket.accept", "headers": [ (b"custom-test-header-1", b"test-header-value-1"), (b"custom-test-header-2", b"test-header-value-2"), (b"Regex-Test-Header-1", b"Regex Test Value 1"), ( b"regex-test-header-2", b"RegexTestValue2,RegexTestValue3", ), (b"My-Secret-Header", b"My Secret Value"), ], } ) await websocket.send_json({"message": "hello world"}) await websocket.close() if message.get("type") == "websocket.disconnect": pass return app def test_web_socket_custom_request_headers_in_span_attributes(self): expected = { "http.request.header.custom_test_header_1": ("test-header-value-1",), "http.request.header.custom_test_header_2": ("test-header-value-2",), } with self.client.websocket_connect( "/foobar_web", headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", }, ) as websocket: data = websocket.receive_json() self.assertEqual(data, {"message": "hello world"}) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 5) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] self.assertSpanHasAttributes(server_span, expected) @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", }, ) def test_web_socket_custom_request_headers_not_in_span_attributes(self): not_expected = { "http.request.header.custom_test_header_3": ("test-header-value-3",), } with self.client.websocket_connect( "/foobar_web", headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", }, ) as websocket: data = websocket.receive_json() self.assertEqual(data, {"message": "hello world"}) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 5) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] for key, _ in not_expected.items(): self.assertNotIn(key, server_span.attributes) def test_web_socket_custom_response_headers_in_span_attributes(self): expected = { "http.response.header.custom_test_header_1": ("test-header-value-1",), "http.response.header.custom_test_header_2": ("test-header-value-2",), } with self.client.websocket_connect("/foobar_web") as websocket: data = websocket.receive_json() self.assertEqual(data, {"message": "hello world"}) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 5) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] self.assertSpanHasAttributes(server_span, expected) def test_web_socket_custom_response_headers_not_in_span_attributes(self): not_expected = { "http.response.header.custom_test_header_3": ("test-header-value-3",), } with self.client.websocket_connect("/foobar_web") as websocket: data = websocket.receive_json() self.assertEqual(data, {"message": "hello world"}) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 5) server_span = [span for span in span_list if span.kind == trace.SpanKind.SERVER][0] for key, _ in not_expected.items(): self.assertNotIn(key, server_span.attributes) @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", }, ) class TestNonRecordingSpanWithCustomHeaders(TestBase): def setUp(self): super().setUp() self.app = fastapi.FastAPI() @self.app.get("/foobar") async def _(): return {"message": "hello world"} reset_trace_globals() tracer_provider = trace.NoOpTracerProvider() trace.set_tracer_provider(tracer_provider=tracer_provider) self._instrumentor = otel_fastapi.FastAPIInstrumentor() self._instrumentor.instrument_app(self.app) self.client = TestClient(self.app) def tearDown(self) -> None: super().tearDown() with self.disable_logging(): self._instrumentor.uninstrument_app(self.app) def test_custom_header_not_present_in_non_recording_span(self): resp = self.client.get( "/foobar", headers={ "custom-test-header-1": "test-header-value-1", }, ) self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) test_fastapi_instrumentation_wrapped.py000066400000000000000000000035421527070072300437660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-fastapi/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import fastapi from starlette.testclient import TestClient import opentelemetry.instrumentation.fastapi as otel_fastapi from opentelemetry import trace from opentelemetry.test.test_base import TestBase class TestWrappedApplication(TestBase): def setUp(self): super().setUp() self.app = fastapi.FastAPI() @self.app.get("/foobar") async def _(): return {"message": "hello world"} otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) self.client = TestClient(self.app) self.tracer = self.tracer_provider.get_tracer(__name__) def tearDown(self) -> None: super().tearDown() with self.disable_logging(): otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) def test_mark_span_internal_in_presence_of_span_from_other_framework(self): with self.tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER) as parent_span: resp = self.client.get("/foobar") self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() for span in span_list: print(str(span.__class__) + ": " + str(span.__dict__)) # there should be 4 spans - single SERVER "test" and three INTERNAL "FastAPI" self.assertEqual(trace.SpanKind.INTERNAL, span_list[0].kind) self.assertEqual(trace.SpanKind.INTERNAL, span_list[1].kind) # main INTERNAL span - child of test self.assertEqual(trace.SpanKind.INTERNAL, span_list[2].kind) self.assertEqual(parent_span.context.span_id, span_list[2].parent.span_id) # SERVER "test" self.assertEqual(trace.SpanKind.SERVER, span_list[3].kind) self.assertEqual(parent_span.context.span_id, span_list[3].context.span_id) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/000077500000000000000000000000001527070072300322435ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/LICENSE000066400000000000000000000261351527070072300332570ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/README.rst000066400000000000000000000013461527070072300337360ustar00rootroot00000000000000OpenTelemetry Flask Tracing =========================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-flask.svg :target: https://pypi.org/project/opentelemetry-instrumentation-flask/ This library builds on the OpenTelemetry WSGI middleware to track web requests in Flask applications. Installation ------------ :: pip install opentelemetry-instrumentation-flask References ---------- * `OpenTelemetry Flask Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000032751527070072300351070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-flask" dynamic = ["version"] description = "Flask instrumentation for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-instrumentation-wsgi == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-util-http == 0.66b2", "packaging >= 21.0", ] [project.optional-dependencies] instruments = [ "flask >= 1.0", ] [project.entry-points.opentelemetry_instrumentor] flask = "opentelemetry.instrumentation.flask:FlaskInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-flask" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/flask/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/src/000077500000000000000000000000001527070072300330325ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300356475ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/srcinstrumentation/000077500000000000000000000000001527070072300411125ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetryflask/000077500000000000000000000000001527070072300422125ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation__init__.py000066400000000000000000001021221527070072300443210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Note: This package is not named "flask" because of # https://github.com/PyCQA/pylint/issues/2648 """ This library builds on the OpenTelemetry WSGI middleware to track web requests in Flask applications. In addition to opentelemetry-util-http, it supports Flask-specific features such as: * The Flask url rule pattern is used as the Span name. * The ``http.route`` Span attribute is set so that one can see which URL rule matched a request. Usage ----- .. code-block:: python from flask import Flask from opentelemetry.instrumentation.flask import FlaskInstrumentor app = Flask(__name__) FlaskInstrumentor().instrument_app(app) @app.route("/") def hello(): return "Hello!" if __name__ == "__main__": app.run(debug=True) Configuration ------------- Exclude lists ************* To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_FLASK_EXCLUDED_URLS`` (or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the URLs. For example, :: export OTEL_PYTHON_FLASK_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. You can also pass comma delimited regexes directly to the ``instrument_app`` method: .. code-block:: python FlaskInstrumentor().instrument_app(app, excluded_urls="client/.*/info,healthcheck") Request/Response hooks ********************** This instrumentation supports request and response hooks. These are functions that get called right after a span is created for a request and right before the span is finished for the response. - The client request hook is called with the internal span and an instance of WSGIEnvironment (flask.request.environ) when the method ``receive`` is called. - The client response hook is called with the internal span, the status of the response and a list of key-value (tuples) representing the response headers returned from the response when the method ``send`` is called. For example, .. code-block:: python from opentelemetry.trace import Span from wsgiref.types import WSGIEnvironment from typing import List from opentelemetry.instrumentation.flask import FlaskInstrumentor def request_hook(span: Span, environ: WSGIEnvironment): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_request_hook", "some-value") def response_hook(span: Span, status: str, response_headers: List): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_response_hook", "some-value") FlaskInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook) Flask Request object reference: https://flask.palletsprojects.com/en/2.1.x/api/#flask.Request Capture HTTP request and response headers ***************************************** You can configure the agent to capture specified HTTP headers as span attributes, according to the `semantic conventions `_. Request headers *************** To capture HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. Request header names in Flask are case-insensitive and ``-`` characters are replaced by ``_``. So, giving the header name as ``CUStom_Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" Would match all request headers that start with ``Accept`` and ``X-``. To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*" The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a single item list containing all the header values. For example: ``http.request.header.custom_request_header = [","]`` Response headers **************** To capture HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. Response header names in Flask are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" Would match all response headers that start with ``Content`` and ``X-``. To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*" The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a single item list containing all the header values. For example: ``http.response.header.custom_response_header = [","]`` Sanitizing headers ****************** In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be matched in a case-insensitive manner. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. Note: The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. SQLCommenter ************ You can optionally enable sqlcommenter which enriches the query with contextual information. Queries made after setting up trace integration with sqlcommenter enabled will have configurable key-value pairs appended to them, e.g. ``"select * from auth_users; /*framework=flask%%3A2.9.3*/"``. This supports context propagation between database client and server when database log records are enabled. For more information, see: * `Semantic Conventions - Database Spans `_ * `sqlcommenter `_ .. code:: python from opentelemetry.instrumentation.flask import FlaskInstrumentor FlaskInstrumentor().instrument(enable_commenter=True) Note: FlaskInstrumentor sqlcommenter requires that sqlcommenter is also enabled for an active instrumentation of a database driver or object-relational mapper (ORM) in the same database client stack. The latter, such as Psycopg2Instrumentor of SQLAlchemyInstrumentor, will create a base sqlcomment that is enhanced by FlaskInstrumentor with additional values from context before appending to the query statement. SQLCommenter with commenter_options *********************************** The key-value pairs appended to the query can be configured using ``commenter_options``. When sqlcommenter is enabled, all available KVs/tags are calculated by default. ``commenter_options`` supports *opting out* of specific KVs. .. code:: python from opentelemetry.instrumentation.flask import FlaskInstrumentor # Opts into sqlcomment for Flask trace integration. # Opts out of tags for controller. FlaskInstrumentor().instrument( enable_commenter=True, commenter_options={ "controller": False, } ) Available commenter_options ########################### The following sqlcomment key-values can be opted out of through ``commenter_options``: +-------------------+----------------------------------------------------+----------------------------------------+ | Commenter Option | Description | Example | +===================+====================================================+========================================+ | ``framework`` | Flask framework name with version (URL encoded). | ``framework='flask%%%%3A2.9.3'`` | +-------------------+----------------------------------------------------+----------------------------------------+ | ``route`` | Flask route URI pattern. | ``route='/home'`` | +-------------------+----------------------------------------------------+----------------------------------------+ | ``controller`` | Flask controller/endpoint name. | ``controller='home_view'`` | +-------------------+----------------------------------------------------+----------------------------------------+ API --- """ import weakref from collections.abc import Collection from logging import getLogger from time import time_ns from timeit import default_timer import flask from packaging import version as package_version import opentelemetry.instrumentation.wsgi as otel_wsgi from opentelemetry import context, trace from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, _get_schema_url, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _report_new, _report_old, _StabilityMode, ) from opentelemetry.instrumentation.flask.package import _instruments from opentelemetry.instrumentation.flask.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.propagators import ( get_global_response_propagator, ) from opentelemetry.instrumentation.utils import _start_internal_or_server_span from opentelemetry.metrics import get_meter from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_ROUTE, HTTP_TARGET, ) from opentelemetry.semconv._incubating.metrics.http_metrics import ( create_http_server_active_requests, ) from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.semconv.metrics.http_metrics import ( HTTP_SERVER_REQUEST_DURATION, ) from opentelemetry.util._importlib_metadata import version from opentelemetry.util.http import ( get_excluded_urls, parse_excluded_urls, sanitize_method, ) _logger = getLogger(__name__) _ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key" _ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key" _ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key" _ENVIRON_REQCTX_REF_KEY = "opentelemetry-flask.reqctx_ref_key" _ENVIRON_TOKEN = "opentelemetry-flask.token" _excluded_urls_from_env = get_excluded_urls("FLASK") flask_version = version("flask") # Global constant for Flask 3.1+ streaming context cleanup _IS_FLASK_31_PLUS = package_version.parse(flask_version) >= package_version.parse("3.1.0") if package_version.parse(flask_version) >= package_version.parse("2.2.0"): def _request_ctx_ref() -> weakref.ReferenceType: return weakref.ref(flask.globals.request_ctx._get_current_object()) else: def _request_ctx_ref() -> weakref.ReferenceType: return weakref.ref(flask._request_ctx_stack.top) def get_default_span_name(): method = sanitize_method(flask.request.environ.get("REQUEST_METHOD", "").strip()) if method == "_OTHER": method = "HTTP" try: span_name = f"{method} {flask.request.url_rule.rule}" except AttributeError: span_name = otel_wsgi.get_default_span_name(flask.request.environ) return span_name def _rewrapped_app( wsgi_app, active_requests_counter, duration_histogram_old=None, response_hook=None, excluded_urls=None, sem_conv_opt_in_mode=_StabilityMode.DEFAULT, duration_histogram_new=None, ): # pylint: disable=too-many-statements def _wrapped_app(wrapped_app_environ, start_response): # We want to measure the time for route matching, etc. # In theory, we could start the span here and use # update_name later but that API is "highly discouraged" so # we better avoid it. wrapped_app_environ[_ENVIRON_STARTTIME_KEY] = time_ns() start = default_timer() attributes = otel_wsgi.collect_request_attributes(wrapped_app_environ, sem_conv_opt_in_mode) active_requests_count_attrs = otel_wsgi._parse_active_request_count_attrs( attributes, sem_conv_opt_in_mode, ) active_requests_counter.add(1, active_requests_count_attrs) request_route = None should_trace = True def _start_response(status, response_headers, *args, **kwargs): nonlocal should_trace should_trace = _should_trace(excluded_urls) if should_trace: nonlocal request_route request_route = flask.request.url_rule span = flask.request.environ.get(_ENVIRON_SPAN_KEY) propagator = get_global_response_propagator() if propagator: propagator.inject( response_headers, setter=otel_wsgi.default_response_propagation_setter, ) if span: otel_wsgi.add_response_attributes( span, status, response_headers, attributes, sem_conv_opt_in_mode, ) if span.is_recording() and span.kind == trace.SpanKind.SERVER: custom_attributes = otel_wsgi.collect_custom_response_headers_attributes(response_headers) if len(custom_attributes) > 0: span.set_attributes(custom_attributes) else: _logger.warning( "Flask environ's OpenTelemetry span missing at _start_response(%s)", status, ) if response_hook is not None: response_hook(span, status, response_headers) return start_response(status, response_headers, *args, **kwargs) try: result = wsgi_app(wrapped_app_environ, _start_response) # Note: Streaming response context cleanup is now handled in the Flask teardown function # (_wrapped_teardown_request) to ensure proper cleanup following Logfire's recommendations # for OpenTelemetry generator context management if should_trace: duration_s = default_timer() - start # Get the span from wrapped_app_environ and re-create context manually # to pass to histogram for exemplars generation span = wrapped_app_environ.get(_ENVIRON_SPAN_KEY) metrics_context = trace.set_span_in_context(span) if duration_histogram_old: duration_attrs_old = otel_wsgi._parse_duration_attrs(attributes, _StabilityMode.DEFAULT) if request_route: # http.target to be included in old semantic conventions duration_attrs_old[HTTP_TARGET] = str(request_route) duration_histogram_old.record( max(round(duration_s * 1000), 0), duration_attrs_old, context=metrics_context, ) if duration_histogram_new: duration_attrs_new = otel_wsgi._parse_duration_attrs(attributes, _StabilityMode.HTTP) if request_route: duration_attrs_new[HTTP_ROUTE] = str(request_route) duration_histogram_new.record( max(duration_s, 0), duration_attrs_new, context=metrics_context, ) return result finally: active_requests_counter.add(-1, active_requests_count_attrs) def _should_trace(excluded_urls) -> bool: return bool(flask.request and (excluded_urls is None or not excluded_urls.url_disabled(flask.request.url))) return _wrapped_app def _wrapped_before_request( request_hook=None, tracer=None, excluded_urls=None, enable_commenter=True, commenter_options=None, sem_conv_opt_in_mode=_StabilityMode.DEFAULT, ): def _before_request(): if excluded_urls and excluded_urls.url_disabled(flask.request.url): return flask_request_environ = flask.request.environ span_name = get_default_span_name() attributes = otel_wsgi.collect_request_attributes( flask_request_environ, sem_conv_opt_in_mode=sem_conv_opt_in_mode, ) if flask.request.url_rule: # For 404 that result from no route found, etc, we # don't have a url_rule. attributes[HTTP_ROUTE] = flask.request.url_rule.rule span, token = _start_internal_or_server_span( tracer=tracer, span_name=span_name, start_time=flask_request_environ.get(_ENVIRON_STARTTIME_KEY), context_carrier=flask_request_environ, context_getter=otel_wsgi.wsgi_getter, attributes=attributes, ) if request_hook: request_hook(span, flask_request_environ) if span.is_recording(): for key, value in attributes.items(): span.set_attribute(key, value) if span.is_recording() and span.kind == trace.SpanKind.SERVER: custom_attributes = otel_wsgi.collect_custom_request_headers_attributes(flask_request_environ) if len(custom_attributes) > 0: span.set_attributes(custom_attributes) activation = trace.use_span(span, end_on_exit=True) activation.__enter__() # pylint: disable=unnecessary-dunder-call flask_request_environ[_ENVIRON_ACTIVATION_KEY] = activation flask_request_environ[_ENVIRON_REQCTX_REF_KEY] = _request_ctx_ref() flask_request_environ[_ENVIRON_SPAN_KEY] = span flask_request_environ[_ENVIRON_TOKEN] = token if enable_commenter: current_context = context.get_current() flask_info = {} # https://flask.palletsprojects.com/en/1.1.x/api/#flask.has_request_context if flask and flask.request: if commenter_options.get("framework", True): flask_info["framework"] = f"flask:{flask_version}" if commenter_options.get("controller", True) and flask.request.endpoint: flask_info["controller"] = flask.request.endpoint if commenter_options.get("route", True) and flask.request.url_rule and flask.request.url_rule.rule: flask_info["route"] = flask.request.url_rule.rule sqlcommenter_context = context.set_value("SQLCOMMENTER_ORM_TAGS_AND_VALUES", flask_info, current_context) context.attach(sqlcommenter_context) return _before_request def _wrapped_teardown_request( excluded_urls=None, ): def _teardown_request(exc): # pylint: disable=unnecessary-dunder-call if excluded_urls and excluded_urls.url_disabled(flask.request.url): return activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY) token = flask.request.environ.get(_ENVIRON_TOKEN) original_reqctx_ref = flask.request.environ.get(_ENVIRON_REQCTX_REF_KEY) current_reqctx_ref = _request_ctx_ref() if not activation or original_reqctx_ref != current_reqctx_ref: # This request didn't start a span, maybe because it was created in # a way that doesn't run `before_request`, like when it is created # with `app.test_request_context`. # # Similarly, check that the request_ctx that created the span # matches the current request_ctx, and only tear down if they match. # This situation can arise if the original request_ctx handling # the request calls functions that push new request_ctx's, # like any decorated with `flask.copy_current_request_context`. return try: # For Flask 3.1+, check if this is a streaming response that might # have already been cleaned up to prevent double cleanup is_streaming = False if _IS_FLASK_31_PLUS: try: # Additional safety check: verify we're in a Flask request context if hasattr(flask, "request") and hasattr(flask.request, "response"): is_streaming = ( hasattr(flask.request, "response") and flask.request.response and hasattr(flask.request.response, "stream") and flask.request.response.stream ) except (RuntimeError, AttributeError): # Not in a proper Flask request context, don't check for streaming is_streaming = False if _IS_FLASK_31_PLUS and is_streaming: # For Flask 3.1+ streaming responses, ensure OpenTelemetry contexts are cleaned up # This addresses the generator context leak issues documented by Logfire # (open-telemetry/opentelemetry-python#2606) try: context.detach(token) if hasattr(activation, "__exit__"): activation.__exit__(None, None, None) # Mark as cleaned up flask.request.environ[_ENVIRON_ACTIVATION_KEY] = None flask.request.environ[_ENVIRON_TOKEN] = None _logger.debug("Streaming response context cleanup completed in teardown function") except ( RuntimeError, ValueError, TypeError, AttributeError, ) as cleanup_exc: _logger.debug( "Teardown streaming context cleanup failed: %s", cleanup_exc, ) return if exc is None: activation.__exit__(None, None, None) else: activation.__exit__(type(exc), exc, getattr(exc, "__traceback__", None)) if token: context.detach(token) flask.request.environ.pop(_ENVIRON_ACTIVATION_KEY, None) flask.request.environ.pop(_ENVIRON_TOKEN, None) except (RuntimeError, AttributeError, ValueError) as teardown_exc: # Log the error but don't raise it to avoid breaking the request handling _logger.debug( "Error during request teardown: %s", teardown_exc, exc_info=True, ) return _teardown_request class _InstrumentedFlask(flask.Flask): _excluded_urls = None _tracer_provider = None _request_hook = None _response_hook = None _enable_commenter = True _commenter_options = None _meter_provider = None _sem_conv_opt_in_mode = _StabilityMode.DEFAULT def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._original_wsgi_app = self.wsgi_app self._is_instrumented_by_opentelemetry = True meter = get_meter( __name__, __version__, _InstrumentedFlask._meter_provider, schema_url=_get_schema_url(_InstrumentedFlask._sem_conv_opt_in_mode), ) duration_histogram_old = None if _report_old(_InstrumentedFlask._sem_conv_opt_in_mode): duration_histogram_old = meter.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", description="Measures the duration of inbound HTTP requests.", ) duration_histogram_new = None if _report_new(_InstrumentedFlask._sem_conv_opt_in_mode): duration_histogram_new = meter.create_histogram( name=HTTP_SERVER_REQUEST_DURATION, unit="s", description="Duration of HTTP server requests.", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) if _report_new(_InstrumentedFlask._sem_conv_opt_in_mode): active_requests_counter = create_http_server_active_requests(meter) else: active_requests_counter = meter.create_up_down_counter( name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, unit="requests", description="Measures the number of concurrent HTTP requests that are currently in-flight.", ) self.wsgi_app = _rewrapped_app( self.wsgi_app, active_requests_counter, duration_histogram_old, _InstrumentedFlask._response_hook, excluded_urls=_InstrumentedFlask._excluded_urls, sem_conv_opt_in_mode=_InstrumentedFlask._sem_conv_opt_in_mode, duration_histogram_new=duration_histogram_new, ) tracer = trace.get_tracer( __name__, __version__, _InstrumentedFlask._tracer_provider, schema_url=_get_schema_url(_InstrumentedFlask._sem_conv_opt_in_mode), ) _before_request = _wrapped_before_request( _InstrumentedFlask._request_hook, tracer, excluded_urls=_InstrumentedFlask._excluded_urls, enable_commenter=_InstrumentedFlask._enable_commenter, commenter_options=_InstrumentedFlask._commenter_options, sem_conv_opt_in_mode=_InstrumentedFlask._sem_conv_opt_in_mode, ) self._before_request = _before_request self.before_request(_before_request) _teardown_request = _wrapped_teardown_request( excluded_urls=_InstrumentedFlask._excluded_urls, ) self.teardown_request(_teardown_request) class FlaskInstrumentor(BaseInstrumentor): # pylint: disable=protected-access """An instrumentor for flask.Flask See `BaseInstrumentor` """ def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): self._original_flask = flask.Flask request_hook = kwargs.get("request_hook") response_hook = kwargs.get("response_hook") if callable(request_hook): _InstrumentedFlask._request_hook = request_hook if callable(response_hook): _InstrumentedFlask._response_hook = response_hook tracer_provider = kwargs.get("tracer_provider") _InstrumentedFlask._tracer_provider = tracer_provider excluded_urls = kwargs.get("excluded_urls") _InstrumentedFlask._excluded_urls = ( _excluded_urls_from_env if excluded_urls is None else parse_excluded_urls(excluded_urls) ) enable_commenter = kwargs.get("enable_commenter", True) _InstrumentedFlask._enable_commenter = enable_commenter commenter_options = kwargs.get("commenter_options", {}) _InstrumentedFlask._commenter_options = commenter_options meter_provider = kwargs.get("meter_provider") _InstrumentedFlask._meter_provider = meter_provider sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) _InstrumentedFlask._sem_conv_opt_in_mode = sem_conv_opt_in_mode flask.Flask = _InstrumentedFlask def _uninstrument(self, **kwargs): flask.Flask = self._original_flask # pylint: disable=too-many-locals @staticmethod def instrument_app( app, request_hook=None, response_hook=None, tracer_provider=None, excluded_urls=None, enable_commenter=True, commenter_options=None, meter_provider=None, ): if not hasattr(app, "_is_instrumented_by_opentelemetry"): app._is_instrumented_by_opentelemetry = False if not app._is_instrumented_by_opentelemetry: # initialize semantic conventions opt-in if needed _OpenTelemetrySemanticConventionStability._initialize() sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) excluded_urls = parse_excluded_urls(excluded_urls) if excluded_urls is not None else _excluded_urls_from_env meter = get_meter( __name__, __version__, meter_provider, schema_url=_get_schema_url(sem_conv_opt_in_mode), ) duration_histogram_old = None if _report_old(sem_conv_opt_in_mode): duration_histogram_old = meter.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", description="Measures the duration of inbound HTTP requests.", ) duration_histogram_new = None if _report_new(sem_conv_opt_in_mode): duration_histogram_new = meter.create_histogram( name=HTTP_SERVER_REQUEST_DURATION, unit="s", description="Duration of HTTP server requests.", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) if _report_new(sem_conv_opt_in_mode): active_requests_counter = create_http_server_active_requests(meter) else: active_requests_counter = meter.create_up_down_counter( name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, unit="requests", description="Measures the number of concurrent HTTP requests that are currently in-flight.", ) app._original_wsgi_app = app.wsgi_app app.wsgi_app = _rewrapped_app( app.wsgi_app, active_requests_counter, duration_histogram_old, response_hook=response_hook, excluded_urls=excluded_urls, sem_conv_opt_in_mode=sem_conv_opt_in_mode, duration_histogram_new=duration_histogram_new, ) tracer = trace.get_tracer( __name__, __version__, tracer_provider, schema_url=_get_schema_url(sem_conv_opt_in_mode), ) _before_request = _wrapped_before_request( request_hook, tracer, excluded_urls=excluded_urls, enable_commenter=enable_commenter, commenter_options=(commenter_options if commenter_options else {}), sem_conv_opt_in_mode=sem_conv_opt_in_mode, ) app._before_request = _before_request app.before_request(_before_request) _teardown_request = _wrapped_teardown_request( excluded_urls=excluded_urls, ) app._teardown_request = _teardown_request app.teardown_request(_teardown_request) app._is_instrumented_by_opentelemetry = True else: _logger.warning("Attempting to instrument Flask app while already instrumented") @staticmethod def uninstrument_app(app): if hasattr(app, "_original_wsgi_app"): app.wsgi_app = app._original_wsgi_app # FIXME add support for other Flask blueprints that are not None app.before_request_funcs[None].remove(app._before_request) app.teardown_request_funcs[None].remove(app._teardown_request) del app._original_wsgi_app app._is_instrumented_by_opentelemetry = False else: _logger.warning("Attempting to uninstrument Flask app while already uninstrumented") package.py000066400000000000000000000002501527070072300441540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("flask >= 1.0",) _supports_metrics = True _semconv_status = "migration" version.py000066400000000000000000000001441527070072300442500ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements-0.txt000066400000000000000000000006771527070072300365740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flaskasgiref==3.8.1 click==8.1.7 Deprecated==1.2.14 Flask==2.1.3 iniconfig==2.0.0 itsdangerous==2.1.2 Jinja2==3.1.6 MarkupSafe==2.1.2 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 Werkzeug==2.3.8 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-wsgi -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-flask test-requirements-1.txt000066400000000000000000000006771527070072300365750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flaskasgiref==3.8.1 click==8.1.7 Deprecated==1.2.14 Flask==2.2.0 iniconfig==2.0.0 itsdangerous==2.1.2 Jinja2==3.1.6 MarkupSafe==2.1.2 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 Werkzeug==2.3.8 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-wsgi -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-flask test-requirements-2.txt000066400000000000000000000007161527070072300365700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flaskasgiref==3.8.1 blinker==1.7.0 click==8.1.7 Deprecated==1.2.14 Flask==3.0.2 iniconfig==2.0.0 itsdangerous==2.1.2 Jinja2==3.1.6 MarkupSafe==2.1.2 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 Werkzeug==3.0.6 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-wsgi -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-flask test-requirements-3.txt000066400000000000000000000007161527070072300365710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flaskasgiref==3.8.1 blinker>=1.9.0 click==8.1.7 Deprecated==1.2.14 Flask>=3.1.0 iniconfig==2.0.0 itsdangerous>=2.2.0 Jinja2==3.1.6 MarkupSafe==2.1.2 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 Werkzeug>=3.1.0 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-wsgi -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-flask opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/tests/000077500000000000000000000000001527070072300334055ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300354250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/testsbase_test.py000066400000000000000000000067641527070072300356660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from concurrent.futures import ( # pylint: disable=E0611 ThreadPoolExecutor, as_completed, ) from random import randint import flask from werkzeug.test import Client from werkzeug.wrappers import Response from opentelemetry import context, trace class InstrumentationTest: @staticmethod def _hello_endpoint(helloid): if helloid == 500: raise ValueError(":-(") return "Hello: " + str(helloid) @staticmethod def _sqlcommenter_endpoint(): current_context = context.get_current() sqlcommenter_flask_values = current_context.get("SQLCOMMENTER_ORM_TAGS_AND_VALUES", {}) return sqlcommenter_flask_values @staticmethod def _copy_context_endpoint(): @flask.copy_current_request_context def _extract_header(): return flask.request.headers["x-req"] # Despite `_extract_header` copying the request context, # calling it shouldn't detach the parent Flask span's contextvar request_header = _extract_header() return { "span_name": trace.get_current_span().name, "request_header": request_header, } @staticmethod def _multithreaded_endpoint(count): def do_random_stuff(): @flask.copy_current_request_context def inner(): return randint(0, 100) return inner executor = ThreadPoolExecutor(count) futures = [] for _ in range(count): futures.append(executor.submit(do_random_stuff())) numbers = [] for future in as_completed(futures): numbers.append(future.result()) return " ".join([str(i) for i in numbers]) @staticmethod def _custom_response_headers(): resp = flask.Response("test response") resp.headers["content-type"] = "text/plain; charset=utf-8" resp.headers["content-length"] = "13" resp.headers["my-custom-header"] = "my-custom-value-1,my-custom-header-2" resp.headers["my-custom-regex-header-1"] = "my-custom-regex-value-1,my-custom-regex-value-2" resp.headers["My-Custom-Regex-Header-2"] = "my-custom-regex-value-3,my-custom-regex-value-4" resp.headers["my-secret-header"] = "my-secret-value" return resp @staticmethod def _repeat_custom_response_headers(): headers = { "content-type": "text/plain; charset=utf-8", "my-custom-header": ["my-custom-value-1", "my-custom-header-2"], } return flask.Response("test response", headers=headers) def _common_initialization(self): def excluded_endpoint(): return "excluded" def excluded2_endpoint(): return "excluded2" # pylint: disable=no-member self.app.route("/hello/")(self._hello_endpoint) self.app.route("/sqlcommenter")(self._sqlcommenter_endpoint) self.app.route("/multithreaded")(self._multithreaded_endpoint) self.app.route("/copy_context")(self._copy_context_endpoint) self.app.route("/excluded/")(self._hello_endpoint) self.app.route("/excluded")(excluded_endpoint) self.app.route("/excluded2")(excluded2_endpoint) self.app.route("/test_custom_response_headers")(self._custom_response_headers) self.app.route("/test_repeat_custom_response_headers")(self._repeat_custom_response_headers) self.client = Client(self.app, Response) test_automatic.py000066400000000000000000000055131527070072300367310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import flask from werkzeug.test import Client from werkzeug.wrappers import Response from opentelemetry import trace as trace_api from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.test.wsgitestutil import WsgiTestBase # pylint: disable=import-error from .base_test import InstrumentationTest class TestAutomatic(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() FlaskInstrumentor().instrument() self.app = flask.Flask(__name__) self._common_initialization() def tearDown(self): super().tearDown() with self.disable_logging(): FlaskInstrumentor().uninstrument() def test_uninstrument(self): # pylint: disable=access-member-before-definition resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) self.assertEqual([b"Hello: 123"], list(resp.response)) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) FlaskInstrumentor().uninstrument() self.app = flask.Flask(__name__) self.app.route("/hello/")(self._hello_endpoint) self.client = Client(self.app, Response) resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) self.assertEqual([b"Hello: 123"], list(resp.response)) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) def test_exluded_urls_explicit(self): FlaskInstrumentor().uninstrument() FlaskInstrumentor().instrument(excluded_urls="/hello/456") self.app = flask.Flask(__name__) self.app.route("/hello/")(self._hello_endpoint) client = Client(self.app, Response) resp = client.get("/hello/123") self.assertEqual(200, resp.status_code) self.assertEqual([b"Hello: 123"], list(resp.response)) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) resp = client.get("/hello/456") self.assertEqual(200, resp.status_code) self.assertEqual([b"Hello: 456"], list(resp.response)) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) def test_no_op_tracer_provider(self): FlaskInstrumentor().uninstrument() FlaskInstrumentor().instrument(tracer_provider=trace_api.NoOpTracerProvider()) self.app = flask.Flask(__name__) self.app.route("/hello/")(self._hello_endpoint) self.client = Client(self.app, Response) self.client.get("/hello/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) test_copy_context.py000066400000000000000000000024701527070072300374600ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import flask from werkzeug.test import Client from werkzeug.wrappers import Response from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.test.wsgitestutil import WsgiTestBase from .base_test import InstrumentationTest class TestCopyContext(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() FlaskInstrumentor().instrument() self.app = flask.Flask(__name__) self._common_initialization() def tearDown(self): super().tearDown() with self.disable_logging(): FlaskInstrumentor().uninstrument() def test_copycontext(self): """Test that instrumentation tear down does not blow up when the request calls functions where the context has been copied via `flask.copy_current_request_context` """ self.app = flask.Flask(__name__) self.app.route("/copy_context")(self._copy_context_endpoint) client = Client(self.app, Response) resp = client.get("/copy_context", headers={"x-req": "a-header"}) self.assertEqual(200, resp.status_code) self.assertEqual("GET /copy_context", resp.json["span_name"]) self.assertEqual("a-header", resp.json["request_header"]) test_flask_compatibility.py000066400000000000000000000344141527070072300407760ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Tests for Flask compatibility across versions, focusing on context cleanup and streaming response handling. """ import io import threading import time from importlib.metadata import version from unittest import mock, skipIf import flask from opentelemetry import trace from opentelemetry.instrumentation.flask import ( _ENVIRON_ACTIVATION_KEY, _ENVIRON_TOKEN, FlaskInstrumentor, _request_ctx_ref, ) from opentelemetry.test.wsgitestutil import WsgiTestBase class TestFlaskCompatibility(WsgiTestBase): def setUp(self): super().setUp() self.flask_version = version("flask") def test_streaming_response_context_cleanup(self): """Test that streaming responses properly clean up context""" app = flask.Flask(__name__) FlaskInstrumentor().instrument_app(app) @app.route("/stream") def streaming_endpoint(): def generate(): yield "Hello " yield "World!" return flask.Response(flask.stream_with_context(generate())) @app.route("/normal") def normal_endpoint(): return "Normal Response" client = app.test_client() # Test streaming response with self.subTest("streaming_response"): response = client.get("/stream") self.assertEqual(response.status_code, 200) self.assertEqual(b"Hello World!", response.data) # Verify that context is properly cleaned up current_span = trace.get_current_span() span_context = current_span.get_span_context() # Either we have a valid trace (non-zero) or we have a NoOp span self.assertTrue(span_context.trace_id >= 0) # Test normal response for comparison with self.subTest("normal_response"): response = client.get("/normal") self.assertEqual(response.status_code, 200) self.assertEqual(b"Normal Response", response.data) def test_generator_response_context_cleanup(self): """Test that generator responses properly clean up context""" app = flask.Flask(__name__) FlaskInstrumentor().instrument_app(app) @app.route("/generator") def generator_endpoint(): def generate(): for chunk_idx in range(5): yield f"Chunk {chunk_idx}\n" return flask.Response(flask.stream_with_context(generate())) client = app.test_client() response = client.get("/generator") self.assertEqual(response.status_code, 200) expected = b"".join(f"Chunk {chunk_idx}\n".encode() for chunk_idx in range(5)) self.assertEqual(response.data, expected) def test_file_response_context_cleanup(self): """Test context cleanup with file responses""" app = flask.Flask(__name__) FlaskInstrumentor().instrument_app(app) @app.route("/file") def file_endpoint(): # Simulate file response using io.BytesIO file_data = io.BytesIO(b"File content here") return flask.send_file(file_data, as_attachment=True, download_name="test.txt") client = app.test_client() response = client.get("/file") self.assertEqual(response.status_code, 200) self.assertEqual(response.data, b"File content here") def test_multiple_requests_context_isolation(self): """Test context isolation between multiple requests""" app = flask.Flask(__name__) FlaskInstrumentor().instrument_app(app) results = [] @app.route("/test/") def test_endpoint(request_id): results.append(f"Request {request_id}") return f"Response {request_id}" client = app.test_client() # Make multiple requests and verify they don't interfere for request_idx in range(10): response = client.get(f"/test/{request_idx}") self.assertEqual(response.status_code, 200) self.assertEqual(response.data, f"Response {request_idx}".encode()) # Verify all requests were processed self.assertEqual(len(results), 10) for request_idx in range(10): self.assertIn(f"Request {request_idx}", results) def test_request_context_reference_handling(self): """Test request context reference handling works correctly""" app = flask.Flask(__name__) FlaskInstrumentor().instrument_app(app) @app.route("/context_test") def context_test_endpoint(): # Store request context reference at different points context_refs = [] context_refs.append(_request_ctx_ref()) # Do some work result = "Context test" context_refs.append(_request_ctx_ref()) return result client = app.test_client() response = client.get("/context_test") self.assertEqual(response.status_code, 200) self.assertEqual(response.data, b"Context test") def test_concurrent_requests_isolation(self): """Test that concurrent requests have proper context isolation""" app = flask.Flask(__name__) FlaskInstrumentor().instrument_app(app) results = [] errors = [] @app.route("/slow_stream/") def slow_stream_endpoint(request_id): def generate(): for chunk_idx in range(3): time.sleep(0.01) # Small delay to simulate work yield f"Request {request_id} - Chunk {chunk_idx}\n" return flask.Response(flask.stream_with_context(generate())) def make_request(request_id): try: client = app.test_client() response = client.get(f"/slow_stream/{request_id}") results.append((request_id, response.status_code, response.data)) except (RuntimeError, ValueError) as exc: errors.append((request_id, exc)) # Create multiple concurrent requests threads = [] for thread_idx in range(5): thread = threading.Thread(target=make_request, args=(thread_idx,)) threads.append(thread) thread.start() # Wait for all threads to complete for thread in threads: thread.join() # Verify all requests completed successfully self.assertEqual(len(errors), 0, f"Errors occurred: {errors}") self.assertEqual(len(results), 5) for request_id, status_code, data in results: self.assertEqual(status_code, 200) expected = b"".join(f"Request {request_id} - Chunk {chunk_idx}\n".encode() for chunk_idx in range(3)) self.assertEqual(data, expected) def test_flask_version_compatibility(self): """Test that the instrumentation works with the current Flask version""" app = flask.Flask(__name__) FlaskInstrumentor().instrument_app(app) @app.route("/version_test") def version_test_endpoint(): return f"Flask {self.flask_version} compatible" client = app.test_client() response = client.get("/version_test") self.assertEqual(response.status_code, 200) expected_text = f"Flask {self.flask_version} compatible" self.assertEqual(response.data, expected_text.encode()) def test_context_leak_prevention(self): """Test that context doesn't leak after multiple requests""" app = flask.Flask(__name__) FlaskInstrumentor().instrument_app(app) @app.route("/leak_test") def leak_test_endpoint(): def generate(): yield "Part 1\n" yield "Part 2\n" yield "Part 3\n" return flask.Response(flask.stream_with_context(generate())) client = app.test_client() # Make multiple streaming requests for _ in range(10): response = client.get("/leak_test") self.assertEqual(response.status_code, 200) expected = b"Part 1\nPart 2\nPart 3\n" self.assertEqual(response.data, expected) # After all requests, there should be no lingering context # This is a basic check - more sophisticated leak detection would require # instrumentation-specific testing utilities # If we got here without errors, context cleanup worked def test_streaming_with_error_in_cleanup(self): """Test graceful handling when cleanup operations fail""" app = flask.Flask(__name__) FlaskInstrumentor().instrument_app(app) # Mock the cleanup functions to raise exceptions with ( mock.patch("opentelemetry.instrumentation.flask.context.detach") as mock_detach, mock.patch("opentelemetry.trace.use_span") as mock_use_span, ): # Make detach raise an exception mock_detach.side_effect = RuntimeError("Detach error") # Make the span activation __exit__ raise an exception mock_span_instance = mock.Mock() mock_span_instance.is_recording.return_value = True mock_span_instance.__exit__ = mock.Mock(side_effect=RuntimeError("Exit error")) mock_use_span.return_value.__enter__ = mock.Mock(return_value=mock_span_instance) @app.route("/stream_cleanup_error") def stream_cleanup_error_endpoint(): def generate(): yield "Data" return flask.Response(flask.stream_with_context(generate())) client = app.test_client() # The request should still complete successfully despite cleanup errors response = client.get("/stream_cleanup_error") self.assertEqual(response.status_code, 200) self.assertEqual(response.data, b"Data") @skipIf( lambda: ( not __import__( "opentelemetry.instrumentation.flask", fromlist=["_IS_FLASK_31_PLUS"], )._IS_FLASK_31_PLUS ), "Flask 3.1+ streaming context cleanup requires Flask 3.1+", ) def test_flask_31_streaming_context_cleanup(self): """Test that Flask 3.1+ streaming responses have proper context cleanup to prevent token reuse""" app = flask.Flask(__name__) FlaskInstrumentor().instrument_app(app) @app.route("/stream_flask31") def flask31_streaming_endpoint(): def generate(): for chunk_idx in range(10): yield f"Chunk {chunk_idx}\n" return flask.Response(flask.stream_with_context(generate())) @app.route("/stream_normal") def stream_normal_endpoint(): def generate(): for chunk_idx in range(5): yield f"Normal {chunk_idx}\n" return flask.Response(flask.stream_with_context(generate())) client = app.test_client() # Make multiple consecutive streaming requests to test for token reuse issues for request_idx in range(20): response = client.get("/stream_flask31") self.assertEqual(response.status_code, 200) expected_data = "".join(f"Chunk {j}\n" for j in range(10)).encode() self.assertEqual(response.data, expected_data) # Test normal streaming requests as well for request_idx in range(5): response = client.get("/stream_normal") self.assertEqual(response.status_code, 200) expected_data = "".join(f"Normal {j}\n" for j in range(5)).encode() self.assertEqual(response.data, expected_data) # Mix of streaming requests for request_idx in range(10): endpoint = "/stream_flask31" if request_idx % 2 == 0 else "/stream_normal" response = client.get(endpoint) self.assertEqual(response.status_code, 200) # For Flask 3.1+, streaming context cleanup is now handled in Flask's teardown function # This ensures OpenTelemetry contexts are properly cleaned up for streaming responses # following Logfire's recommendations (see open-telemetry/opentelemetry-python#2606) # If we reach this point, the Flask 3.1+ streaming context cleanup is working def test_duplicate_teardown_request_does_not_cause_errors(self): """Test that _teardown_request can be called multiple times without errors. Flask may call teardown_request multiple times in some scenarios. After the first call, _ENVIRON_ACTIVATION_KEY and _ENVIRON_TOKEN should be cleaned up so subsequent calls are no-ops. """ app = flask.Flask(__name__) # Register the check handler BEFORE instrumenting so it runs AFTER # the instrumentation's teardown (Flask uses LIFO order). cleaned_up = {} call_count = {"teardown_calls": 0} @app.teardown_request def check_cleanup(exc): cleaned_up["activation_present"] = _ENVIRON_ACTIVATION_KEY in flask.request.environ cleaned_up["token_present"] = _ENVIRON_TOKEN in flask.request.environ FlaskInstrumentor().instrument_app(app) # Wrap the instrumentation's teardown to count calls and invoke it # a second time to simulate duplicate teardown. instrumentation_teardown = app.teardown_request_funcs[None][-1] def counting_teardown(exc): call_count["teardown_calls"] += 1 instrumentation_teardown(exc) # Call it again to simulate duplicate teardown - should not raise instrumentation_teardown(exc) app.teardown_request_funcs[None][-1] = counting_teardown @app.route("/test") def test_endpoint(): return "OK" client = app.test_client() response = client.get("/test") self.assertEqual(response.status_code, 200) # Verify the teardown was actually called self.assertGreater(call_count["teardown_calls"], 0) # Verify env keys are cleaned up after teardown self.assertFalse( cleaned_up.get("activation_present", True), "_ENVIRON_ACTIVATION_KEY should be cleaned up after teardown", ) self.assertFalse( cleaned_up.get("token_present", True), "_ENVIRON_TOKEN should be cleaned up after teardown", ) test_multithreading.py000066400000000000000000000025271527070072300377650ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import flask from werkzeug.test import Client from werkzeug.wrappers import Response from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.test.wsgitestutil import WsgiTestBase # pylint: disable=import-error from .base_test import InstrumentationTest class TestMultiThreading(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() FlaskInstrumentor().instrument() self.app = flask.Flask(__name__) self._common_initialization() def tearDown(self): super().tearDown() with self.disable_logging(): FlaskInstrumentor().uninstrument() def test_multithreaded(self): """Test that instrumentation tear down does not blow up when the request thread spawn children threads and the request context is copied to the children threads """ self.app = flask.Flask(__name__) self.app.route("/multithreaded/")(self._multithreaded_endpoint) client = Client(self.app, Response) count = 5 resp = client.get(f"/multithreaded/{count}") self.assertEqual(200, resp.status_code) # Should return the specified number of random integers self.assertEqual(count, len(resp.text.split(" "))) test_programmatic.py000066400000000000000000001316141527070072300374320ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines from timeit import default_timer from unittest.mock import Mock, patch from flask import Flask, request from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, _server_active_requests_count_attrs_new, _server_active_requests_count_attrs_old, _server_duration_attrs_new, _server_duration_attrs_old, ) from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.instrumentation.propagators import ( TraceResponsePropagator, get_global_response_propagator, set_global_response_propagator, ) from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, NumberDataPoint, ) from opentelemetry.sdk.resources import Resource from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_FLAVOR, HTTP_HOST, HTTP_METHOD, HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, HTTP_ROUTE, HTTP_SCHEME, HTTP_SERVER_NAME, HTTP_STATUS_CODE, HTTP_TARGET, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_HOST_NAME, NET_HOST_PORT, ) from opentelemetry.semconv._incubating.attributes.network_attributes import ( NETWORK_PROTOCOL_VERSION, ) from opentelemetry.semconv._incubating.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.semconv._incubating.attributes.url_attributes import ( URL_PATH, URL_SCHEME, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS, get_excluded_urls, ) # pylint: disable=import-error from .base_test import InstrumentationTest def expected_attributes(override_attributes): default_attributes = { HTTP_METHOD: "GET", HTTP_SERVER_NAME: "localhost", HTTP_SCHEME: "http", NET_HOST_PORT: 80, NET_HOST_NAME: "localhost", HTTP_HOST: "localhost", HTTP_TARGET: "/", HTTP_FLAVOR: "1.1", HTTP_STATUS_CODE: 200, } for key, val in override_attributes.items(): default_attributes[key] = val return default_attributes def expected_attributes_new(override_attributes): default_attributes = { HTTP_REQUEST_METHOD: "GET", SERVER_PORT: 80, SERVER_ADDRESS: "localhost", URL_PATH: "/hello/123", NETWORK_PROTOCOL_VERSION: "1.1", HTTP_RESPONSE_STATUS_CODE: 200, } for key, val in override_attributes.items(): default_attributes[key] = val return default_attributes _server_duration_attrs_old_copy = _server_duration_attrs_old.copy() _server_duration_attrs_old_copy.append("http.target") _server_duration_attrs_new_copy = _server_duration_attrs_new.copy() _server_duration_attrs_new_copy.append("http.route") _expected_metric_names_old = [ "http.server.active_requests", "http.server.duration", ] _expected_metric_names_new = [ "http.server.active_requests", "http.server.request.duration", ] _recommended_metrics_attrs_old = { "http.server.active_requests": _server_active_requests_count_attrs_old, "http.server.duration": _server_duration_attrs_old_copy, } _recommended_metrics_attrs_new = { "http.server.active_requests": _server_active_requests_count_attrs_new, "http.server.request.duration": _server_duration_attrs_new_copy, } _server_active_requests_count_attrs_both = _server_active_requests_count_attrs_old _server_active_requests_count_attrs_both.extend(_server_active_requests_count_attrs_new) _recommended_metrics_attrs_both = { "http.server.active_requests": _server_active_requests_count_attrs_both, "http.server.duration": _server_duration_attrs_old_copy, "http.server.request.duration": _server_duration_attrs_new_copy, } SCOPE = "opentelemetry.instrumentation.flask" # pylint: disable=too-many-public-methods class TestProgrammatic(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() test_name = "" if hasattr(self, "_testMethodName"): test_name = self._testMethodName sem_conv_mode = "default" if "new_semconv" in test_name: sem_conv_mode = "http" elif "both_semconv" in test_name: sem_conv_mode = "http/dup" self.env_patch = patch.dict( "os.environ", { "OTEL_PYTHON_FLASK_EXCLUDED_URLS": "http://localhost/env_excluded_arg/123,env_excluded_noarg,env_excluded_arg/789", OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) _OpenTelemetrySemanticConventionStability._initialized = False self.env_patch.start() self.exclude_patch = patch( "opentelemetry.instrumentation.flask._excluded_urls_from_env", get_excluded_urls("FLASK"), ) self.exclude_patch.start() self.app = Flask(__name__) FlaskInstrumentor().instrument_app(self.app) self._common_initialization() def tearDown(self): super().tearDown() self.env_patch.stop() self.exclude_patch.stop() with self.disable_logging(): FlaskInstrumentor().uninstrument_app(self.app) def test_instrument_app_and_instrument(self): FlaskInstrumentor().instrument() resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) self.assertEqual([b"Hello: 123"], list(resp.response)) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) FlaskInstrumentor().uninstrument() def test_uninstrument_app(self): resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) self.assertEqual([b"Hello: 123"], list(resp.response)) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) FlaskInstrumentor().uninstrument_app(self.app) resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) self.assertEqual([b"Hello: 123"], list(resp.response)) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) def test_uninstrument_app_after_instrument(self): FlaskInstrumentor().instrument() FlaskInstrumentor().uninstrument_app(self.app) resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) self.assertEqual([b"Hello: 123"], list(resp.response)) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) FlaskInstrumentor().uninstrument() # pylint: disable=no-member def test_only_strings_in_environ(self): """ Some WSGI servers (such as Gunicorn) expect keys in the environ object to be strings OpenTelemetry should adhere to this convention. """ nonstring_keys = set() def assert_environ(): for key in request.environ: if not isinstance(key, str): nonstring_keys.add(key) return "hi" self.app.route("/assert_environ")(assert_environ) self.client.get("/assert_environ") self.assertEqual(nonstring_keys, set()) def test_simple(self): expected_attrs = expected_attributes( { HTTP_TARGET: "/hello/123", HTTP_ROUTE: "/hello/", } ) self.client.get("/hello/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, "GET /hello/") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) def test_simple_new_semconv(self): expected_attrs = expected_attributes_new( { HTTP_ROUTE: "/hello/", URL_SCHEME: "http", } ) self.client.get("/hello/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, "GET /hello/") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) def test_simple_both_semconv(self): expected_attrs = expected_attributes( { HTTP_TARGET: "/hello/123", HTTP_ROUTE: "/hello/", } ) expected_attrs.update( expected_attributes_new( { HTTP_ROUTE: "/hello/", URL_SCHEME: "http", } ) ) self.client.get("/hello/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, "GET /hello/") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) def test_trace_response(self): orig = get_global_response_propagator() set_global_response_propagator(TraceResponsePropagator()) response = self.client.get("/hello/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertTraceResponseHeaderMatchesSpan( response.headers, span_list[0], ) set_global_response_propagator(orig) def test_not_recording(self): mock_tracer = Mock() mock_span = Mock() mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span with patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer self.client.get("/hello/123") self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) def test_404(self): expected_attrs = expected_attributes( { HTTP_METHOD: "POST", HTTP_TARGET: "/bye", HTTP_STATUS_CODE: 404, } ) resp = self.client.post("/bye") self.assertEqual(404, resp.status_code) resp.close() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, "POST /bye") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) def test_404_new_semconv(self): expected_attrs = expected_attributes_new( { HTTP_REQUEST_METHOD: "POST", HTTP_RESPONSE_STATUS_CODE: 404, URL_PATH: "/bye", URL_SCHEME: "http", } ) resp = self.client.post("/bye") self.assertEqual(404, resp.status_code) resp.close() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, "POST /bye") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) def test_404_both_semconv(self): expected_attrs = expected_attributes( { HTTP_METHOD: "POST", HTTP_TARGET: "/bye", HTTP_STATUS_CODE: 404, } ) expected_attrs.update( expected_attributes_new( { HTTP_REQUEST_METHOD: "POST", HTTP_RESPONSE_STATUS_CODE: 404, URL_PATH: "/bye", URL_SCHEME: "http", } ) ) resp = self.client.post("/bye") self.assertEqual(404, resp.status_code) resp.close() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, "POST /bye") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) def test_internal_error(self): expected_attrs = expected_attributes( { HTTP_TARGET: "/hello/500", HTTP_ROUTE: "/hello/", HTTP_STATUS_CODE: 500, } ) resp = self.client.get("/hello/500") self.assertEqual(500, resp.status_code) resp.close() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, "GET /hello/") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) def test_internal_error_new_semconv(self): expected_attrs = expected_attributes_new( { URL_PATH: "/hello/500", HTTP_ROUTE: "/hello/", HTTP_RESPONSE_STATUS_CODE: 500, ERROR_TYPE: "500", URL_SCHEME: "http", } ) resp = self.client.get("/hello/500") self.assertEqual(500, resp.status_code) resp.close() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, "GET /hello/") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) def test_internal_error_both_semconv(self): expected_attrs = expected_attributes( { HTTP_TARGET: "/hello/500", HTTP_ROUTE: "/hello/", HTTP_STATUS_CODE: 500, } ) expected_attrs.update( expected_attributes_new( { URL_PATH: "/hello/500", HTTP_RESPONSE_STATUS_CODE: 500, ERROR_TYPE: "500", URL_SCHEME: "http", } ) ) resp = self.client.get("/hello/500") self.assertEqual(500, resp.status_code) resp.close() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, "GET /hello/") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) def test_exclude_lists_from_env(self): self.client.get("/env_excluded_arg/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) self.client.get("/env_excluded_arg/125") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.client.get("/env_excluded_noarg") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.client.get("/env_excluded_noarg2") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) def test_exclude_lists_from_explicit(self): excluded_urls = "http://localhost/explicit_excluded_arg/123,explicit_excluded_noarg" app = Flask(__name__) FlaskInstrumentor().instrument_app(app, excluded_urls=excluded_urls) client = app.test_client() client.get("/explicit_excluded_arg/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) client.get("/explicit_excluded_arg/125") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) client.get("/explicit_excluded_noarg") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) client.get("/explicit_excluded_noarg2") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) def test_flask_metrics(self): start = default_timer() self.client.get("/hello/123") self.client.get("/hello/321") self.client.get("/hello/756") duration = max(round((default_timer() - start) * 1000), 0) metrics = self.get_sorted_metrics(SCOPE) number_data_point_seen = False histogram_data_point_seen = False self.assertTrue(len(metrics) != 0) for metric in metrics: self.assertIn(metric.name, _expected_metric_names_old) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 3) self.assertAlmostEqual(duration, point.sum, delta=10) histogram_data_point_seen = True if isinstance(point, NumberDataPoint): number_data_point_seen = True for attr in point.attributes: self.assertIn( attr, _recommended_metrics_attrs_old[metric.name], ) self.assertTrue(number_data_point_seen and histogram_data_point_seen) def test_flask_metrics_new_semconv(self): start = default_timer() self.client.get("/hello/123") self.client.get("/hello/321") self.client.get("/hello/756") duration_s = max(default_timer() - start, 0) metrics = self.get_sorted_metrics(SCOPE) number_data_point_seen = False histogram_data_point_seen = False self.assertTrue(len(metrics) != 0) for metric in metrics: self.assertIn(metric.name, _expected_metric_names_new) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 3) self.assertAlmostEqual(duration_s, point.sum, places=1) self.assertEqual( point.explicit_bounds, HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) histogram_data_point_seen = True if isinstance(point, NumberDataPoint): number_data_point_seen = True for attr in point.attributes: self.assertIn( attr, _recommended_metrics_attrs_new[metric.name], ) self.assertTrue(number_data_point_seen and histogram_data_point_seen) def test_flask_metric_values(self): start = default_timer() self.client.post("/hello/756") self.client.post("/hello/756") self.client.post("/hello/756") duration = max(round((default_timer() - start) * 1000), 0) metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 3) self.assertAlmostEqual(duration, point.sum, delta=10) if isinstance(point, NumberDataPoint): self.assertEqual(point.value, 0) def _assert_basic_metric( self, expected_duration_attributes, expected_requests_count_attributes, expected_histogram_explicit_bounds=None, ): # pylint: disable=too-many-nested-blocks metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertDictEqual( expected_duration_attributes, dict(point.attributes), ) if expected_histogram_explicit_bounds is not None: self.assertEqual( expected_histogram_explicit_bounds, point.explicit_bounds, ) self.assertEqual(point.count, 1) elif isinstance(point, NumberDataPoint): self.assertDictEqual( expected_requests_count_attributes, dict(point.attributes), ) self.assertEqual(point.value, 0) def test_basic_metric_success(self): self.client.get("/hello/756") expected_duration_attributes = { "http.method": "GET", "http.target": "/hello/", "http.host": "localhost", "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "localhost", "net.host.port": 80, "http.status_code": 200, "net.host.name": "localhost", } expected_requests_count_attributes = { "http.method": "GET", "http.host": "localhost", "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "localhost", } self._assert_basic_metric( expected_duration_attributes, expected_requests_count_attributes, ) def test_basic_metric_success_new_semconv(self): self.client.get("/hello/756") expected_duration_attributes = { "http.request.method": "GET", "url.scheme": "http", "http.route": "/hello/", "network.protocol.version": "1.1", "http.response.status_code": 200, } expected_requests_count_attributes = { "http.request.method": "GET", "url.scheme": "http", } self._assert_basic_metric( expected_duration_attributes, expected_requests_count_attributes, expected_histogram_explicit_bounds=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) def test_basic_metric_nonstandard_http_method_success(self): self.client.open("/hello/756", method="NONSTANDARD") expected_duration_attributes = { "http.method": "_OTHER", "http.host": "localhost", "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "localhost", "net.host.port": 80, "http.status_code": 405, "net.host.name": "localhost", } expected_requests_count_attributes = { "http.method": "_OTHER", "http.host": "localhost", "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "localhost", } self._assert_basic_metric( expected_duration_attributes, expected_requests_count_attributes, ) def test_basic_metric_nonstandard_http_method_success_new_semconv(self): self.client.open("/hello/756", method="NONSTANDARD") expected_duration_attributes = { "http.request.method": "_OTHER", "url.scheme": "http", "network.protocol.version": "1.1", "http.response.status_code": 405, } expected_requests_count_attributes = { "http.request.method": "_OTHER", "url.scheme": "http", } self._assert_basic_metric( expected_duration_attributes, expected_requests_count_attributes, expected_histogram_explicit_bounds=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) @patch.dict( "os.environ", { OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS: "1", }, ) def test_basic_metric_nonstandard_http_method_allowed_success_new_semconv( self, ): self.client.open("/hello/756", method="NONSTANDARD") expected_duration_attributes = { "http.request.method": "NONSTANDARD", "url.scheme": "http", "network.protocol.version": "1.1", "http.response.status_code": 405, } expected_requests_count_attributes = { "http.request.method": "NONSTANDARD", "url.scheme": "http", } self._assert_basic_metric( expected_duration_attributes, expected_requests_count_attributes, expected_histogram_explicit_bounds=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) def test_metric_uninstrument(self): self.client.delete("/hello/756") FlaskInstrumentor().uninstrument_app(self.app) self.client.delete("/hello/756") metrics_list = self.memory_metrics_reader.get_metrics_data() for resource_metric in metrics_list.resource_metrics: for scope_metric in resource_metric.scope_metrics: for metric in scope_metric.metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) def test_flask_metrics_excluded_urls(self): start = default_timer() self.client.get("/env_excluded_arg/123") self.client.get("/env_excluded_noarg") self.client.get("/env_excluded_arg/789") duration = max(round((default_timer() - start) * 1000), 0) metrics_list = self.memory_metrics_reader.get_metrics_data() number_data_point_seen = False histogram_data_point_seen = False self.assertTrue(len(metrics_list.resource_metrics) != 0) for resource_metric in metrics_list.resource_metrics: self.assertTrue(len(resource_metric.scope_metrics) != 0) for scope_metric in resource_metric.scope_metrics: self.assertTrue(len(scope_metric.metrics) != 0) for metric in scope_metric.metrics: self.assertIn(metric.name, _expected_metric_names_old) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 0) self.assertAlmostEqual(duration, point.sum, delta=10) histogram_data_point_seen = True if isinstance(point, NumberDataPoint): number_data_point_seen = True for attr in point.attributes: self.assertIn( attr, _recommended_metrics_attrs_old[metric.name], ) self.assertTrue(number_data_point_seen) self.assertFalse(histogram_data_point_seen) def test_flask_metrics_excluded_urls_new_semconv(self): start = default_timer() self.client.get("/env_excluded_arg/123") self.client.get("/env_excluded_noarg") self.client.get("/env_excluded_arg/789") duration_s = max(default_timer() - start, 0) metrics_list = self.memory_metrics_reader.get_metrics_data() number_data_point_seen = False histogram_data_point_seen = False self.assertTrue(len(metrics_list.resource_metrics) != 0) for resource_metric in metrics_list.resource_metrics: self.assertTrue(len(resource_metric.scope_metrics) != 0) for scope_metric in resource_metric.scope_metrics: self.assertTrue(len(scope_metric.metrics) != 0) for metric in scope_metric.metrics: self.assertIn(metric.name, _expected_metric_names_new) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 0) self.assertAlmostEqual(duration_s, point.sum, places=1) self.assertEqual( point.explicit_bounds, HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) histogram_data_point_seen = True if isinstance(point, NumberDataPoint): number_data_point_seen = True for attr in point.attributes: self.assertIn( attr, _recommended_metrics_attrs_new[metric.name], ) self.assertTrue(number_data_point_seen) self.assertFalse(histogram_data_point_seen) def test_duration_histogram_old_record_with_context(self): with patch("opentelemetry.trace.set_span_in_context") as mock_set_span: self.client.get("/hello/123") # Verify that trace.set_span_in_context was called for metrics exemplar context # with same trace, span ID as trace mock_set_span.assert_called() call_args = mock_set_span.call_args self.assertEqual(len(call_args[0]), 1) span_arg = call_args[0][0] self.assertIsNotNone(span_arg) finished_spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(finished_spans), 1) finished_span = finished_spans[0] self.assertEqual(span_arg.context.trace_id, finished_span.context.trace_id) self.assertEqual(span_arg.context.span_id, finished_span.context.span_id) def test_duration_histogram_new_record_with_context_new_semconv(self): with patch("opentelemetry.trace.set_span_in_context") as mock_set_span: self.client.get("/hello/123") # Verify that trace.set_span_in_context was called for metrics exemplar context # with same trace, span ID as trace mock_set_span.assert_called() call_args = mock_set_span.call_args self.assertEqual(len(call_args[0]), 1) span_arg = call_args[0][0] self.assertIsNotNone(span_arg) finished_spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(finished_spans), 1) finished_span = finished_spans[0] self.assertEqual(span_arg.context.trace_id, finished_span.context.trace_id) self.assertEqual(span_arg.context.span_id, finished_span.context.span_id) def test_active_requests_counter_decremented_on_error(self): """Regression test for https://github.com/open-telemetry/opentelemetry-python-contrib/issues/4431. http.server.active_requests must be decremented even when the view raises an exception, so it does not permanently leak. """ self.client.get("/hello/123") resp = self.client.get("/hello/500") self.assertEqual(500, resp.status_code) metrics_list = self.memory_metrics_reader.get_metrics_data() active_requests_value = None for resource_metric in metrics_list.resource_metrics: for scope_metric in resource_metric.scope_metrics: for metric in scope_metric.metrics: if metric.name != "http.server.active_requests": continue points = [p.value for p in metric.data.data_points if isinstance(p, NumberDataPoint)] if points: active_requests_value = points[-1] self.assertIsNotNone( active_requests_value, "http.server.active_requests metric not found", ) self.assertEqual( 0, active_requests_value, f"active_requests counter leaked: expected 0 but got {active_requests_value}", ) class TestProgrammaticHooks(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() hook_headers = ( "hook_attr", "hello otel", ) def request_hook_test(span, environ): span.update_name("name from hook") def response_hook_test(span, environ, response_headers): span.set_attribute("hook_attr", "hello world") response_headers.append(hook_headers) self.app = Flask(__name__) FlaskInstrumentor().instrument_app( self.app, request_hook=request_hook_test, response_hook=response_hook_test, ) self._common_initialization() def tearDown(self): super().tearDown() with self.disable_logging(): FlaskInstrumentor().uninstrument_app(self.app) def test_hooks(self): expected_attrs = expected_attributes( { "http.target": "/hello/123", "http.route": "/hello/", "hook_attr": "hello world", } ) resp = self.client.get("/hello/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, "name from hook") self.assertEqual(span_list[0].attributes, expected_attrs) self.assertEqual(resp.headers["hook_attr"], "hello otel") class TestProgrammaticHooksWithoutApp(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() hook_headers = ( "hook_attr", "hello otel without app", ) def request_hook_test(span, environ): span.update_name("without app") def response_hook_test(span, environ, response_headers): span.set_attribute("hook_attr", "hello world without app") response_headers.append(hook_headers) FlaskInstrumentor().instrument(request_hook=request_hook_test, response_hook=response_hook_test) # pylint: disable=import-outside-toplevel,reimported,redefined-outer-name from flask import Flask # noqa: PLC0415 self.app = Flask(__name__) self._common_initialization() def tearDown(self): super().tearDown() with self.disable_logging(): FlaskInstrumentor().uninstrument() def test_no_app_hooks(self): expected_attrs = expected_attributes( { "http.target": "/hello/123", "http.route": "/hello/", "hook_attr": "hello world without app", } ) resp = self.client.get("/hello/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, "without app") self.assertEqual(span_list[0].attributes, expected_attrs) self.assertEqual(resp.headers["hook_attr"], "hello otel without app") class TestProgrammaticCustomTracerProvider(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() resource = Resource.create({"service.name": "flask-api"}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result self.memory_exporter = exporter self.app = Flask(__name__) FlaskInstrumentor().instrument_app(self.app, tracer_provider=tracer_provider) self._common_initialization() def tearDown(self): super().tearDown() with self.disable_logging(): FlaskInstrumentor().uninstrument_app(self.app) def test_custom_span_name(self): self.client.get("/hello/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].resource.attributes["service.name"], "flask-api") class TestProgrammaticCustomTracerProviderWithoutApp(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() resource = Resource.create({"service.name": "flask-api-no-app"}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result self.memory_exporter = exporter FlaskInstrumentor().instrument(tracer_provider=tracer_provider) # pylint: disable=import-outside-toplevel,reimported,redefined-outer-name from flask import Flask # noqa: PLC0415 self.app = Flask(__name__) self._common_initialization() def tearDown(self): super().tearDown() with self.disable_logging(): FlaskInstrumentor().uninstrument() def test_custom_span_name(self): self.client.get("/hello/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual( span_list[0].resource.attributes["service.name"], "flask-api-no-app", ) class TestProgrammaticWrappedWithOtherFramework(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() self.app = Flask(__name__) self.app.wsgi_app = OpenTelemetryMiddleware(self.app.wsgi_app) FlaskInstrumentor().instrument_app(self.app) self._common_initialization() def tearDown(self) -> None: super().tearDown() with self.disable_logging(): FlaskInstrumentor().uninstrument_app(self.app) def test_mark_span_internal_in_presence_of_span_from_other_framework(self): resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) resp.get_data() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(trace.SpanKind.INTERNAL, span_list[0].kind) self.assertEqual(trace.SpanKind.SERVER, span_list[1].kind) self.assertEqual(span_list[0].parent.span_id, span_list[1].context.span_id) @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", }, ) class TestCustomRequestResponseHeaders(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() self.app = Flask(__name__) FlaskInstrumentor().instrument_app(self.app) self._common_initialization() def tearDown(self): super().tearDown() with self.disable_logging(): FlaskInstrumentor().uninstrument_app(self.app) def test_custom_request_header_added_in_server_span(self): headers = { "Custom-Test-Header-1": "Test Value 1", "Custom-Test-Header-2": "TestValue2,TestValue3", "Regex-Test-Header-1": "Regex Test Value 1", "regex-test-header-2": "RegexTestValue2,RegexTestValue3", "My-Secret-Header": "My Secret Value", } resp = self.client.get("/hello/123", headers=headers) self.assertEqual(200, resp.status_code) span = self.memory_exporter.get_finished_spans()[0] expected = { "http.request.header.custom_test_header_1": ("Test Value 1",), "http.request.header.custom_test_header_2": ("TestValue2,TestValue3",), "http.request.header.regex_test_header_1": ("Regex Test Value 1",), "http.request.header.regex_test_header_2": ("RegexTestValue2,RegexTestValue3",), "http.request.header.my_secret_header": ("[REDACTED]",), } self.assertEqual(span.kind, trace.SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) def test_repeat_custom_request_header_added_in_server_span(self): headers = [ ("Custom-Test-Header-1", "Test Value 1"), ("Custom-Test-Header-1", "Test Value 2"), ] resp = self.client.get("/hello/123", headers=headers) self.assertEqual(200, resp.status_code) span = self.memory_exporter.get_finished_spans()[0] expected = { "http.request.header.custom_test_header_1": ("Test Value 1, Test Value 2",), } self.assertEqual(span.kind, trace.SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) def test_custom_request_header_not_added_in_internal_span(self): tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER): headers = { "Custom-Test-Header-1": "Test Value 1", "Custom-Test-Header-2": "TestValue2,TestValue3", "Regex-Test-Header-1": "Regex Test Value 1", "regex-test-header-2": "RegexTestValue2,RegexTestValue3", "My-Secret-Header": "My Secret Value", } resp = self.client.get("/hello/123", headers=headers) self.assertEqual(200, resp.status_code) span = self.memory_exporter.get_finished_spans()[0] not_expected = { "http.request.header.custom_test_header_1": ("Test Value 1",), "http.request.header.custom_test_header_2": ("TestValue2,TestValue3",), "http.request.header.regex_test_header_1": ("Regex Test Value 1",), "http.request.header.regex_test_header_2": ("RegexTestValue2,RegexTestValue3",), "http.request.header.my_secret_header": ("[REDACTED]",), } self.assertEqual(span.kind, trace.SpanKind.INTERNAL) for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) def test_custom_response_header_added_in_server_span(self): resp = self.client.get("/test_custom_response_headers") self.assertEqual(resp.status_code, 200) span = self.memory_exporter.get_finished_spans()[0] expected = { "http.response.header.content_type": ("text/plain; charset=utf-8",), "http.response.header.content_length": ("13",), "http.response.header.my_custom_header": ("my-custom-value-1,my-custom-header-2",), "http.response.header.my_custom_regex_header_1": ("my-custom-regex-value-1,my-custom-regex-value-2",), "http.response.header.my_custom_regex_header_2": ("my-custom-regex-value-3,my-custom-regex-value-4",), "http.response.header.my_secret_header": ("[REDACTED]",), } self.assertEqual(span.kind, trace.SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) def test_repeat_custom_response_header_added_in_server_span(self): resp = self.client.get("/test_repeat_custom_response_headers") self.assertEqual(resp.status_code, 200) span = self.memory_exporter.get_finished_spans()[0] expected = { "http.response.header.content_type": ("text/plain; charset=utf-8",), "http.response.header.my_custom_header": ("my-custom-value-1,my-custom-header-2",), } self.assertEqual(span.kind, trace.SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) def test_custom_response_header_not_added_in_internal_span(self): tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER): resp = self.client.get("/test_custom_response_headers") self.assertEqual(resp.status_code, 200) span = self.memory_exporter.get_finished_spans()[0] not_expected = { "http.response.header.content_type": ("text/plain; charset=utf-8",), "http.response.header.content_length": ("13",), "http.response.header.my_custom_header": ("my-custom-value-1,my-custom-header-2",), "http.response.header.my_custom_regex_header_1": ("my-custom-regex-value-1,my-custom-regex-value-2",), "http.response.header.my_custom_regex_header_2": ("my-custom-regex-value-3,my-custom-regex-value-4",), "http.response.header.my_secret_header": ("[REDACTED]",), } self.assertEqual(span.kind, trace.SpanKind.INTERNAL) for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) test_sqlcommenter.py000066400000000000000000000043431527070072300374540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-flask/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import flask from werkzeug.test import Client from werkzeug.wrappers import Response from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.test.wsgitestutil import WsgiTestBase # pylint: disable=import-error from .base_test import InstrumentationTest class TestSQLCommenter(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() FlaskInstrumentor().instrument() self.app = flask.Flask(__name__) self._common_initialization() def tearDown(self): super().tearDown() with self.disable_logging(): FlaskInstrumentor().uninstrument() def test_sqlcommenter_enabled_default(self): self.app = flask.Flask(__name__) self.app.route("/sqlcommenter")(self._sqlcommenter_endpoint) client = Client(self.app, Response) resp = client.get("/sqlcommenter") self.assertEqual(200, resp.status_code) self.assertRegex( list(resp.response)[0].strip(), b'{"controller":"_sqlcommenter_endpoint","framework":"flask:(.*)","route":"/sqlcommenter"}', ) def test_sqlcommenter_enabled_with_configurations(self): FlaskInstrumentor().uninstrument() FlaskInstrumentor().instrument(enable_commenter=True, commenter_options={"route": False}) self.app = flask.Flask(__name__) self.app.route("/sqlcommenter")(self._sqlcommenter_endpoint) client = Client(self.app, Response) resp = client.get("/sqlcommenter") self.assertEqual(200, resp.status_code) self.assertRegex( list(resp.response)[0].strip(), b'{"controller":"_sqlcommenter_endpoint","framework":"flask:(.*)"}', ) def test_sqlcommenter_disabled(self): FlaskInstrumentor().uninstrument() FlaskInstrumentor().instrument(enable_commenter=False) self.app = flask.Flask(__name__) self.app.route("/sqlcommenter")(self._sqlcommenter_endpoint) client = Client(self.app, Response) resp = client.get("/sqlcommenter") self.assertEqual(200, resp.status_code) self.assertEqual(list(resp.response)[0].strip(), b"{}") opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/000077500000000000000000000000001527070072300320765ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/LICENSE000066400000000000000000000261351527070072300331120ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/README.rst000066400000000000000000000011541527070072300335660ustar00rootroot00000000000000OpenTelemetry gRPC Integration ============================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-grpc.svg :target: https://pypi.org/project/opentelemetry-instrumentation-grpc/ Client and server interceptors for `gRPC Python`_. .. _gRPC Python: https://grpc.github.io/grpc/python/grpc.html Installation ------------ :: pip install opentelemetry-instrumentation-grpc References ---------- * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_pyproject.toml000066400000000000000000000035341527070072300347400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-grpc" dynamic = ["version"] description = "OpenTelemetry gRPC instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [ "grpcio >= 1.42.0", ] [project.entry-points.opentelemetry_instrumentor] grpc_client = "opentelemetry.instrumentation.grpc:GrpcInstrumentorClient" grpc_server = "opentelemetry.instrumentation.grpc:GrpcInstrumentorServer" grpc_aio_client = "opentelemetry.instrumentation.grpc:GrpcAioInstrumentorClient" grpc_aio_server = "opentelemetry.instrumentation.grpc:GrpcAioInstrumentorServer" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-grpc" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/grpc/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/src/000077500000000000000000000000001527070072300326655ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300355025ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/srcinstrumentation/000077500000000000000000000000001527070072300407455ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetrygrpc/000077500000000000000000000000001527070072300417005ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation__init__.py000066400000000000000000000506051527070072300440170ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint:disable=no-name-in-module # pylint:disable=relative-beyond-top-level # pylint:disable=import-error # pylint:disable=no-self-use """ Usage Client ------------ .. code-block:: python import logging import grpc from opentelemetry import trace from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleSpanProcessor, ) try: from .gen import helloworld_pb2, helloworld_pb2_grpc except ImportError: from gen import helloworld_pb2, helloworld_pb2_grpc trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) grpc_client_instrumentor = GrpcInstrumentorClient() grpc_client_instrumentor.instrument() def run(): with grpc.insecure_channel("localhost:50051") as channel: stub = helloworld_pb2_grpc.GreeterStub(channel) response = stub.SayHello(helloworld_pb2.HelloRequest(name="YOU")) print("Greeter client received: " + response.message) if __name__ == "__main__": logging.basicConfig() run() Usage Server ------------ .. code-block:: python import logging from concurrent import futures import grpc from opentelemetry import trace from opentelemetry.instrumentation.grpc import GrpcInstrumentorServer from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleSpanProcessor, ) try: from .gen import helloworld_pb2, helloworld_pb2_grpc except ImportError: from gen import helloworld_pb2, helloworld_pb2_grpc trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) grpc_server_instrumentor = GrpcInstrumentorServer() grpc_server_instrumentor.instrument() class Greeter(helloworld_pb2_grpc.GreeterServicer): def SayHello(self, request, context): return helloworld_pb2.HelloReply(message="Hello, %s!" % request.name) def serve(): server = grpc.server(futures.ThreadPoolExecutor()) helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server) server.add_insecure_port("[::]:50051") server.start() server.wait_for_termination() if __name__ == "__main__": logging.basicConfig() serve() You can also add the interceptor manually, rather than using :py:class:`~opentelemetry.instrumentation.grpc.GrpcInstrumentorServer`: .. code-block:: python from opentelemetry.instrumentation.grpc import server_interceptor server = grpc.server(futures.ThreadPoolExecutor(), interceptors=[server_interceptor()]) Usage Aio Client ---------------- .. code-block:: python import logging import asyncio import grpc from opentelemetry import trace from opentelemetry.instrumentation.grpc import GrpcAioInstrumentorClient from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleSpanProcessor, ) try: from .gen import helloworld_pb2, helloworld_pb2_grpc except ImportError: from gen import helloworld_pb2, helloworld_pb2_grpc trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) grpc_client_instrumentor = GrpcAioInstrumentorClient() grpc_client_instrumentor.instrument() async def run(): async with grpc.aio.insecure_channel("localhost:50051") as channel: stub = helloworld_pb2_grpc.GreeterStub(channel) response = await stub.SayHello(helloworld_pb2.HelloRequest(name="YOU")) print("Greeter client received: " + response.message) if __name__ == "__main__": logging.basicConfig() asyncio.run(run()) You can also add the interceptor manually, rather than using :py:class:`~opentelemetry.instrumentation.grpc.GrpcAioInstrumentorClient`: .. code-block:: python from opentelemetry.instrumentation.grpc import aio_client_interceptors async with grpc.aio.insecure_channel("localhost:50051", interceptors=aio_client_interceptors()) as channel: Usage Aio Server ---------------- .. code-block:: python import logging import asyncio import grpc from opentelemetry import trace from opentelemetry.instrumentation.grpc import GrpcAioInstrumentorServer from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleSpanProcessor, ) try: from .gen import helloworld_pb2, helloworld_pb2_grpc except ImportError: from gen import helloworld_pb2, helloworld_pb2_grpc trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) grpc_server_instrumentor = GrpcAioInstrumentorServer() grpc_server_instrumentor.instrument() class Greeter(helloworld_pb2_grpc.GreeterServicer): async def SayHello(self, request, context): return helloworld_pb2.HelloReply(message="Hello, %s!" % request.name) async def serve(): server = grpc.aio.server() helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server) server.add_insecure_port("[::]:50051") await server.start() await server.wait_for_termination() if __name__ == "__main__": logging.basicConfig() asyncio.run(serve()) You can also add the interceptor manually, rather than using :py:class:`~opentelemetry.instrumentation.grpc.GrpcAioInstrumentorServer`: .. code-block:: python from opentelemetry.instrumentation.grpc import aio_server_interceptor server = grpc.aio.server(interceptors=[aio_server_interceptor()]) Filters ------- If you prefer to filter specific requests to be instrumented, you can specify the condition by assigning filters to instrumentors. You can write a global server instrumentor as follows: .. code-block:: from opentelemetry.instrumentation.grpc import filters, GrpcInstrumentorServer grpc_server_instrumentor = GrpcInstrumentorServer( filter_ = filters.any_of( filters.method_name("SimpleMethod"), filters.method_name("ComplexMethod"), ) ) grpc_server_instrumentor.instrument() You can also use the filters directly on the provided interceptors: .. code-block:: import grpc from concurrent import futures from opentelemetry.instrumentation.grpc import filters from opentelemetry.instrumentation.grpc import server_interceptor my_interceptor = server_interceptor( filter_ = filters.negate(filters.method_name("TestMethod")) ) server = grpc.server(futures.ThreadPoolExecutor(), interceptors = [my_interceptor]) ``filter_`` option also applies to both global and manual client intrumentors. Environment variable -------------------- If you'd like to exclude specific services for the instrumentations, you can use ``OTEL_PYTHON_GRPC_EXCLUDED_SERVICES`` environment variables. For example, if you assign ``"GRPCTestServer,GRPCHealthServer"`` to the variable, then the global interceptor automatically adds the filters to exclude requests to services ``GRPCTestServer`` and ``GRPCHealthServer``. """ import os from collections.abc import Callable, Collection import grpc # pylint:disable=import-self from wrapt import wrap_function_wrapper as _wrap from opentelemetry import trace from opentelemetry.instrumentation.grpc.filters import ( any_of, negate, service_name, ) from opentelemetry.instrumentation.grpc.grpcext import intercept_channel from opentelemetry.instrumentation.grpc.package import _instruments from opentelemetry.instrumentation.grpc.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap # pylint:disable=import-outside-toplevel # pylint:disable=import-self # pylint:disable=unused-argument class GrpcInstrumentorServer(BaseInstrumentor): """ Globally instrument the grpc server. Usage:: grpc_server_instrumentor = GrpcInstrumentorServer() grpc_server_instrumentor.instrument() If you want to add a filter that only intercept requests to match the condition, pass ``filter_`` to GrpcInstrumentorServer. grpc_server_instrumentor = GrpcInstrumentorServer( filter_=filters.method_prefix("SimpleMethod")) grpc_server_instrumentor.instrument() """ # pylint:disable=redefined-outer-name def __init__(self, filter_=None): excluded_service_filter = _excluded_service_filter() if excluded_service_filter is not None: if filter_ is None: filter_ = excluded_service_filter else: filter_ = any_of(filter_, excluded_service_filter) self._filter = filter_ def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): self._original_func = grpc.server tracer_provider = kwargs.get("tracer_provider") def server(*args, **kwargs): if kwargs.get("interceptors"): kwargs["interceptors"] = list(kwargs["interceptors"]) # add our interceptor as the first kwargs["interceptors"].insert( 0, server_interceptor(tracer_provider=tracer_provider, filter_=self._filter), ) else: kwargs["interceptors"] = [server_interceptor(tracer_provider=tracer_provider, filter_=self._filter)] return self._original_func(*args, **kwargs) grpc.server = server def _uninstrument(self, **kwargs): grpc.server = self._original_func class GrpcAioInstrumentorServer(BaseInstrumentor): """ Globally instrument the grpc.aio server. Usage:: grpc_aio_server_instrumentor = GrpcAioInstrumentorServer() grpc_aio_server_instrumentor.instrument() """ # pylint:disable=redefined-outer-name def __init__(self, filter_=None): excluded_service_filter = _excluded_service_filter() if excluded_service_filter is not None: if filter_ is None: filter_ = excluded_service_filter else: filter_ = any_of(filter_, excluded_service_filter) self._filter = filter_ def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): self._original_func = grpc.aio.server tracer_provider = kwargs.get("tracer_provider") def server(*args, **kwargs): if kwargs.get("interceptors"): kwargs["interceptors"] = list(kwargs["interceptors"]) # add our interceptor as the first kwargs["interceptors"].insert( 0, aio_server_interceptor(tracer_provider=tracer_provider, filter_=self._filter), ) else: kwargs["interceptors"] = [aio_server_interceptor(tracer_provider=tracer_provider, filter_=self._filter)] return self._original_func(*args, **kwargs) grpc.aio.server = server def _uninstrument(self, **kwargs): grpc.aio.server = self._original_func class GrpcInstrumentorClient(BaseInstrumentor): """ Globally instrument the grpc client Usage:: grpc_client_instrumentor = GrpcInstrumentorClient() grpc_client_instrumentor.instrument() If you want to add a filter that only intercept requests to match the condition, pass ``filter_`` option to GrpcInstrumentorClient. grpc_client_instrumentor = GrpcInstrumentorClient( filter_=filters.negate(filters.health_check()) ) grpc_client_instrumentor.instrument() """ def __init__(self, filter_=None): excluded_service_filter = _excluded_service_filter() if excluded_service_filter is not None: if filter_ is None: filter_ = excluded_service_filter else: filter_ = any_of(filter_, excluded_service_filter) self._filter = filter_ self._request_hook = None self._response_hook = None # Figures out which channel type we need to wrap def _which_channel(self, kwargs): # handle legacy argument if "channel_type" in kwargs: if kwargs.get("channel_type") == "secure": return ("secure_channel",) return ("insecure_channel",) # handle modern arguments types = [] for ctype in ("secure_channel", "insecure_channel"): if kwargs.get(ctype, True): types.append(ctype) return tuple(types) def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): self._request_hook = kwargs.get("request_hook") self._response_hook = kwargs.get("response_hook") for ctype in self._which_channel(kwargs): _wrap( "grpc", ctype, self.wrapper_fn, ) def _uninstrument(self, **kwargs): for ctype in self._which_channel(kwargs): unwrap(grpc, ctype) def wrapper_fn(self, original_func, instance, args, kwargs): channel = original_func(*args, **kwargs) tracer_provider = kwargs.get("tracer_provider") request_hook = self._request_hook response_hook = self._response_hook return intercept_channel( channel, client_interceptor( tracer_provider=tracer_provider, filter_=self._filter, request_hook=request_hook, response_hook=response_hook, ), ) class GrpcAioInstrumentorClient(BaseInstrumentor): """ Globally instrument the grpc.aio client. Usage:: grpc_aio_client_instrumentor = GrpcAioInstrumentorClient() grpc_aio_client_instrumentor.instrument() """ # pylint:disable=redefined-outer-name def __init__(self, filter_=None): excluded_service_filter = _excluded_service_filter() if excluded_service_filter is not None: if filter_ is None: filter_ = excluded_service_filter else: filter_ = any_of(filter_, excluded_service_filter) self._filter = filter_ self._request_hook = None self._response_hook = None def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _add_interceptors(self, tracer_provider, kwargs): if kwargs.get("interceptors"): kwargs["interceptors"] = list(kwargs["interceptors"]) kwargs["interceptors"] = ( aio_client_interceptors( tracer_provider=tracer_provider, filter_=self._filter, request_hook=self._request_hook, response_hook=self._response_hook, ) + kwargs["interceptors"] ) else: kwargs["interceptors"] = aio_client_interceptors( tracer_provider=tracer_provider, filter_=self._filter, request_hook=self._request_hook, response_hook=self._response_hook, ) return kwargs def _instrument(self, **kwargs): self._original_insecure = grpc.aio.insecure_channel self._original_secure = grpc.aio.secure_channel self._request_hook = kwargs.get("request_hook") self._response_hook = kwargs.get("response_hook") tracer_provider = kwargs.get("tracer_provider") def insecure(*args, **kwargs): kwargs = self._add_interceptors(tracer_provider, kwargs) return self._original_insecure(*args, **kwargs) def secure(*args, **kwargs): kwargs = self._add_interceptors(tracer_provider, kwargs) return self._original_secure(*args, **kwargs) grpc.aio.insecure_channel = insecure grpc.aio.secure_channel = secure def _uninstrument(self, **kwargs): grpc.aio.insecure_channel = self._original_insecure grpc.aio.secure_channel = self._original_secure def client_interceptor(tracer_provider=None, filter_=None, request_hook=None, response_hook=None): """Create a gRPC client channel interceptor. Args: tracer: The tracer to use to create client-side spans. filter_: filter function that returns True if gRPC requests matches the condition. Default is None and intercept all requests. Returns: An invocation-side interceptor object. """ from . import _client # noqa: PLC0415 tracer = trace.get_tracer( __name__, __version__, tracer_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) return _client.OpenTelemetryClientInterceptor( tracer, filter_=filter_, request_hook=request_hook, response_hook=response_hook, ) def server_interceptor(tracer_provider=None, filter_=None): """Create a gRPC server interceptor. Args: tracer: The tracer to use to create server-side spans. filter_: filter function that returns True if gRPC requests matches the condition. Default is None and intercept all requests. Returns: A service-side interceptor object. """ from . import _server # noqa: PLC0415 tracer = trace.get_tracer( __name__, __version__, tracer_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) return _server.OpenTelemetryServerInterceptor(tracer, filter_=filter_) def aio_client_interceptors(tracer_provider=None, filter_=None, request_hook=None, response_hook=None): """Create a gRPC client channel interceptor. Args: tracer: The tracer to use to create client-side spans. Returns: An invocation-side interceptor object. """ from . import _aio_client # noqa: PLC0415 tracer = trace.get_tracer( __name__, __version__, tracer_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) return [ _aio_client.UnaryUnaryAioClientInterceptor( tracer, filter_=filter_, request_hook=request_hook, response_hook=response_hook, ), _aio_client.UnaryStreamAioClientInterceptor( tracer, filter_=filter_, request_hook=request_hook, response_hook=response_hook, ), _aio_client.StreamUnaryAioClientInterceptor( tracer, filter_=filter_, request_hook=request_hook, response_hook=response_hook, ), _aio_client.StreamStreamAioClientInterceptor( tracer, filter_=filter_, request_hook=request_hook, response_hook=response_hook, ), ] def aio_server_interceptor(tracer_provider=None, filter_=None): """Create a gRPC aio server interceptor. Args: tracer: The tracer to use to create server-side spans. Returns: A service-side interceptor object. """ from . import _aio_server # noqa: PLC0415 tracer = trace.get_tracer( __name__, __version__, tracer_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) return _aio_server.OpenTelemetryAioServerInterceptor(tracer, filter_=filter_) def _excluded_service_filter() -> Callable[[object], bool] | None: services = _parse_services(os.environ.get("OTEL_PYTHON_GRPC_EXCLUDED_SERVICES", "")) if len(services) == 0: return None filters = (service_name(srv) for srv in services) return negate(any_of(*filters)) def _parse_services(excluded_services: str) -> list[str]: if excluded_services != "": excluded_service_list = [s.strip() for s in excluded_services.split(",")] else: excluded_service_list = [] return excluded_service_list _aio_client.py000066400000000000000000000165071527070072300445300ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import functools import logging import grpc from grpc.aio import ClientCallDetails, Metadata from opentelemetry.instrumentation.grpc._client import ( OpenTelemetryClientInterceptor, _carrier_setter, ) from opentelemetry.instrumentation.utils import is_instrumentation_enabled from opentelemetry.propagate import inject from opentelemetry.semconv._incubating.attributes.rpc_attributes import ( RPC_GRPC_STATUS_CODE, ) from opentelemetry.trace.status import Status, StatusCode logger = logging.getLogger(__name__) def _unary_done_callback(span, code, details, response_hook): def callback(call): try: span.set_attribute( RPC_GRPC_STATUS_CODE, code.value[0], ) if code != grpc.StatusCode.OK: span.set_status( Status( status_code=StatusCode.ERROR, description=details, ) ) response_hook(span, details) finally: span.end() return callback class _BaseAioClientInterceptor(OpenTelemetryClientInterceptor): @staticmethod def propagate_trace_in_details(client_call_details: ClientCallDetails): metadata = client_call_details.metadata if not metadata: mutable_metadata = Metadata() else: mutable_metadata = Metadata(*tuple(metadata)) inject(mutable_metadata, setter=_carrier_setter) return ClientCallDetails( client_call_details.method, client_call_details.timeout, mutable_metadata, client_call_details.credentials, client_call_details.wait_for_ready, ) @staticmethod def add_error_details_to_span(span, exc): if isinstance(exc, grpc.RpcError): span.set_attribute( RPC_GRPC_STATUS_CODE, exc.code().value[0], ) span.set_status( Status( status_code=StatusCode.ERROR, description=f"{type(exc).__name__}: {exc}", ) ) span.record_exception(exc) def _start_interceptor_span(self, method): # method _should_ be a string here but due to a bug in grpc, it is # populated with a bytes object. Handle both cases such that we # are forward-compatible with a fixed version of grpc # More info: https://github.com/grpc/grpc/issues/31092 if isinstance(method, bytes): method = method.decode() return self._start_span( method, end_on_exit=False, record_exception=False, set_status_on_exception=False, ) async def _wrap_unary_response(self, continuation, span): try: call = await continuation() # code and details are both coroutines that need to be await-ed, # the callbacks added with add_done_callback do not allow async # code so we need to get the code and details here then pass them # to the callback. code = await call.code() details = await call.details() callback = _unary_done_callback(span, code, details, self._call_response_hook) try: call.add_done_callback(callback) except NotImplementedError: # Some grpc.aio interceptors (e.g. interceptors that await the # call object) wrap the call in a type that does not implement # add_done_callback. In that case call the callback immediately # since code and details are already known. callback(call) return call except grpc.aio.AioRpcError as exc: self.add_error_details_to_span(span, exc) raise exc async def _wrap_stream_response(self, span, call): try: async for response in call: if self._response_hook: self._call_response_hook(span, response) yield response except Exception as exc: self.add_error_details_to_span(span, exc) raise exc finally: span.end() def tracing_skipped(self, client_call_details): return not is_instrumentation_enabled() or not self.rpc_matches_filters(client_call_details) def rpc_matches_filters(self, client_call_details): return self._filter is None or self._filter(client_call_details) class UnaryUnaryAioClientInterceptor( grpc.aio.UnaryUnaryClientInterceptor, _BaseAioClientInterceptor, ): async def intercept_unary_unary(self, continuation, client_call_details, request): if self.tracing_skipped(client_call_details): return await continuation(client_call_details, request) with self._start_interceptor_span( client_call_details.method, ) as span: new_details = self.propagate_trace_in_details(client_call_details) if self._request_hook: self._call_request_hook(span, request) continuation_with_args = functools.partial(continuation, new_details, request) return await self._wrap_unary_response(continuation_with_args, span) class UnaryStreamAioClientInterceptor( grpc.aio.UnaryStreamClientInterceptor, _BaseAioClientInterceptor, ): async def intercept_unary_stream(self, continuation, client_call_details, request): if self.tracing_skipped(client_call_details): return await continuation(client_call_details, request) with self._start_interceptor_span( client_call_details.method, ) as span: new_details = self.propagate_trace_in_details(client_call_details) resp = await continuation(new_details, request) if self._request_hook: self._call_request_hook(span, request) return self._wrap_stream_response(span, resp) class StreamUnaryAioClientInterceptor( grpc.aio.StreamUnaryClientInterceptor, _BaseAioClientInterceptor, ): async def intercept_stream_unary(self, continuation, client_call_details, request_iterator): if self.tracing_skipped(client_call_details): return await continuation(client_call_details, request_iterator) with self._start_interceptor_span( client_call_details.method, ) as span: new_details = self.propagate_trace_in_details(client_call_details) continuation_with_args = functools.partial(continuation, new_details, request_iterator) return await self._wrap_unary_response(continuation_with_args, span) class StreamStreamAioClientInterceptor( grpc.aio.StreamStreamClientInterceptor, _BaseAioClientInterceptor, ): async def intercept_stream_stream(self, continuation, client_call_details, request_iterator): if self.tracing_skipped(client_call_details): return await continuation(client_call_details, request_iterator) with self._start_interceptor_span( client_call_details.method, ) as span: new_details = self.propagate_trace_in_details(client_call_details) resp = await continuation(new_details, request_iterator) return self._wrap_stream_response(span, resp) _aio_server.py000066400000000000000000000124771527070072300445620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import grpc import grpc.aio try: # wrapt 2.0.0+ from wrapt import BaseObjectProxy # pylint: disable=no-name-in-module except ImportError: from wrapt import ObjectProxy as BaseObjectProxy from opentelemetry.instrumentation.utils import is_instrumentation_enabled from opentelemetry.semconv._incubating.attributes.rpc_attributes import ( RPC_GRPC_STATUS_CODE, ) from ._server import OpenTelemetryServerInterceptor, _wrap_rpc_behavior from ._utilities import _server_status # pylint:disable=abstract-method,no-member class _OpenTelemetryAioServicerContext(BaseObjectProxy): def __init__(self, servicer_context, active_span): super().__init__(servicer_context) self._self_active_span = active_span self._self_code = grpc.StatusCode.OK self._self_details = None async def abort(self, code, details="", trailing_metadata=tuple()): self._self_code = code self._self_details = details self._self_active_span.set_attribute(RPC_GRPC_STATUS_CODE, code.value[0]) status = _server_status(code, details) self._self_active_span.set_status(status) return await self.__wrapped__.abort(code, details, trailing_metadata) def set_code(self, code): self._self_code = code details = self._self_details or code.value[1] self._self_active_span.set_attribute(RPC_GRPC_STATUS_CODE, code.value[0]) if code != grpc.StatusCode.OK: status = _server_status(code, details) self._self_active_span.set_status(status) return self.__wrapped__.set_code(code) def set_details(self, details): self._self_details = details if self._self_code != grpc.StatusCode.OK: status = _server_status(self._self_code, details) self._self_active_span.set_status(status) return self.__wrapped__.set_details(details) class OpenTelemetryAioServerInterceptor(grpc.aio.ServerInterceptor, OpenTelemetryServerInterceptor): """ An AsyncIO gRPC server interceptor, to add OpenTelemetry. Usage:: tracer = some OpenTelemetry tracer interceptors = [ AsyncOpenTelemetryServerInterceptor(tracer), ] server = aio.server( futures.ThreadPoolExecutor(max_workers=concurrency), interceptors = (interceptors,)) """ async def intercept_service(self, continuation, handler_call_details): if not is_instrumentation_enabled(): return await continuation(handler_call_details) if self._filter is not None and not self._filter(handler_call_details): return await continuation(handler_call_details) def telemetry_wrapper(behavior, request_streaming, response_streaming): # handle streaming responses specially if response_streaming: return self._intercept_aio_server_stream( behavior, handler_call_details, ) return self._intercept_aio_server_unary( behavior, handler_call_details, ) next_handler = await continuation(handler_call_details) return _wrap_rpc_behavior(next_handler, telemetry_wrapper) def _intercept_aio_server_unary(self, behavior, handler_call_details): async def _unary_interceptor(request_or_iterator, context): with self._set_remote_context(context): with self._start_span( handler_call_details, context, set_status_on_exception=False, ) as span: # wrap the context context = _OpenTelemetryAioServicerContext(context, span) # And now we run the actual RPC. try: return await behavior(request_or_iterator, context) except Exception as error: # Bare exceptions are likely to be gRPC aborts, which # we handle in our context wrapper. # Here, we're interested in uncaught exceptions. # pylint:disable=unidiomatic-typecheck if type(error) != Exception: # noqa: E721 span.record_exception(error) raise error return _unary_interceptor def _intercept_aio_server_stream(self, behavior, handler_call_details): async def _stream_interceptor(request_or_iterator, context): with self._set_remote_context(context): with self._start_span( handler_call_details, context, set_status_on_exception=False, ) as span: context = _OpenTelemetryAioServicerContext(context, span) try: async for response in behavior(request_or_iterator, context): yield response except Exception as error: # pylint:disable=unidiomatic-typecheck if type(error) != Exception: # noqa: E721 span.record_exception(error) raise error return _stream_interceptor _client.py000066400000000000000000000175231527070072300436770ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint:disable=relative-beyond-top-level # pylint:disable=arguments-differ # pylint:disable=no-member # pylint:disable=signature-differs """Implementation of the invocation-side open-telemetry interceptor.""" import logging from collections import OrderedDict from collections.abc import Callable, MutableMapping import grpc from opentelemetry import trace from opentelemetry.instrumentation.grpc import grpcext from opentelemetry.instrumentation.grpc._utilities import RpcInfo from opentelemetry.instrumentation.utils import is_instrumentation_enabled from opentelemetry.propagate import inject from opentelemetry.propagators.textmap import Setter from opentelemetry.semconv._incubating.attributes.rpc_attributes import ( RPC_GRPC_STATUS_CODE, RPC_METHOD, RPC_SERVICE, RPC_SYSTEM, ) from opentelemetry.trace.status import Status, StatusCode logger = logging.getLogger(__name__) class _CarrierSetter(Setter): """We use a custom setter in order to be able to lower case keys as is required by grpc. """ def set(self, carrier: MutableMapping[str, str], key: str, value: str): carrier[key.lower()] = value _carrier_setter = _CarrierSetter() def _make_future_done_callback(span, rpc_info): def callback(response_future): with trace.use_span(span, end_on_exit=True): code = response_future.code() if code != grpc.StatusCode.OK: rpc_info.error = code span.set_attribute(RPC_GRPC_STATUS_CODE, code.value[0]) span.set_status( Status( status_code=StatusCode.ERROR, ) ) return response = response_future.result() rpc_info.response = response return callback def _safe_invoke(function: Callable, *args): function_name = "" try: function_name = function.__name__ function(*args) except Exception as ex: # pylint:disable=broad-except logger.error("Error when invoking function '%s'", function_name, exc_info=ex) class OpenTelemetryClientInterceptor(grpcext.UnaryClientInterceptor, grpcext.StreamClientInterceptor): def __init__(self, tracer, filter_=None, request_hook=None, response_hook=None): self._tracer = tracer self._filter = filter_ self._request_hook = request_hook self._response_hook = response_hook def _start_span(self, method, **kwargs): service, meth = method.lstrip("/").split("/", 1) attributes = { RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], RPC_METHOD: meth, RPC_SERVICE: service, } return self._tracer.start_as_current_span( name=method, kind=trace.SpanKind.CLIENT, attributes=attributes, **kwargs, ) # pylint:disable=no-self-use def _trace_result(self, span, rpc_info, result): # If the RPC is called asynchronously, add a callback to end the span # when the future is done, else end the span immediately if isinstance(result, grpc.Future): result.add_done_callback(_make_future_done_callback(span, rpc_info)) return result response = result # Handle the case when the RPC is initiated via the with_call # method and the result is a tuple with the first element as the # response. # http://www.grpc.io/grpc/python/grpc.html#grpc.UnaryUnaryMultiCallable.with_call if isinstance(result, tuple): response = result[0] rpc_info.response = response if self._response_hook: self._call_response_hook(span, response) span.end() return result def _intercept(self, request, metadata, client_info, invoker): if not is_instrumentation_enabled(): return invoker(request, metadata) if not metadata: mutable_metadata = OrderedDict() else: mutable_metadata = OrderedDict(metadata) with self._start_span( client_info.full_method, end_on_exit=False, record_exception=False, set_status_on_exception=False, ) as span: result = None try: inject(mutable_metadata, setter=_carrier_setter) metadata = tuple(mutable_metadata.items()) rpc_info = RpcInfo( full_method=client_info.full_method, metadata=metadata, timeout=client_info.timeout, request=request, ) if self._request_hook: self._call_request_hook(span, request) result = invoker(request, metadata) except Exception as exc: if isinstance(exc, grpc.RpcError): span.set_attribute( RPC_GRPC_STATUS_CODE, exc.code().value[0], ) span.set_status( Status( status_code=StatusCode.ERROR, description=f"{type(exc).__name__}: {exc}", ) ) span.record_exception(exc) raise exc finally: if result is None: span.end() return self._trace_result(span, rpc_info, result) def _call_request_hook(self, span, request): if not callable(self._request_hook): return _safe_invoke(self._request_hook, span, request) def _call_response_hook(self, span, response): if not callable(self._response_hook): return _safe_invoke(self._response_hook, span, response) def intercept_unary(self, request, metadata, client_info, invoker): if self._filter is not None and not self._filter(client_info): return invoker(request, metadata) return self._intercept(request, metadata, client_info, invoker) # For RPCs that stream responses, the result can be a generator. To record # the span across the generated responses and detect any errors, we wrap # the result in a new generator that yields the response values. def _intercept_server_stream(self, request_or_iterator, metadata, client_info, invoker): if not metadata: mutable_metadata = OrderedDict() else: mutable_metadata = OrderedDict(metadata) with self._start_span(client_info.full_method) as span: inject(mutable_metadata, setter=_carrier_setter) metadata = tuple(mutable_metadata.items()) rpc_info = RpcInfo( full_method=client_info.full_method, metadata=metadata, timeout=client_info.timeout, ) if client_info.is_client_stream: rpc_info.request = request_or_iterator try: yield from invoker(request_or_iterator, metadata) except grpc.RpcError as err: span.set_status(Status(StatusCode.ERROR)) span.set_attribute(RPC_GRPC_STATUS_CODE, err.code().value[0]) raise err def intercept_stream(self, request_or_iterator, metadata, client_info, invoker): if not is_instrumentation_enabled(): return invoker(request_or_iterator, metadata) if self._filter is not None and not self._filter(client_info): return invoker(request_or_iterator, metadata) if client_info.is_server_stream and not client_info.is_client_stream: return self._intercept_server_stream(request_or_iterator, metadata, client_info, invoker) return self._intercept(request_or_iterator, metadata, client_info, invoker) _server.py000066400000000000000000000262321527070072300437240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint:disable=relative-beyond-top-level # pylint:disable=arguments-differ # pylint:disable=no-member # pylint:disable=signature-differs """ Implementation of the service-side open-telemetry interceptor. """ import logging from contextlib import contextmanager from urllib.parse import unquote import grpc from opentelemetry import trace from opentelemetry.context import attach, detach from opentelemetry.instrumentation.utils import is_instrumentation_enabled from opentelemetry.propagate import extract from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_IP, NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv._incubating.attributes.rpc_attributes import ( RPC_GRPC_STATUS_CODE, RPC_METHOD, RPC_SERVICE, RPC_SYSTEM, ) from ._utilities import _server_status logger = logging.getLogger(__name__) # wrap an RPC call # see https://github.com/grpc/grpc/issues/18191 def _wrap_rpc_behavior(handler, continuation): if handler is None: return None if handler.request_streaming and handler.response_streaming: behavior_fn = handler.stream_stream handler_factory = grpc.stream_stream_rpc_method_handler elif handler.request_streaming and not handler.response_streaming: behavior_fn = handler.stream_unary handler_factory = grpc.stream_unary_rpc_method_handler elif not handler.request_streaming and handler.response_streaming: behavior_fn = handler.unary_stream handler_factory = grpc.unary_stream_rpc_method_handler else: behavior_fn = handler.unary_unary handler_factory = grpc.unary_unary_rpc_method_handler return handler_factory( continuation(behavior_fn, handler.request_streaming, handler.response_streaming), request_deserializer=handler.request_deserializer, response_serializer=handler.response_serializer, ) # pylint:disable=abstract-method class _OpenTelemetryServicerContext(grpc.ServicerContext): def __init__(self, servicer_context, active_span): self._servicer_context = servicer_context self._active_span = active_span self._code = grpc.StatusCode.OK self._details = None super().__init__() def __getattr__(self, attr): return getattr(self._servicer_context, attr) def is_active(self, *args, **kwargs): return self._servicer_context.is_active(*args, **kwargs) def time_remaining(self, *args, **kwargs): return self._servicer_context.time_remaining(*args, **kwargs) def cancel(self, *args, **kwargs): return self._servicer_context.cancel(*args, **kwargs) def add_callback(self, *args, **kwargs): return self._servicer_context.add_callback(*args, **kwargs) def disable_next_message_compression(self): return self._service_context.disable_next_message_compression() def invocation_metadata(self, *args, **kwargs): return self._servicer_context.invocation_metadata(*args, **kwargs) def peer(self): return self._servicer_context.peer() def peer_identities(self): return self._servicer_context.peer_identities() def peer_identity_key(self): return self._servicer_context.peer_identity_key() def auth_context(self): return self._servicer_context.auth_context() def set_compression(self, compression): return self._servicer_context.set_compression(compression) def send_initial_metadata(self, *args, **kwargs): return self._servicer_context.send_initial_metadata(*args, **kwargs) def set_trailing_metadata(self, *args, **kwargs): return self._servicer_context.set_trailing_metadata(*args, **kwargs) def trailing_metadata(self): return self._servicer_context.trailing_metadata() def abort(self, code, details): self._code = code self._details = details self._active_span.set_attribute(RPC_GRPC_STATUS_CODE, code.value[0]) status = _server_status(code, details) self._active_span.set_status(status) return self._servicer_context.abort(code, details) def abort_with_status(self, status): return self._servicer_context.abort_with_status(status) def code(self): if not hasattr(self._servicer_context, "code"): raise RuntimeError("code() is not supported with the installed version of grpcio") return self._servicer_context.code() def details(self): if not hasattr(self._servicer_context, "details"): raise RuntimeError("details() is not supported with the installed version of grpcio") return self._servicer_context.details() def set_code(self, code): self._code = code # use details if we already have it, otherwise the status description details = self._details or code.value[1] self._active_span.set_attribute(RPC_GRPC_STATUS_CODE, code.value[0]) if code != grpc.StatusCode.OK: status = _server_status(code, details) self._active_span.set_status(status) return self._servicer_context.set_code(code) def set_details(self, details): self._details = details if self._code != grpc.StatusCode.OK: status = _server_status(self._code, details) self._active_span.set_status(status) return self._servicer_context.set_details(details) # pylint:disable=abstract-method # pylint:disable=no-self-use # pylint:disable=unused-argument class OpenTelemetryServerInterceptor(grpc.ServerInterceptor): """ A gRPC server interceptor, to add OpenTelemetry. Usage:: tracer = some OpenTelemetry tracer filter = filters.negate(filters.method_name("service.Foo")) interceptors = [ OpenTelemetryServerInterceptor(tracer, filter), ] server = grpc.server( futures.ThreadPoolExecutor(max_workers=concurrency), interceptors = interceptors) """ def __init__(self, tracer, filter_=None): self._tracer = tracer self._filter = filter_ @contextmanager def _set_remote_context(self, servicer_context): metadata = servicer_context.invocation_metadata() if metadata: md_dict = {md.key: md.value for md in metadata} ctx = extract(md_dict) token = attach(ctx) try: yield finally: if token: detach(token) else: yield def _start_span(self, handler_call_details, context, set_status_on_exception=False): # standard attributes attributes = { RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], } # if we have details about the call, split into service and method if handler_call_details.method: service, method = handler_call_details.method.lstrip("/").split("/", 1) attributes.update( { RPC_METHOD: method, RPC_SERVICE: service, } ) # add some attributes from the metadata metadata = dict(context.invocation_metadata()) if "user-agent" in metadata: attributes["rpc.user_agent"] = metadata["user-agent"] # Split up the peer to keep with how other telemetry sources # do it. This looks like: # * ipv6:[::1]:57284 # * ipv4:127.0.0.1:57284 # * ipv4:10.2.1.1:57284,127.0.0.1:57284 # if not context.peer().startswith("unix:"): try: ip, port = context.peer().split(",")[0].split(":", 1)[1].rsplit(":", 1) ip = unquote(ip) attributes.update( { NET_PEER_IP: ip, NET_PEER_PORT: port, } ) # other telemetry sources add this, so we will too if ip in ("[::1]", "127.0.0.1"): attributes[NET_PEER_NAME] = "localhost" except IndexError: logger.warning("Failed to parse peer address '%s'", context.peer()) return self._tracer.start_as_current_span( name=handler_call_details.method, kind=trace.SpanKind.SERVER, attributes=attributes, set_status_on_exception=set_status_on_exception, ) def intercept_service(self, continuation, handler_call_details): if not is_instrumentation_enabled(): return continuation(handler_call_details) if self._filter is not None and not self._filter(handler_call_details): return continuation(handler_call_details) def telemetry_wrapper(behavior, request_streaming, response_streaming): def telemetry_interceptor(request_or_iterator, context): # handle streaming responses specially if response_streaming: return self._intercept_server_stream( behavior, handler_call_details, request_or_iterator, context, ) with self._set_remote_context(context): with self._start_span( handler_call_details, context, set_status_on_exception=False, ) as span: # wrap the context context = _OpenTelemetryServicerContext(context, span) # And now we run the actual RPC. try: return behavior(request_or_iterator, context) except Exception as error: # Bare exceptions are likely to be gRPC aborts, which # we handle in our context wrapper. # Here, we're interested in uncaught exceptions. # pylint:disable=unidiomatic-typecheck if type(error) != Exception: # noqa: E721 span.record_exception(error) raise error return telemetry_interceptor return _wrap_rpc_behavior(continuation(handler_call_details), telemetry_wrapper) # Handle streaming responses separately - we have to do this # to return a *new* generator or various upstream things # get confused, or we'll lose the consistent trace def _intercept_server_stream(self, behavior, handler_call_details, request_or_iterator, context): with self._set_remote_context(context): with self._start_span(handler_call_details, context, set_status_on_exception=False) as span: context = _OpenTelemetryServicerContext(context, span) try: yield from behavior(request_or_iterator, context) except Exception as error: # pylint:disable=unidiomatic-typecheck if type(error) != Exception: # noqa: E721 span.record_exception(error) raise error _utilities.py000066400000000000000000000021241527070072300444230ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Internal utilities.""" import grpc from opentelemetry.trace.status import Status, StatusCode class RpcInfo: def __init__( self, full_method=None, metadata=None, timeout=None, request=None, response=None, error=None, ): self.full_method = full_method self.metadata = metadata self.timeout = timeout self.request = request self.response = response self.error = error def _server_status(code, details): error_status = Status(status_code=StatusCode.ERROR, description=f"{code}:{details}") status_codes = { grpc.StatusCode.UNKNOWN: error_status, grpc.StatusCode.DEADLINE_EXCEEDED: error_status, grpc.StatusCode.UNIMPLEMENTED: error_status, grpc.StatusCode.INTERNAL: error_status, grpc.StatusCode.UNAVAILABLE: error_status, grpc.StatusCode.DATA_LOSS: error_status, } return status_codes.get(code, Status(status_code=StatusCode.UNSET, description="")) filters/000077500000000000000000000000001527070072300433505ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc__init__.py000066400000000000000000000133161527070072300454650ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/filters# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os from collections.abc import Callable from typing import TypeVar import grpc CallDetailsT = TypeVar( "CallDetailsT", grpc.HandlerCallDetails, grpc.ClientCallDetails, grpc.aio.ClientCallDetails, ) # pylint: disable=invalid-name Condition = Callable[[CallDetailsT], bool] def _full_method(metadata): name = "" if isinstance(metadata, grpc.HandlerCallDetails): name = metadata.method elif isinstance(metadata, grpc.aio.ClientCallDetails): name = metadata.method # name _should_ be a string here but due to a bug in grpc, it is # populated with a bytes object. Handle both cases such that we # are forward-compatible with a fixed version of grpc # More info: https://github.com/grpc/grpc/issues/31092 if isinstance(name, bytes): name = name.decode() # NOTE: replace here if there's better way to match cases to handle # grpcext._interceptor._UnaryClientInfo/_StreamClientInfo elif hasattr(metadata, "full_method"): name = metadata.full_method # NOTE: this is to handle the grpc.aio Server case. The type interface # indicates that metadata should be a grpc.HandlerCallDetails and be # matched prior to this but it is in fact an internal C-extension level # object. elif hasattr(metadata, "method"): name = metadata.method return name def _split_full_method(metadata): name = _full_method(metadata) service, method = os.path.split(name) if service != "": service = os.path.normpath(service) service = service.lstrip("/") return (service, method) def all_of(*args: Condition[CallDetailsT]) -> Condition[CallDetailsT]: """Returns a filter function that returns True if all filter functions assigned matches conditions. Args: args (function): a list of filter function Returns: A filter function that returns True if all filter functions assigned matches conditions. """ def filter_fn(metadata): return all(func(metadata) for func in args) return filter_fn def any_of(*args: Condition[CallDetailsT]) -> Condition[CallDetailsT]: """Returns a filter function that returns True if any of filter functions assigned matches conditions. Args: args (function): a list of filter function Returns: A filter function that returns True if any of filter functions assigned matches conditions. """ def filter_fn(metadata): return any(func(metadata) for func in args) return filter_fn def negate(func: Condition[CallDetailsT]) -> Condition[CallDetailsT]: """Returns a filter function that negate the result of func Args: func (function): filter function to negate the result Returns: A filter function that negate the result of func """ def filter_fn(metadata): return not func(metadata) return filter_fn def method_name(name: str) -> Condition[CallDetailsT]: """Returns a filter function that return True if request's gRPC method name matches name. Args: name (str): method name to match Returns: A filter function that returns True if request's gRPC method name matches name """ def filter_fn(metadata): _, method = _split_full_method(metadata) return method == name return filter_fn def method_prefix(prefix: str) -> Condition[CallDetailsT]: """Returns a filter function that return True if request's gRPC method name starts with prefix. Args: prefix (str): method prefix to match Returns: A filter function that returns True if request's gRPC method name starts with prefix """ def filter_fn(metadata): _, method = _split_full_method(metadata) return method.startswith(prefix) return filter_fn def full_method_name(name: str) -> Condition[CallDetailsT]: """Returns a filter function that return True if request's gRPC full method name matches name. Args: name (str): full method name to match Returns: A filter function that returns True if request's gRPC full method name matches name """ def filter_fn(metadata): fm = _full_method(metadata) return fm == name return filter_fn def service_name(name: str) -> Condition[CallDetailsT]: """Returns a filter function that return True if request's gRPC service name matches name. Args: name (str): service name to match Returns: A filter function that returns True if request's gRPC service name matches name """ def filter_fn(metadata): service, _ = _split_full_method(metadata) return service == name return filter_fn def service_prefix(prefix: str) -> Condition[CallDetailsT]: """Returns a filter function that return True if request's gRPC service name starts with prefix. Args: prefix (str): service prefix to match Returns: A filter function that returns True if request's gRPC service name starts with prefix """ def filter_fn(metadata): service, _ = _split_full_method(metadata) return service.startswith(prefix) return filter_fn def health_check() -> Condition[CallDetailsT]: """Returns a Filter that returns true if the request's service name is health check defined by gRPC Health Checking Protocol. https://github.com/grpc/grpc/blob/master/doc/health-checking.md """ return service_prefix("grpc.health.v1.Health") __all__ = [ "full_method_name", "health_check", "method_name", "method_prefix", "service_name", "service_prefix", ] grpcext/000077500000000000000000000000001527070072300433545ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc__init__.py000066400000000000000000000071171527070072300454730ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint:disable=import-outside-toplevel # pylint:disable=import-self # pylint:disable=no-name-in-module import abc class UnaryClientInfo(abc.ABC): """Consists of various information about a unary RPC on the invocation-side. Attributes: full_method: A string of the full RPC method, i.e., /package.service/method. timeout: The length of time in seconds to wait for the computation to terminate or be cancelled, or None if this method should block until the computation is terminated or is cancelled no matter how long that takes. """ class StreamClientInfo(abc.ABC): """Consists of various information about a stream RPC on the invocation-side. Attributes: full_method: A string of the full RPC method, i.e., /package.service/method. is_client_stream: Indicates whether the RPC is client-streaming. is_server_stream: Indicates whether the RPC is server-streaming. timeout: The length of time in seconds to wait for the computation to terminate or be cancelled, or None if this method should block until the computation is terminated or is cancelled no matter how long that takes. """ class UnaryClientInterceptor(abc.ABC): """Affords intercepting unary-unary RPCs on the invocation-side.""" @abc.abstractmethod def intercept_unary(self, request, metadata, client_info, invoker): """Intercepts unary-unary RPCs on the invocation-side. Args: request: The request value for the RPC. metadata: Optional :term:`metadata` to be transmitted to the service-side of the RPC. client_info: A UnaryClientInfo containing various information about the RPC. invoker: The handler to complete the RPC on the client. It is the interceptor's responsibility to call it. Returns: The result from calling invoker(request, metadata). """ raise NotImplementedError() class StreamClientInterceptor(abc.ABC): """Affords intercepting stream RPCs on the invocation-side.""" @abc.abstractmethod def intercept_stream(self, request_or_iterator, metadata, client_info, invoker): """Intercepts stream RPCs on the invocation-side. Args: request_or_iterator: The request value for the RPC if `client_info.is_client_stream` is `false`; otherwise, an iterator of request values. metadata: Optional :term:`metadata` to be transmitted to the service-side of the RPC. client_info: A StreamClientInfo containing various information about the RPC. invoker: The handler to complete the RPC on the client. It is the interceptor's responsibility to call it. Returns: The result from calling invoker(metadata). """ raise NotImplementedError() def intercept_channel(channel, *interceptors): """Creates an intercepted channel. Args: channel: A Channel. interceptors: Zero or more UnaryClientInterceptors or StreamClientInterceptors Returns: A Channel. Raises: TypeError: If an interceptor derives from neither UnaryClientInterceptor nor StreamClientInterceptor. """ from . import _interceptor # noqa: PLC0415 return _interceptor.intercept_channel(channel, *interceptors) __all__ = ( "StreamClientInfo", "StreamClientInterceptor", "UnaryClientInterceptor", "intercept_channel", ) _interceptor.py000066400000000000000000000250401527070072300464240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/grpcext# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint:disable=relative-beyond-top-level # pylint:disable=no-member """Implementation of gRPC Python interceptors.""" import collections import grpc from opentelemetry.instrumentation.grpc import grpcext class _UnaryClientInfo(collections.namedtuple("_UnaryClientInfo", ("full_method", "timeout"))): pass class _StreamClientInfo( collections.namedtuple( "_StreamClientInfo", ("full_method", "is_client_stream", "is_server_stream", "timeout"), ) ): pass class _InterceptorUnaryUnaryMultiCallable(grpc.UnaryUnaryMultiCallable): def __init__(self, method, base_callable, interceptor): self._method = method self._base_callable = base_callable self._interceptor = interceptor def __call__( self, request, timeout=None, metadata=None, credentials=None, wait_for_ready=None, compression=None, ): def invoker(request, metadata): return self._base_callable( request, timeout, metadata, credentials, wait_for_ready, compression, ) client_info = _UnaryClientInfo(self._method, timeout) return self._interceptor.intercept_unary(request, metadata, client_info, invoker) def with_call( self, request, timeout=None, metadata=None, credentials=None, wait_for_ready=None, compression=None, ): def invoker(request, metadata): return self._base_callable.with_call( request, timeout, metadata, credentials, wait_for_ready, compression, ) client_info = _UnaryClientInfo(self._method, timeout) return self._interceptor.intercept_unary(request, metadata, client_info, invoker) def future( self, request, timeout=None, metadata=None, credentials=None, wait_for_ready=None, compression=None, ): def invoker(request, metadata): return self._base_callable.future( request, timeout, metadata, credentials, wait_for_ready, compression, ) client_info = _UnaryClientInfo(self._method, timeout) return self._interceptor.intercept_unary(request, metadata, client_info, invoker) class _InterceptorUnaryStreamMultiCallable(grpc.UnaryStreamMultiCallable): def __init__(self, method, base_callable, interceptor): self._method = method self._base_callable = base_callable self._interceptor = interceptor def __call__( self, request, timeout=None, metadata=None, credentials=None, wait_for_ready=None, compression=None, ): def invoker(request, metadata): return self._base_callable( request, timeout, metadata, credentials, wait_for_ready, compression, ) client_info = _StreamClientInfo(self._method, False, True, timeout) return self._interceptor.intercept_stream(request, metadata, client_info, invoker) class _InterceptorStreamUnaryMultiCallable(grpc.StreamUnaryMultiCallable): def __init__(self, method, base_callable, interceptor): self._method = method self._base_callable = base_callable self._interceptor = interceptor def __call__( self, request_iterator, timeout=None, metadata=None, credentials=None, wait_for_ready=None, compression=None, ): def invoker(request_iterator, metadata): return self._base_callable( request_iterator, timeout, metadata, credentials, wait_for_ready, compression, ) client_info = _StreamClientInfo(self._method, True, False, timeout) return self._interceptor.intercept_stream(request_iterator, metadata, client_info, invoker) def with_call( self, request_iterator, timeout=None, metadata=None, credentials=None, wait_for_ready=None, compression=None, ): def invoker(request_iterator, metadata): return self._base_callable.with_call( request_iterator, timeout, metadata, credentials, wait_for_ready, compression, ) client_info = _StreamClientInfo(self._method, True, False, timeout) return self._interceptor.intercept_stream(request_iterator, metadata, client_info, invoker) def future( self, request_iterator, timeout=None, metadata=None, credentials=None, wait_for_ready=None, compression=None, ): def invoker(request_iterator, metadata): return self._base_callable.future( request_iterator, timeout, metadata, credentials, wait_for_ready, compression, ) client_info = _StreamClientInfo(self._method, True, False, timeout) return self._interceptor.intercept_stream(request_iterator, metadata, client_info, invoker) class _InterceptorStreamStreamMultiCallable(grpc.StreamStreamMultiCallable): def __init__(self, method, base_callable, interceptor): self._method = method self._base_callable = base_callable self._interceptor = interceptor def __call__( self, request_iterator, timeout=None, metadata=None, credentials=None, wait_for_ready=None, compression=None, ): def invoker(request_iterator, metadata): return self._base_callable( request_iterator, timeout, metadata, credentials, wait_for_ready, compression, ) client_info = _StreamClientInfo(self._method, True, True, timeout) return self._interceptor.intercept_stream(request_iterator, metadata, client_info, invoker) class _InterceptorChannel(grpc.Channel): def __init__(self, channel, interceptor): self._channel = channel self._interceptor = interceptor def subscribe(self, *args, **kwargs): self._channel.subscribe(*args, **kwargs) def unsubscribe(self, *args, **kwargs): self._channel.unsubscribe(*args, **kwargs) def unary_unary( self, method, request_serializer=None, response_deserializer=None, _registered_method=False, ): if _registered_method: base_callable = self._channel.unary_unary( method, request_serializer, response_deserializer, _registered_method, ) else: base_callable = self._channel.unary_unary(method, request_serializer, response_deserializer) if isinstance(self._interceptor, grpcext.UnaryClientInterceptor): return _InterceptorUnaryUnaryMultiCallable(method, base_callable, self._interceptor) return base_callable def unary_stream( self, method, request_serializer=None, response_deserializer=None, _registered_method=False, ): if _registered_method: base_callable = self._channel.unary_stream( method, request_serializer, response_deserializer, _registered_method, ) else: base_callable = self._channel.unary_stream(method, request_serializer, response_deserializer) if isinstance(self._interceptor, grpcext.StreamClientInterceptor): return _InterceptorUnaryStreamMultiCallable(method, base_callable, self._interceptor) return base_callable def stream_unary( self, method, request_serializer=None, response_deserializer=None, _registered_method=False, ): if _registered_method: base_callable = self._channel.stream_unary( method, request_serializer, response_deserializer, _registered_method, ) else: base_callable = self._channel.stream_unary(method, request_serializer, response_deserializer) if isinstance(self._interceptor, grpcext.StreamClientInterceptor): return _InterceptorStreamUnaryMultiCallable(method, base_callable, self._interceptor) return base_callable def stream_stream( self, method, request_serializer=None, response_deserializer=None, _registered_method=False, ): if _registered_method: base_callable = self._channel.stream_stream( method, request_serializer, response_deserializer, _registered_method, ) else: base_callable = self._channel.stream_stream(method, request_serializer, response_deserializer) if isinstance(self._interceptor, grpcext.StreamClientInterceptor): return _InterceptorStreamStreamMultiCallable(method, base_callable, self._interceptor) return base_callable def close(self): if not hasattr(self._channel, "close"): raise RuntimeError("close() is not supported with the installed version of grpcio") self._channel.close() def __enter__(self): """Enters the runtime context related to the channel object.""" return self def __exit__(self, exc_type, exc_val, exc_tb): """Exits the runtime context related to the channel object.""" self.close() def intercept_channel(channel, *interceptors): result = channel for interceptor in interceptors: if not isinstance(interceptor, grpcext.UnaryClientInterceptor) and not isinstance( interceptor, grpcext.StreamClientInterceptor ): raise TypeError( "interceptor must be either a grpcext.UnaryClientInterceptor or a grpcext.StreamClientInterceptor" ) result = _InterceptorChannel(result, interceptor) return result package.py000066400000000000000000000001631527070072300436450ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("grpcio >= 1.42.0",) version.py000066400000000000000000000001441527070072300437360ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements-0-wrapt1.txt000066400000000000000000000000511527070072300376250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc-r test-requirements-0.txt wrapt==1.17.3 test-requirements-0-wrapt2.txt000066400000000000000000000000501527070072300376250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc-r test-requirements-0.txt wrapt==2.1.2 test-requirements-0.txt000066400000000000000000000004111527070072300364110ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpcasgiref==3.8.1 grpcio==1.62.0 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 protobuf==3.20.3 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-grpc test-requirements-1-wrapt1.txt000066400000000000000000000000511527070072300376260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc-r test-requirements-1.txt wrapt==1.17.3 test-requirements-1-wrapt2.txt000066400000000000000000000000501527070072300376260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc-r test-requirements-1.txt wrapt==2.1.2 test-requirements-1.txt000066400000000000000000000004111527070072300364120ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpcasgiref==3.8.1 grpcio==1.75.1 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 protobuf==3.20.3 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-grpc opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests/000077500000000000000000000000001527070072300332405ustar00rootroot00000000000000__init__.py000066400000000000000000000001141527070072300352660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _aio_client.py000066400000000000000000000023201527070072300357750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from .protobuf.test_server_pb2 import Request CLIENT_ID = 1 async def simple_method(stub, error=False): request = Request(client_id=CLIENT_ID, request_data="error" if error else "data") return await stub.SimpleMethod(request, metadata=(("key", "value"),)) async def client_streaming_method(stub, error=False): # create a generator def request_messages(): for _ in range(5): request = Request(client_id=CLIENT_ID, request_data="error" if error else "data") yield request return await stub.ClientStreamingMethod(request_messages()) def server_streaming_method(stub, error=False): request = Request(client_id=CLIENT_ID, request_data="error" if error else "data") return stub.ServerStreamingMethod(request, metadata=(("key", "value"),)) def bidirectional_streaming_method(stub, error=False): # create a generator def request_messages(): for _ in range(5): request = Request(client_id=CLIENT_ID, request_data="error" if error else "data") yield request return stub.BidirectionalStreamingMethod(request_messages(), metadata=(("key", "value"),)) _client.py000066400000000000000000000027211527070072300351520ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from .protobuf.test_server_pb2 import Request CLIENT_ID = 1 def simple_method(stub, error=False): request = Request(client_id=CLIENT_ID, request_data="error" if error else "data") stub.SimpleMethod(request, metadata=(("key", "value"),)) def simple_method_future(stub, error=False): request = Request(client_id=CLIENT_ID, request_data="error" if error else "data") return stub.SimpleMethod.future(request, metadata=(("key", "value"),)) def client_streaming_method(stub, error=False): # create a generator def request_messages(): for _ in range(5): request = Request(client_id=CLIENT_ID, request_data="error" if error else "data") yield request stub.ClientStreamingMethod(request_messages(), metadata=(("key", "value"),)) def server_streaming_method(stub, error=False): request = Request(client_id=CLIENT_ID, request_data="error" if error else "data") response_iterator = stub.ServerStreamingMethod(request, metadata=(("key", "value"),)) list(response_iterator) def bidirectional_streaming_method(stub, error=False): def request_messages(): for _ in range(5): request = Request(client_id=CLIENT_ID, request_data="error" if error else "data") yield request response_iterator = stub.BidirectionalStreamingMethod(request_messages(), metadata=(("key", "value"),)) list(response_iterator) _server.py000066400000000000000000000043511527070072300352030ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from concurrent import futures import grpc from .protobuf import test_server_pb2, test_server_pb2_grpc SERVER_ID = 1 class TestServer(test_server_pb2_grpc.GRPCTestServerServicer): # pylint: disable=invalid-name # pylint: disable=no-self-use def SimpleMethod(self, request, context): if request.request_data == "error": context.set_code(grpc.StatusCode.INVALID_ARGUMENT) return test_server_pb2.Response() response = test_server_pb2.Response(server_id=SERVER_ID, response_data="data") return response def ClientStreamingMethod(self, request_iterator, context): data = list(request_iterator) if data[0].request_data == "error": context.set_code(grpc.StatusCode.INVALID_ARGUMENT) return test_server_pb2.Response() response = test_server_pb2.Response(server_id=SERVER_ID, response_data="data") return response def ServerStreamingMethod(self, request, context): if request.request_data == "error": context.abort( code=grpc.StatusCode.INVALID_ARGUMENT, details="server stream error", ) return test_server_pb2.Response() # create a generator def response_messages(): for _ in range(5): response = test_server_pb2.Response(server_id=SERVER_ID, response_data="data") yield response return response_messages() def BidirectionalStreamingMethod(self, request_iterator, context): data = list(request_iterator) if data[0].request_data == "error": context.abort( code=grpc.StatusCode.INVALID_ARGUMENT, details="bidirectional error", ) return for _ in range(5): yield test_server_pb2.Response(server_id=SERVER_ID, response_data="data") def create_test_server(port): # pylint: disable=consider-using-with server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) test_server_pb2_grpc.add_GRPCTestServerServicer_to_server(TestServer(), server) server.add_insecure_port(f"localhost:{port}") return server protobuf/000077500000000000000000000000001527070072300350215ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/teststest_server.proto000066400000000000000000000020421527070072300404510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf// Copyright 2019 gRPC authors. // // 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. syntax = "proto3"; message Request { int64 client_id = 1; string request_data = 2; } message Response { int64 server_id = 1; string response_data = 2; } service GRPCTestServer { rpc SimpleMethod (Request) returns (Response); rpc ClientStreamingMethod (stream Request) returns (Response); rpc ServerStreamingMethod (Request) returns (stream Response); rpc BidirectionalStreamingMethod (stream Request) returns (stream Response); } test_server_pb2.py000066400000000000000000000143441527070072300405110ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf# Generated by the protocol buffer compiler. DO NOT EDIT! # source: test_server.proto from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor.FileDescriptor( name="test_server.proto", package="", syntax="proto3", serialized_options=None, serialized_pb=b'\n\x11test_server.proto"2\n\x07Request\x12\x11\n\tclient_id\x18\x01 \x01(\x03\x12\x14\n\x0crequest_data\x18\x02 \x01(\t"4\n\x08Response\x12\x11\n\tserver_id\x18\x01 \x01(\x03\x12\x15\n\rresponse_data\x18\x02 \x01(\t2\xce\x01\n\x0eGRPCTestServer\x12#\n\x0cSimpleMethod\x12\x08.Request\x1a\t.Response\x12.\n\x15\x43lientStreamingMethod\x12\x08.Request\x1a\t.Response(\x01\x12.\n\x15ServerStreamingMethod\x12\x08.Request\x1a\t.Response0\x01\x12\x37\n\x1c\x42idirectionalStreamingMethod\x12\x08.Request\x1a\t.Response(\x01\x30\x01\x62\x06proto3', ) _REQUEST = _descriptor.Descriptor( name="Request", full_name="Request", filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name="client_id", full_name="Request.client_id", index=0, number=1, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, ), _descriptor.FieldDescriptor( name="request_data", full_name="Request.request_data", index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode("utf-8"), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, ), ], extensions=[], nested_types=[], enum_types=[], serialized_options=None, is_extendable=False, syntax="proto3", extension_ranges=[], oneofs=[], serialized_start=21, serialized_end=71, ) _RESPONSE = _descriptor.Descriptor( name="Response", full_name="Response", filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name="server_id", full_name="Response.server_id", index=0, number=1, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, ), _descriptor.FieldDescriptor( name="response_data", full_name="Response.response_data", index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode("utf-8"), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, ), ], extensions=[], nested_types=[], enum_types=[], serialized_options=None, is_extendable=False, syntax="proto3", extension_ranges=[], oneofs=[], serialized_start=73, serialized_end=125, ) DESCRIPTOR.message_types_by_name["Request"] = _REQUEST DESCRIPTOR.message_types_by_name["Response"] = _RESPONSE _sym_db.RegisterFileDescriptor(DESCRIPTOR) Request = _reflection.GeneratedProtocolMessageType( "Request", (_message.Message,), { "DESCRIPTOR": _REQUEST, "__module__": "test_server_pb2", # @@protoc_insertion_point(class_scope:Request) }, ) _sym_db.RegisterMessage(Request) Response = _reflection.GeneratedProtocolMessageType( "Response", (_message.Message,), { "DESCRIPTOR": _RESPONSE, "__module__": "test_server_pb2", # @@protoc_insertion_point(class_scope:Response) }, ) _sym_db.RegisterMessage(Response) _GRPCTESTSERVER = _descriptor.ServiceDescriptor( name="GRPCTestServer", full_name="GRPCTestServer", file=DESCRIPTOR, index=0, serialized_options=None, serialized_start=128, serialized_end=334, methods=[ _descriptor.MethodDescriptor( name="SimpleMethod", full_name="GRPCTestServer.SimpleMethod", index=0, containing_service=None, input_type=_REQUEST, output_type=_RESPONSE, serialized_options=None, ), _descriptor.MethodDescriptor( name="ClientStreamingMethod", full_name="GRPCTestServer.ClientStreamingMethod", index=1, containing_service=None, input_type=_REQUEST, output_type=_RESPONSE, serialized_options=None, ), _descriptor.MethodDescriptor( name="ServerStreamingMethod", full_name="GRPCTestServer.ServerStreamingMethod", index=2, containing_service=None, input_type=_REQUEST, output_type=_RESPONSE, serialized_options=None, ), _descriptor.MethodDescriptor( name="BidirectionalStreamingMethod", full_name="GRPCTestServer.BidirectionalStreamingMethod", index=3, containing_service=None, input_type=_REQUEST, output_type=_RESPONSE, serialized_options=None, ), ], ) _sym_db.RegisterServiceDescriptor(_GRPCTESTSERVER) DESCRIPTOR.services_by_name["GRPCTestServer"] = _GRPCTESTSERVER # @@protoc_insertion_point(module_scope) test_server_pb2_grpc.py000066400000000000000000000162211527070072300415200ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! import grpc from tests.protobuf import test_server_pb2 as test__server__pb2 class GRPCTestServerStub: """Missing associated documentation comment in .proto file""" def __init__(self, channel): """Constructor. Args: channel: A grpc.Channel. """ # pylint: disable=invalid-name self.SimpleMethod = channel.unary_unary( "/GRPCTestServer/SimpleMethod", request_serializer=test__server__pb2.Request.SerializeToString, response_deserializer=test__server__pb2.Response.FromString, ) self.ClientStreamingMethod = channel.stream_unary( "/GRPCTestServer/ClientStreamingMethod", request_serializer=test__server__pb2.Request.SerializeToString, response_deserializer=test__server__pb2.Response.FromString, ) self.ServerStreamingMethod = channel.unary_stream( "/GRPCTestServer/ServerStreamingMethod", request_serializer=test__server__pb2.Request.SerializeToString, response_deserializer=test__server__pb2.Response.FromString, ) self.BidirectionalStreamingMethod = channel.stream_stream( "/GRPCTestServer/BidirectionalStreamingMethod", request_serializer=test__server__pb2.Request.SerializeToString, response_deserializer=test__server__pb2.Response.FromString, ) class GRPCTestServerServicer: """Missing associated documentation comment in .proto file""" # pylint: disable=invalid-name # pylint: disable=no-self-use def SimpleMethod(self, request, context): """Missing associated documentation comment in .proto file""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") raise NotImplementedError("Method not implemented!") def ClientStreamingMethod(self, request_iterator, context): """Missing associated documentation comment in .proto file""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") raise NotImplementedError("Method not implemented!") def ServerStreamingMethod(self, request, context): """Missing associated documentation comment in .proto file""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") raise NotImplementedError("Method not implemented!") def BidirectionalStreamingMethod(self, request_iterator, context): """Missing associated documentation comment in .proto file""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") raise NotImplementedError("Method not implemented!") def add_GRPCTestServerServicer_to_server( servicer, server ): # pylint: disable=invalid-name rpc_method_handlers = { "SimpleMethod": grpc.unary_unary_rpc_method_handler( servicer.SimpleMethod, request_deserializer=test__server__pb2.Request.FromString, response_serializer=test__server__pb2.Response.SerializeToString, ), "ClientStreamingMethod": grpc.stream_unary_rpc_method_handler( servicer.ClientStreamingMethod, request_deserializer=test__server__pb2.Request.FromString, response_serializer=test__server__pb2.Response.SerializeToString, ), "ServerStreamingMethod": grpc.unary_stream_rpc_method_handler( servicer.ServerStreamingMethod, request_deserializer=test__server__pb2.Request.FromString, response_serializer=test__server__pb2.Response.SerializeToString, ), "BidirectionalStreamingMethod": grpc.stream_stream_rpc_method_handler( servicer.BidirectionalStreamingMethod, request_deserializer=test__server__pb2.Request.FromString, response_serializer=test__server__pb2.Response.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( "GRPCTestServer", rpc_method_handlers ) server.add_generic_rpc_handlers((generic_handler,)) # This class is part of an EXPERIMENTAL API. class GRPCTestServer: """Missing associated documentation comment in .proto file""" # pylint: disable=invalid-name @staticmethod def SimpleMethod( request, target, options=(), channel_credentials=None, call_credentials=None, compression=None, wait_for_ready=None, timeout=None, metadata=None, ): return grpc.experimental.unary_unary( request, target, "/GRPCTestServer/SimpleMethod", test__server__pb2.Request.SerializeToString, test__server__pb2.Response.FromString, options, channel_credentials, call_credentials, compression, wait_for_ready, timeout, metadata, ) @staticmethod def ClientStreamingMethod( request_iterator, target, options=(), channel_credentials=None, call_credentials=None, compression=None, wait_for_ready=None, timeout=None, metadata=None, ): return grpc.experimental.stream_unary( request_iterator, target, "/GRPCTestServer/ClientStreamingMethod", test__server__pb2.Request.SerializeToString, test__server__pb2.Response.FromString, options, channel_credentials, call_credentials, compression, wait_for_ready, timeout, metadata, ) @staticmethod def ServerStreamingMethod( request, target, options=(), channel_credentials=None, call_credentials=None, compression=None, wait_for_ready=None, timeout=None, metadata=None, ): return grpc.experimental.unary_stream( request, target, "/GRPCTestServer/ServerStreamingMethod", test__server__pb2.Request.SerializeToString, test__server__pb2.Response.FromString, options, channel_credentials, call_credentials, compression, wait_for_ready, timeout, metadata, ) @staticmethod def BidirectionalStreamingMethod( request_iterator, target, options=(), channel_credentials=None, call_credentials=None, compression=None, wait_for_ready=None, timeout=None, metadata=None, ): return grpc.experimental.stream_stream( request_iterator, target, "/GRPCTestServer/BidirectionalStreamingMethod", test__server__pb2.Request.SerializeToString, test__server__pb2.Response.FromString, options, channel_credentials, call_credentials, compression, wait_for_ready, timeout, metadata, ) test_aio_client_interceptor.py000066400000000000000000000275561527070072300413350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import IsolatedAsyncioTestCase from unittest.mock import patch import grpc import opentelemetry.instrumentation.grpc from opentelemetry import trace from opentelemetry.instrumentation.grpc import ( GrpcAioInstrumentorClient, aio_client_interceptors, ) from opentelemetry.instrumentation.grpc._aio_client import ( UnaryUnaryAioClientInterceptor, ) from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.semconv._incubating.attributes.rpc_attributes import ( RPC_GRPC_STATUS_CODE, RPC_METHOD, RPC_SERVICE, RPC_SYSTEM, ) from opentelemetry.test.mock_textmap import MockTextMapPropagator from opentelemetry.test.test_base import TestBase from ._aio_client import ( bidirectional_streaming_method, client_streaming_method, server_streaming_method, simple_method, ) from ._server import create_test_server from .protobuf import test_server_pb2_grpc # pylint: disable=no-name-in-module class RecordingInterceptor(grpc.aio.UnaryUnaryClientInterceptor): recorded_details = None async def intercept_unary_unary(self, continuation, client_call_details, request): self.recorded_details = client_call_details return await continuation(client_call_details, request) class TestAioClientInterceptor(TestBase, IsolatedAsyncioTestCase): def setUp(self): super().setUp() self.server = create_test_server(25565) self.server.start() interceptors = aio_client_interceptors() self._channel = grpc.aio.insecure_channel("localhost:25565", interceptors=interceptors) self._stub = test_server_pb2_grpc.GRPCTestServerStub(self._channel) def tearDown(self): super().tearDown() self.server.stop(1000) async def asyncTearDown(self): await self._channel.close() async def test_instrument(self): instrumentor = GrpcAioInstrumentorClient() try: instrumentor.instrument() channel = grpc.aio.insecure_channel("localhost:25565") stub = test_server_pb2_grpc.GRPCTestServerStub(channel) response = await simple_method(stub) assert response.response_data == "data" spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) finally: instrumentor.uninstrument() async def test_uninstrument(self): instrumentor = GrpcAioInstrumentorClient() instrumentor.instrument() instrumentor.uninstrument() channel = grpc.aio.insecure_channel("localhost:25565") stub = test_server_pb2_grpc.GRPCTestServerStub(channel) response = await simple_method(stub) assert response.response_data == "data" spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) async def test_unary_unary(self): response = await simple_method(self._stub) assert response.response_data == "data" spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) self.assertSpanHasAttributes( span, { RPC_METHOD: "SimpleMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) async def test_unary_stream(self): async for response in server_streaming_method(self._stub): self.assertEqual(response.response_data, "data") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/ServerStreamingMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) self.assertSpanHasAttributes( span, { RPC_METHOD: "ServerStreamingMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) async def test_stream_unary(self): response = await client_streaming_method(self._stub) assert response.response_data == "data" spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/ClientStreamingMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) self.assertSpanHasAttributes( span, { RPC_METHOD: "ClientStreamingMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) async def test_stream_stream(self): async for response in bidirectional_streaming_method(self._stub): self.assertEqual(response.response_data, "data") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/BidirectionalStreamingMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) self.assertSpanHasAttributes( span, { RPC_METHOD: "BidirectionalStreamingMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) async def test_error_simple(self): with self.assertRaises(grpc.RpcError): await simple_method(self._stub, error=True) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) self.assertEqual( span.attributes[RPC_GRPC_STATUS_CODE], grpc.StatusCode.INVALID_ARGUMENT.value[0], ) async def test_error_unary_stream(self): with self.assertRaises(grpc.RpcError): async for _ in server_streaming_method(self._stub, error=True): pass spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) self.assertEqual( span.attributes[RPC_GRPC_STATUS_CODE], grpc.StatusCode.INVALID_ARGUMENT.value[0], ) async def test_error_stream_unary(self): with self.assertRaises(grpc.RpcError): await client_streaming_method(self._stub, error=True) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) self.assertEqual( span.attributes[RPC_GRPC_STATUS_CODE], grpc.StatusCode.INVALID_ARGUMENT.value[0], ) async def test_error_stream_stream(self): with self.assertRaises(grpc.RpcError): async for _ in bidirectional_streaming_method(self._stub, error=True): pass spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) self.assertEqual( span.attributes[RPC_GRPC_STATUS_CODE], grpc.StatusCode.INVALID_ARGUMENT.value[0], ) # pylint:disable=no-self-use async def test_client_interceptor_trace_context_propagation(self): """ensure that client interceptor correctly inject trace context into all outgoing requests.""" previous_propagator = get_global_textmap() try: set_global_textmap(MockTextMapPropagator()) interceptor = UnaryUnaryAioClientInterceptor(trace.NoOpTracer()) recording_interceptor = RecordingInterceptor() interceptors = [interceptor, recording_interceptor] channel = grpc.aio.insecure_channel("localhost:25565", interceptors=interceptors) stub = test_server_pb2_grpc.GRPCTestServerStub(channel) await simple_method(stub) metadata = recording_interceptor.recorded_details.metadata assert len(metadata) == 3 assert metadata.get_all("key") == ["value"] assert metadata.get_all("mock-traceid") == ["0"] assert metadata.get_all("mock-spanid") == ["0"] finally: set_global_textmap(previous_propagator) async def test_unary_unary_with_suppress_key(self): with suppress_instrumentation(): response = await simple_method(self._stub) assert response.response_data == "data" spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) async def test_unary_stream_with_suppress_key(self): with suppress_instrumentation(): async for response in server_streaming_method(self._stub): self.assertEqual(response.response_data, "data") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) async def test_stream_unary_with_suppress_key(self): with suppress_instrumentation(): response = await client_streaming_method(self._stub) assert response.response_data == "data" spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) async def test_stream_stream_with_suppress_key(self): with suppress_instrumentation(): async for response in bidirectional_streaming_method(self._stub): self.assertEqual(response.response_data, "data") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) async def test_unary_unary_add_done_callback_not_implemented(self): """Span should still be finished when add_done_callback raises NotImplementedError.""" def _raise_not_implemented(self_call, callback): raise NotImplementedError with patch.object( grpc.aio.UnaryUnaryCall, "add_done_callback", _raise_not_implemented, ): response = await simple_method(self._stub) assert response.response_data == "data" spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertSpanHasAttributes( span, { RPC_METHOD: "SimpleMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) test_aio_client_interceptor_filter.py000066400000000000000000000103571527070072300426710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os from unittest import IsolatedAsyncioTestCase, mock import grpc from opentelemetry.instrumentation.grpc import ( GrpcAioInstrumentorClient, aio_client_interceptors, filters, ) from opentelemetry.test.test_base import TestBase from ._aio_client import ( bidirectional_streaming_method, client_streaming_method, server_streaming_method, simple_method, ) from ._server import create_test_server from .protobuf import test_server_pb2_grpc # pylint: disable=no-name-in-module class TestAioClientInterceptorFiltered(TestBase, IsolatedAsyncioTestCase): def setUp(self): super().setUp() self.server = create_test_server(25565) self.server.start() interceptors = aio_client_interceptors(filter_=filters.method_name("NotSimpleMethod")) self._channel = grpc.aio.insecure_channel("localhost:25565", interceptors=interceptors) self._stub = test_server_pb2_grpc.GRPCTestServerStub(self._channel) def tearDown(self): super().tearDown() self.server.stop(1000) async def asyncTearDown(self): await self._channel.close() async def test_instrument_filtered(self): instrumentor = GrpcAioInstrumentorClient(filter_=filters.method_name("NotSimpleMethod")) try: instrumentor.instrument() channel = grpc.aio.insecure_channel("localhost:25565") stub = test_server_pb2_grpc.GRPCTestServerStub(channel) response = await simple_method(stub) assert response.response_data == "data" spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) finally: instrumentor.uninstrument() async def test_instrument_filtered_env(self): with mock.patch.dict( os.environ, {"OTEL_PYTHON_GRPC_EXCLUDED_SERVICES": "GRPCMockServer,GRPCTestServer"}, ): instrumentor = GrpcAioInstrumentorClient() try: instrumentor.instrument() channel = grpc.aio.insecure_channel("localhost:25565") stub = test_server_pb2_grpc.GRPCTestServerStub(channel) response = await simple_method(stub) assert response.response_data == "data" spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) finally: instrumentor.uninstrument() async def test_instrument_filtered_env_and_option(self): with mock.patch.dict( os.environ, {"OTEL_PYTHON_GRPC_EXCLUDED_SERVICES": "GRPCMockServer"}, ): instrumentor = GrpcAioInstrumentorClient(filter_=filters.service_prefix("GRPCTestServer")) try: instrumentor.instrument() channel = grpc.aio.insecure_channel("localhost:25565") stub = test_server_pb2_grpc.GRPCTestServerStub(channel) response = await simple_method(stub) assert response.response_data == "data" spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) finally: instrumentor.uninstrument() async def test_unary_unary_filtered(self): response = await simple_method(self._stub) assert response.response_data == "data" spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) async def test_unary_stream_filtered(self): async for response in server_streaming_method(self._stub): self.assertEqual(response.response_data, "data") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) async def test_stream_unary_filtered(self): response = await client_streaming_method(self._stub) assert response.response_data == "data" spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) async def test_stream_stream_filtered(self): async for response in bidirectional_streaming_method(self._stub): self.assertEqual(response.response_data, "data") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) test_aio_client_interceptor_hooks.py000066400000000000000000000055361527070072300425320ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import IsolatedAsyncioTestCase import grpc from opentelemetry.instrumentation.grpc import GrpcAioInstrumentorClient from opentelemetry.test.test_base import TestBase from ._aio_client import simple_method from ._server import create_test_server from .protobuf import test_server_pb2_grpc # pylint: disable=no-name-in-module def request_hook(span, request): span.set_attribute("request_data", request.request_data) def response_hook(span, response): span.set_attribute("response_data", response) def request_hook_with_exception(_span, _request): raise Exception() # pylint: disable=broad-exception-raised def response_hook_with_exception(_span, _response): raise Exception() # pylint: disable=broad-exception-raised class TestAioClientInterceptorWithHooks(TestBase, IsolatedAsyncioTestCase): def setUp(self): super().setUp() self.server = create_test_server(25565) self.server.start() def tearDown(self): super().tearDown() self.server.stop(None) async def test_request_and_response_hooks(self): instrumentor = GrpcAioInstrumentorClient() try: instrumentor.instrument( request_hook=request_hook, response_hook=response_hook, ) channel = grpc.aio.insecure_channel( "localhost:25565", ) stub = test_server_pb2_grpc.GRPCTestServerStub(channel) response = await simple_method(stub) assert response.response_data == "data" spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIn("request_data", span.attributes) self.assertEqual(span.attributes["request_data"], "data") self.assertIn("response_data", span.attributes) self.assertEqual(span.attributes["response_data"], "") finally: instrumentor.uninstrument() async def test_hooks_with_exception(self): instrumentor = GrpcAioInstrumentorClient() try: instrumentor.instrument( request_hook=request_hook_with_exception, response_hook=response_hook_with_exception, ) channel = grpc.aio.insecure_channel( "localhost:25565", ) stub = test_server_pb2_grpc.GRPCTestServerStub(channel) response = await simple_method(stub) assert response.response_data == "data" spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod") finally: instrumentor.uninstrument() test_aio_server_interceptor.py000066400000000000000000000603171527070072300413550ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio from unittest import IsolatedAsyncioTestCase import grpc import grpc.aio import opentelemetry.instrumentation.grpc from opentelemetry import trace from opentelemetry.instrumentation.grpc import ( GrpcAioInstrumentorServer, aio_server_interceptor, ) from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.sdk import trace as trace_sdk from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_IP, NET_PEER_NAME, ) from opentelemetry.semconv._incubating.attributes.rpc_attributes import ( RPC_GRPC_STATUS_CODE, RPC_METHOD, RPC_SERVICE, RPC_SYSTEM, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import StatusCode from .protobuf.test_server_pb2 import Request, Response from .protobuf.test_server_pb2_grpc import ( GRPCTestServerServicer, add_GRPCTestServerServicer_to_server, ) # pylint:disable=unused-argument # pylint:disable=no-self-use class Servicer(GRPCTestServerServicer): """Our test servicer""" # pylint:disable=C0103 async def SimpleMethod(self, request, context): return Response( server_id=request.client_id, response_data=request.request_data, ) # pylint:disable=C0103 async def ServerStreamingMethod(self, request, context): for data in ("one", "two", "three"): yield Response( server_id=request.client_id, response_data=data, ) class SuppressAioRpcInterceptor(grpc.aio.ServerInterceptor): """An interceptor which runs the rest of the server interceptor chain with instrumentation suppressed.""" async def intercept_service(self, continuation, handler_call_details): with suppress_instrumentation(): return await continuation(handler_call_details) async def run_with_test_server(runnable, servicer=Servicer(), interceptors=None): server = grpc.aio.server(interceptors=interceptors) add_GRPCTestServerServicer_to_server(servicer, server) port = server.add_insecure_port("[::]:0") channel = grpc.aio.insecure_channel(f"localhost:{port:d}") await server.start() try: resp = await runnable(channel) finally: await server.stop(1000) return resp class TestOpenTelemetryAioServerInterceptor(TestBase, IsolatedAsyncioTestCase): async def test_instrumentor(self): """Check that automatic instrumentation configures the interceptor""" rpc_call = "/GRPCTestServer/SimpleMethod" grpc_aio_server_instrumentor = GrpcAioInstrumentorServer() try: grpc_aio_server_instrumentor.instrument() async def request(channel): request = Request(client_id=1, request_data="test") msg = request.SerializeToString() return await channel.unary_unary(rpc_call)(msg) await run_with_test_server(request) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, rpc_call) self.assertIs(span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) # Check attributes self.assertSpanHasAttributes( span, { NET_PEER_IP: "[::1]", NET_PEER_NAME: "localhost", RPC_METHOD: "SimpleMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) finally: grpc_aio_server_instrumentor.uninstrument() async def test_uninstrument(self): """Check that uninstrument removes the interceptor""" rpc_call = "/GRPCTestServer/SimpleMethod" grpc_aio_server_instrumentor = GrpcAioInstrumentorServer() grpc_aio_server_instrumentor.instrument() grpc_aio_server_instrumentor.uninstrument() async def request(channel): request = Request(client_id=1, request_data="test") msg = request.SerializeToString() return await channel.unary_unary(rpc_call)(msg) await run_with_test_server(request) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) async def test_create_span(self): """Check that the interceptor wraps calls with spans server-side.""" rpc_call = "/GRPCTestServer/SimpleMethod" async def request(channel): request = Request(client_id=1, request_data="test") msg = request.SerializeToString() return await channel.unary_unary(rpc_call)(msg) await run_with_test_server(request, interceptors=[aio_server_interceptor()]) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, rpc_call) self.assertIs(span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) # Check attributes self.assertSpanHasAttributes( span, { NET_PEER_IP: "[::1]", NET_PEER_NAME: "localhost", RPC_METHOD: "SimpleMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) async def test_create_two_spans(self): """Verify that the interceptor captures sub spans within the given trace""" rpc_call = "/GRPCTestServer/SimpleMethod" class TwoSpanServicer(GRPCTestServerServicer): # pylint:disable=C0103 async def SimpleMethod(self, request, context): # create another span tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("child") as child: child.add_event("child event") return Response( server_id=request.client_id, response_data=request.request_data, ) async def request(channel): request = Request(client_id=1, request_data="test") msg = request.SerializeToString() return await channel.unary_unary(rpc_call)(msg) await run_with_test_server( request, servicer=TwoSpanServicer(), interceptors=[aio_server_interceptor()], ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 2) child_span = spans_list[0] parent_span = spans_list[1] self.assertEqual(parent_span.name, rpc_call) self.assertIs(parent_span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(parent_span, opentelemetry.instrumentation.grpc) # Check attributes self.assertSpanHasAttributes( parent_span, { NET_PEER_IP: "[::1]", NET_PEER_NAME: "localhost", RPC_METHOD: "SimpleMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) # Check the child span self.assertEqual(child_span.name, "child") self.assertEqual(parent_span.context.trace_id, child_span.context.trace_id) async def test_create_span_streaming(self): """Check that the interceptor wraps calls with spans server-side, on a streaming call.""" rpc_call = "/GRPCTestServer/ServerStreamingMethod" async def request(channel): request = Request(client_id=1, request_data="test") msg = request.SerializeToString() async for response in channel.unary_stream(rpc_call)(msg): print(response) await run_with_test_server(request, interceptors=[aio_server_interceptor()]) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, rpc_call) self.assertIs(span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) # Check attributes self.assertSpanHasAttributes( span, { NET_PEER_IP: "[::1]", NET_PEER_NAME: "localhost", RPC_METHOD: "ServerStreamingMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) async def test_create_two_spans_streaming(self): """Verify that the interceptor captures sub spans within the given trace""" rpc_call = "/GRPCTestServer/ServerStreamingMethod" class TwoSpanServicer(GRPCTestServerServicer): # pylint:disable=C0103 async def ServerStreamingMethod(self, request, context): # create another span tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("child") as child: child.add_event("child event") for data in ("one", "two", "three"): yield Response( server_id=request.client_id, response_data=data, ) async def request(channel): request = Request(client_id=1, request_data="test") msg = request.SerializeToString() async for response in channel.unary_stream(rpc_call)(msg): print(response) await run_with_test_server( request, servicer=TwoSpanServicer(), interceptors=[aio_server_interceptor()], ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 2) child_span = spans_list[0] parent_span = spans_list[1] self.assertEqual(parent_span.name, rpc_call) self.assertIs(parent_span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(parent_span, opentelemetry.instrumentation.grpc) # Check attributes self.assertSpanHasAttributes( parent_span, { NET_PEER_IP: "[::1]", NET_PEER_NAME: "localhost", RPC_METHOD: "ServerStreamingMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) # Check the child span self.assertEqual(child_span.name, "child") self.assertEqual(parent_span.context.trace_id, child_span.context.trace_id) async def test_suppress_instrumentation(self): """Check that no span is created for a call when instrumentation is suppressed.""" rpc_call = "/GRPCTestServer/SimpleMethod" async def request(channel): request = Request(client_id=1, request_data="test") msg = request.SerializeToString() return await channel.unary_unary(rpc_call)(msg) await run_with_test_server( request, interceptors=[ SuppressAioRpcInterceptor(), aio_server_interceptor(), ], ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) async def test_suppress_instrumentation_streaming(self): """Check that no span is created for a streaming call when instrumentation is suppressed.""" rpc_call = "/GRPCTestServer/ServerStreamingMethod" async def request(channel): request = Request(client_id=1, request_data="test") msg = request.SerializeToString() async for response in channel.unary_stream(rpc_call)(msg): print(response) await run_with_test_server( request, interceptors=[ SuppressAioRpcInterceptor(), aio_server_interceptor(), ], ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) async def test_span_lifetime(self): """Verify that the interceptor captures sub spans within the given trace""" rpc_call = "/GRPCTestServer/SimpleMethod" class SpanLifetimeServicer(GRPCTestServerServicer): # pylint:disable=C0103 async def SimpleMethod(self, request, context): self.span = trace.get_current_span() return Response( server_id=request.client_id, response_data=request.request_data, ) async def request(channel): request = Request(client_id=1, request_data="test") msg = request.SerializeToString() return await channel.unary_unary(rpc_call)(msg) lifetime_servicer = SpanLifetimeServicer() active_span_before_call = trace.get_current_span() await run_with_test_server( request, servicer=lifetime_servicer, interceptors=[aio_server_interceptor()], ) active_span_in_handler = lifetime_servicer.span active_span_after_call = trace.get_current_span() self.assertEqual(active_span_before_call, trace.INVALID_SPAN) self.assertEqual(active_span_after_call, trace.INVALID_SPAN) self.assertIsInstance(active_span_in_handler, trace_sdk.Span) self.assertIsNone(active_span_in_handler.parent) async def test_sequential_server_spans(self): """Check that sequential RPCs get separate server spans.""" rpc_call = "/GRPCTestServer/SimpleMethod" async def request(channel): request = Request(client_id=1, request_data="test") msg = request.SerializeToString() return await channel.unary_unary(rpc_call)(msg) async def sequential_requests(channel): await request(channel) await request(channel) await run_with_test_server(sequential_requests, interceptors=[aio_server_interceptor()]) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 2) span1 = spans_list[0] span2 = spans_list[1] # Spans should belong to separate traces self.assertNotEqual(span1.context.span_id, span2.context.span_id) self.assertNotEqual(span1.context.trace_id, span2.context.trace_id) for span in (span1, span2): # each should be a root span self.assertIsNone(span2.parent) # check attributes self.assertSpanHasAttributes( span, { NET_PEER_IP: "[::1]", NET_PEER_NAME: "localhost", RPC_METHOD: "SimpleMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) async def test_concurrent_server_spans(self): """Check that concurrent RPC calls don't interfere with each other. This is the same check as test_sequential_server_spans except that the RPCs are concurrent. Two handlers are invoked at the same time on two separate threads. Each one should see a different active span and context. """ rpc_call = "/GRPCTestServer/SimpleMethod" latch = get_latch(2) class LatchedServicer(GRPCTestServerServicer): # pylint:disable=C0103 async def SimpleMethod(self, request, context): await latch() return Response( server_id=request.client_id, response_data=request.request_data, ) async def request(channel): request = Request(client_id=1, request_data="test") msg = request.SerializeToString() return await channel.unary_unary(rpc_call)(msg) async def concurrent_requests(channel): await asyncio.gather(request(channel), request(channel)) await run_with_test_server( concurrent_requests, servicer=LatchedServicer(), interceptors=[aio_server_interceptor()], ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 2) span1 = spans_list[0] span2 = spans_list[1] # Spans should belong to separate traces self.assertNotEqual(span1.context.span_id, span2.context.span_id) self.assertNotEqual(span1.context.trace_id, span2.context.trace_id) for span in (span1, span2): # each should be a root span self.assertIsNone(span2.parent) # check attributes self.assertSpanHasAttributes( span, { NET_PEER_IP: "[::1]", NET_PEER_NAME: "localhost", RPC_METHOD: "SimpleMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) async def test_abort(self): """Check that we can catch an abort properly""" rpc_call = "/GRPCTestServer/SimpleMethod" failure_message = "failure message" class AbortServicer(GRPCTestServerServicer): # pylint:disable=C0103 async def SimpleMethod(self, request, context): await context.abort(grpc.StatusCode.INTERNAL, failure_message) testcase = self async def request(channel): request = Request(client_id=1, request_data=failure_message) msg = request.SerializeToString() with testcase.assertRaises(grpc.RpcError) as cm: await channel.unary_unary(rpc_call)(msg) self.assertEqual(cm.exception.code(), grpc.StatusCode.INTERNAL) self.assertEqual(cm.exception.details(), failure_message) await run_with_test_server( request, servicer=AbortServicer(), interceptors=[aio_server_interceptor()], ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, rpc_call) self.assertIs(span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) # make sure this span errored, with the right status and detail self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual( span.status.description, f"{grpc.StatusCode.INTERNAL}:{failure_message}", ) # Check attributes self.assertSpanHasAttributes( span, { NET_PEER_IP: "[::1]", NET_PEER_NAME: "localhost", RPC_METHOD: "SimpleMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.INTERNAL.value[0], }, ) async def test_abort_with_trailing_metadata(self): """Check that we can catch an abort properly when trailing_metadata provided""" rpc_call = "/GRPCTestServer/SimpleMethod" failure_message = "failure message" class AbortServicer(GRPCTestServerServicer): # pylint:disable=C0103 async def SimpleMethod(self, request, context): metadata = (("meta", "data"),) await context.abort( grpc.StatusCode.FAILED_PRECONDITION, failure_message, trailing_metadata=metadata, ) testcase = self async def request(channel): request = Request(client_id=1, request_data=failure_message) msg = request.SerializeToString() with testcase.assertRaises(grpc.RpcError) as cm: await channel.unary_unary(rpc_call)(msg) self.assertEqual(cm.exception.code(), grpc.StatusCode.FAILED_PRECONDITION) self.assertEqual(cm.exception.details(), failure_message) await run_with_test_server( request, servicer=AbortServicer(), interceptors=[aio_server_interceptor()], ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, rpc_call) self.assertIs(span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) # make sure this span errored, with the right status and detail self.assertEqual(span.status.status_code, StatusCode.UNSET) self.assertEqual(span.status.description, None) # Check attributes self.assertSpanHasAttributes( span, { NET_PEER_IP: "[::1]", NET_PEER_NAME: "localhost", RPC_METHOD: "SimpleMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.FAILED_PRECONDITION.value[0], }, ) async def test_non_list_interceptors(self): """Check that we handle non-list interceptors correctly.""" grpc_server_instrumentor = GrpcAioInstrumentorServer() grpc_server_instrumentor.instrument() try: rpc_call = "/GRPCTestServer/SimpleMethod" async def request(channel): request = Request(client_id=1, request_data="test") msg = request.SerializeToString() return await channel.unary_unary(rpc_call)(msg) class MockInterceptor(grpc.aio.ServerInterceptor): async def intercept_service(self, continuation, handler_call_details): return await continuation(handler_call_details) await run_with_test_server(request, interceptors=(MockInterceptor(),)) finally: grpc_server_instrumentor.uninstrument() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, rpc_call) self.assertIs(span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) # Check attributes self.assertSpanHasAttributes( span, { NET_PEER_IP: "[::1]", NET_PEER_NAME: "localhost", RPC_METHOD: "SimpleMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) def get_latch(num): """Get a countdown latch function for use in n threads.""" cv = asyncio.Condition() count = 0 async def countdown_latch(): """Block until n-1 other threads have called.""" nonlocal count async with cv: count += 1 cv.notify() async with cv: while count < num: await cv.wait() return countdown_latch test_aio_server_interceptor_filter.py000066400000000000000000000066711527070072300427250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import IsolatedAsyncioTestCase import grpc import grpc.aio from opentelemetry import trace from opentelemetry.instrumentation.grpc import ( GrpcAioInstrumentorServer, aio_server_interceptor, filters, ) from opentelemetry.test.test_base import TestBase from .protobuf.test_server_pb2 import Request from .protobuf.test_server_pb2_grpc import add_GRPCTestServerServicer_to_server from .test_aio_server_interceptor import Servicer # pylint:disable=unused-argument # pylint:disable=no-self-use async def run_with_test_server(runnable, filter_=None, servicer=Servicer(), add_interceptor=True): if add_interceptor: interceptors = [aio_server_interceptor(filter_=filter_)] server = grpc.aio.server(interceptors=interceptors) else: server = grpc.aio.server() add_GRPCTestServerServicer_to_server(servicer, server) port = server.add_insecure_port("[::]:0") channel = grpc.aio.insecure_channel(f"localhost:{port:d}") await server.start() resp = await runnable(channel) await server.stop(1000) return resp class TestOpenTelemetryAioServerInterceptor(TestBase, IsolatedAsyncioTestCase): async def test_instrumentor(self): """Check that automatic instrumentation configures the interceptor""" rpc_call = "/GRPCTestServer/SimpleMethod" grpc_aio_server_instrumentor = GrpcAioInstrumentorServer(filter_=filters.method_name("NotSimpleMethod")) try: grpc_aio_server_instrumentor.instrument() async def request(channel): request = Request(client_id=1, request_data="test") msg = request.SerializeToString() return await channel.unary_unary(rpc_call)(msg) await run_with_test_server(request, add_interceptor=False) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) finally: grpc_aio_server_instrumentor.uninstrument() async def test_create_span(self): """ Check that the interceptor wraps calls with spans server-side when filter passed and RPC matches the filter. """ rpc_call = "/GRPCTestServer/SimpleMethod" async def request(channel): request = Request(client_id=1, request_data="test") msg = request.SerializeToString() return await channel.unary_unary(rpc_call)(msg) await run_with_test_server( request, filter_=filters.method_name("SimpleMethod"), ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, rpc_call) self.assertIs(span.kind, trace.SpanKind.SERVER) async def test_create_span_filtered(self): """Check that the interceptor wraps calls with spans server-side.""" rpc_call = "/GRPCTestServer/SimpleMethod" async def request(channel): request = Request(client_id=1, request_data="test") msg = request.SerializeToString() return await channel.unary_unary(rpc_call)(msg) await run_with_test_server( request, filter_=filters.method_name("NotSimpleMethod"), ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) test_client_interceptor.py000066400000000000000000000323231527070072300404710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint:disable=cyclic-import from unittest import mock import grpc import opentelemetry.instrumentation.grpc from opentelemetry import trace from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient from opentelemetry.instrumentation.grpc._client import ( OpenTelemetryClientInterceptor, ) from opentelemetry.instrumentation.grpc.grpcext._interceptor import ( _UnaryClientInfo, ) from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.sdk.trace import Span as SdkSpan from opentelemetry.semconv._incubating.attributes.rpc_attributes import ( RPC_GRPC_STATUS_CODE, RPC_METHOD, RPC_SERVICE, RPC_SYSTEM, ) from opentelemetry.test.mock_textmap import MockTextMapPropagator from opentelemetry.test.test_base import TestBase from ._client import ( bidirectional_streaming_method, client_streaming_method, server_streaming_method, simple_method, simple_method_future, ) from ._server import create_test_server from .protobuf import test_server_pb2_grpc from .protobuf.test_server_pb2 import Request # User defined interceptor. Is used in the tests along with the opentelemetry client interceptor. class Interceptor( grpc.UnaryUnaryClientInterceptor, grpc.UnaryStreamClientInterceptor, grpc.StreamUnaryClientInterceptor, grpc.StreamStreamClientInterceptor, ): def __init__(self): pass def intercept_unary_unary(self, continuation, client_call_details, request): return self._intercept_call(continuation, client_call_details, request) def intercept_unary_stream(self, continuation, client_call_details, request): return self._intercept_call(continuation, client_call_details, request) def intercept_stream_unary(self, continuation, client_call_details, request_iterator): return self._intercept_call(continuation, client_call_details, request_iterator) def intercept_stream_stream(self, continuation, client_call_details, request_iterator): return self._intercept_call(continuation, client_call_details, request_iterator) @staticmethod def _intercept_call(continuation, client_call_details, request_or_iterator): return continuation(client_call_details, request_or_iterator) class TestClientProto(TestBase): def setUp(self): super().setUp() GrpcInstrumentorClient().instrument() self.server = create_test_server(25565) self.server.start() # use a user defined interceptor along with the opentelemetry client interceptor interceptors = [Interceptor()] self.channel = grpc.insecure_channel("localhost:25565") self.channel = grpc.intercept_channel(self.channel, *interceptors) self._stub = test_server_pb2_grpc.GRPCTestServerStub(self.channel) def tearDown(self): super().tearDown() GrpcInstrumentorClient().uninstrument() self.server.stop(None) self.channel.close() def test_unary_unary_future(self): simple_method_future(self._stub).result() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) def test_unary_unary(self): simple_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) self.assertSpanHasAttributes( span, { RPC_METHOD: "SimpleMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) def test_unary_stream(self): server_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/ServerStreamingMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) self.assertSpanHasAttributes( span, { RPC_METHOD: "ServerStreamingMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) def test_stream_unary(self): client_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/ClientStreamingMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) self.assertSpanHasAttributes( span, { RPC_METHOD: "ClientStreamingMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) def test_stream_stream(self): bidirectional_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/BidirectionalStreamingMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) self.assertSpanHasAttributes( span, { RPC_METHOD: "BidirectionalStreamingMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) def test_stream_stream_preserves_call_interface(self): """Regression test for issue #1180. Bidirectional streaming RPCs must return an object that implements grpc.Call (add_done_callback, cancel, is_active, etc.) rather than a bare generator. Before the fix, bidi streams were routed through the generator-based _intercept_server_stream, which stripped the grpc.Call interface and caused downstream code to crash with: AttributeError: 'generator' object has no attribute 'add_done_callback' """ def request_messages(): for _ in range(5): yield Request(client_id=1, request_data="data") response_iterator = self._stub.BidirectionalStreamingMethod(request_messages(), metadata=(("key", "value"),)) for attr in ("add_done_callback", "cancel", "is_active"): self.assertTrue( hasattr(response_iterator, attr), f"bidi stream response missing grpc.Call method '{attr}'", ) list(response_iterator) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/BidirectionalStreamingMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertSpanHasAttributes( span, { RPC_METHOD: "BidirectionalStreamingMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) def test_error_simple(self): with self.assertRaises(grpc.RpcError): simple_method(self._stub, error=True) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) self.assertEqual( span.attributes[RPC_GRPC_STATUS_CODE], grpc.StatusCode.INVALID_ARGUMENT.value[0], ) def test_error_stream_unary(self): with self.assertRaises(grpc.RpcError): client_streaming_method(self._stub, error=True) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) self.assertEqual( span.attributes[RPC_GRPC_STATUS_CODE], grpc.StatusCode.INVALID_ARGUMENT.value[0], ) def test_error_unary_stream(self): with self.assertRaises(grpc.RpcError): server_streaming_method(self._stub, error=True) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) self.assertEqual( span.attributes[RPC_GRPC_STATUS_CODE], grpc.StatusCode.INVALID_ARGUMENT.value[0], ) def test_error_stream_stream(self): with self.assertRaises(grpc.RpcError): bidirectional_streaming_method(self._stub, error=True) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) self.assertEqual( span.attributes[RPC_GRPC_STATUS_CODE], grpc.StatusCode.INVALID_ARGUMENT.value[0], ) def test_client_interceptor_falsy_response( self, ): """ensure that client interceptor closes the span only once even if the response is falsy.""" with mock.patch.object(SdkSpan, "end") as span_end_mock: tracer_provider, _exporter = self.create_tracer_provider() tracer = tracer_provider.get_tracer(__name__) interceptor = OpenTelemetryClientInterceptor(tracer) def invoker(_request, _metadata): return {} request = Request(client_id=1, request_data="data") interceptor.intercept_unary( request, {}, _UnaryClientInfo( full_method="/GRPCTestServer/SimpleMethod", timeout=None, ), invoker=invoker, ) self.assertEqual(span_end_mock.call_count, 1) def test_client_interceptor_trace_context_propagation( self, ): # pylint: disable=no-self-use """ensure that client interceptor correctly inject trace context into all outgoing requests.""" previous_propagator = get_global_textmap() try: set_global_textmap(MockTextMapPropagator()) interceptor = OpenTelemetryClientInterceptor(trace.NoOpTracer()) carrier = tuple() def invoker(request, metadata): nonlocal carrier carrier = metadata return {} request = Request(client_id=1, request_data="data") interceptor.intercept_unary( request, {}, _UnaryClientInfo(full_method="/GRPCTestServer/SimpleMethod", timeout=None), invoker=invoker, ) assert len(carrier) == 2 assert carrier[0][0] == "mock-traceid" assert carrier[0][1] == "0" assert carrier[1][0] == "mock-spanid" assert carrier[1][1] == "0" finally: set_global_textmap(previous_propagator) def test_unary_unary_with_suppress_key(self): with suppress_instrumentation(): simple_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_unary_stream_with_suppress_key(self): with suppress_instrumentation(): server_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_stream_unary_with_suppress_key(self): with suppress_instrumentation(): client_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_stream_stream_with_suppress_key(self): with suppress_instrumentation(): bidirectional_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) test_client_interceptor_filter.py000066400000000000000000000517561527070072300420510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint:disable=cyclic-import import os from unittest import mock import grpc import opentelemetry.instrumentation.grpc from opentelemetry import trace from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient, filters from opentelemetry.instrumentation.grpc._client import ( OpenTelemetryClientInterceptor, ) from opentelemetry.instrumentation.grpc.grpcext._interceptor import ( _UnaryClientInfo, ) from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.semconv._incubating.attributes.rpc_attributes import ( RPC_GRPC_STATUS_CODE, RPC_METHOD, RPC_SERVICE, RPC_SYSTEM, ) from opentelemetry.test.mock_textmap import MockTextMapPropagator from opentelemetry.test.test_base import TestBase from ._client import ( bidirectional_streaming_method, client_streaming_method, server_streaming_method, simple_method, simple_method_future, ) from ._server import create_test_server from .protobuf import test_server_pb2_grpc from .protobuf.test_server_pb2 import Request # User defined interceptor. Is used in the tests along with the opentelemetry client interceptor. class Interceptor( grpc.UnaryUnaryClientInterceptor, grpc.UnaryStreamClientInterceptor, grpc.StreamUnaryClientInterceptor, grpc.StreamStreamClientInterceptor, ): def __init__(self): pass def intercept_unary_unary(self, continuation, client_call_details, request): return self._intercept_call(continuation, client_call_details, request) def intercept_unary_stream(self, continuation, client_call_details, request): return self._intercept_call(continuation, client_call_details, request) def intercept_stream_unary(self, continuation, client_call_details, request_iterator): return self._intercept_call(continuation, client_call_details, request_iterator) def intercept_stream_stream(self, continuation, client_call_details, request_iterator): return self._intercept_call(continuation, client_call_details, request_iterator) @staticmethod def _intercept_call(continuation, client_call_details, request_or_iterator): return continuation(client_call_details, request_or_iterator) class TestClientProtoFilterMethodName(TestBase): def setUp(self): super().setUp() GrpcInstrumentorClient(filter_=filters.method_name("SimpleMethod")).instrument() self.server = create_test_server(25565) self.server.start() # use a user defined interceptor along with the opentelemetry client interceptor interceptors = [Interceptor()] self.channel = grpc.insecure_channel("localhost:25565") self.channel = grpc.intercept_channel(self.channel, *interceptors) self._stub = test_server_pb2_grpc.GRPCTestServerStub(self.channel) def tearDown(self): super().tearDown() GrpcInstrumentorClient().uninstrument() self.server.stop(None) self.channel.close() def test_unary_unary_future(self): simple_method_future(self._stub).result() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) def test_unary_unary(self): simple_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) self.assertSpanHasAttributes( span, { RPC_METHOD: "SimpleMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) def test_unary_stream(self): server_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_stream_unary(self): client_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_stream_stream(self): bidirectional_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_error_simple(self): with self.assertRaises(grpc.RpcError): simple_method(self._stub, error=True) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) def test_error_stream_unary(self): with self.assertRaises(grpc.RpcError): client_streaming_method(self._stub, error=True) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_error_unary_stream(self): with self.assertRaises(grpc.RpcError): server_streaming_method(self._stub, error=True) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_error_stream_stream(self): with self.assertRaises(grpc.RpcError): bidirectional_streaming_method(self._stub, error=True) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_client_interceptor_trace_context_propagation( self, ): # pylint: disable=no-self-use """ensure that client interceptor correctly inject trace context into all outgoing requests.""" previous_propagator = get_global_textmap() try: set_global_textmap(MockTextMapPropagator()) interceptor = OpenTelemetryClientInterceptor(trace.NoOpTracer()) carrier = tuple() def invoker(request, metadata): nonlocal carrier carrier = metadata return {} request = Request(client_id=1, request_data="data") interceptor.intercept_unary( request, {}, _UnaryClientInfo(full_method="/GRPCTestServer/SimpleMethod", timeout=None), invoker=invoker, ) assert len(carrier) == 2 assert carrier[0][0] == "mock-traceid" assert carrier[0][1] == "0" assert carrier[1][0] == "mock-spanid" assert carrier[1][1] == "0" finally: set_global_textmap(previous_propagator) class TestClientProtoFilterMethodPrefix(TestBase): def setUp(self): super().setUp() GrpcInstrumentorClient(filter_=filters.method_prefix("Simple")).instrument() self.server = create_test_server(25565) self.server.start() # use a user defined interceptor along with the opentelemetry client interceptor interceptors = [Interceptor()] self.channel = grpc.insecure_channel("localhost:25565") self.channel = grpc.intercept_channel(self.channel, *interceptors) self._stub = test_server_pb2_grpc.GRPCTestServerStub(self.channel) def tearDown(self): super().tearDown() GrpcInstrumentorClient().uninstrument() self.server.stop(None) self.channel.close() def test_unary_unary_future(self): simple_method_future(self._stub).result() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) def test_unary_unary(self): simple_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) self.assertSpanHasAttributes( span, { RPC_METHOD: "SimpleMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) def test_unary_stream(self): server_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_stream_unary(self): client_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_stream_stream(self): bidirectional_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_error_simple(self): with self.assertRaises(grpc.RpcError): simple_method(self._stub, error=True) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) def test_error_stream_unary(self): with self.assertRaises(grpc.RpcError): client_streaming_method(self._stub, error=True) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_error_unary_stream(self): with self.assertRaises(grpc.RpcError): server_streaming_method(self._stub, error=True) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_error_stream_stream(self): with self.assertRaises(grpc.RpcError): bidirectional_streaming_method(self._stub, error=True) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_client_interceptor_trace_context_propagation( self, ): # pylint: disable=no-self-use """ensure that client interceptor correctly inject trace context into all outgoing requests.""" previous_propagator = get_global_textmap() try: set_global_textmap(MockTextMapPropagator()) interceptor = OpenTelemetryClientInterceptor(trace.NoOpTracer()) carrier = tuple() def invoker(request, metadata): nonlocal carrier carrier = metadata return {} request = Request(client_id=1, request_data="data") interceptor.intercept_unary( request, {}, _UnaryClientInfo(full_method="/GRPCTestServer/SimpleMethod", timeout=None), invoker=invoker, ) assert len(carrier) == 2 assert carrier[0][0] == "mock-traceid" assert carrier[0][1] == "0" assert carrier[1][0] == "mock-spanid" assert carrier[1][1] == "0" finally: set_global_textmap(previous_propagator) class TestClientProtoFilterByEnv(TestBase): def setUp(self): with mock.patch.dict( os.environ, {"OTEL_PYTHON_GRPC_EXCLUDED_SERVICES": "GRPCMockServer,GRPCTestServer"}, ): super().setUp() GrpcInstrumentorClient().instrument() self.server = create_test_server(25565) self.server.start() # use a user defined interceptor along with the opentelemetry client interceptor interceptors = [Interceptor()] self.channel = grpc.insecure_channel("localhost:25565") self.channel = grpc.intercept_channel(self.channel, *interceptors) self._stub = test_server_pb2_grpc.GRPCTestServerStub(self.channel) def tearDown(self): super().tearDown() GrpcInstrumentorClient().uninstrument() self.server.stop(None) self.channel.close() def test_unary_unary_future(self): simple_method_future(self._stub).result() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_unary_unary(self): simple_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) class TestClientProtoFilterByEnvAndOption(TestBase): def setUp(self): with mock.patch.dict( os.environ, {"OTEL_PYTHON_GRPC_EXCLUDED_SERVICES": "GRPCMockServer"}, ): super().setUp() GrpcInstrumentorClient(filter_=filters.service_prefix("GRPCTestServer")).instrument() self.server = create_test_server(25565) self.server.start() # use a user defined interceptor along with the opentelemetry client interceptor interceptors = [Interceptor()] self.channel = grpc.insecure_channel("localhost:25565") self.channel = grpc.intercept_channel(self.channel, *interceptors) self._stub = test_server_pb2_grpc.GRPCTestServerStub(self.channel) def tearDown(self): super().tearDown() GrpcInstrumentorClient().uninstrument() self.server.stop(None) self.channel.close() def test_unary_unary_future(self): simple_method_future(self._stub).result() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) def test_unary_unary(self): simple_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) self.assertSpanHasAttributes( span, { RPC_METHOD: "SimpleMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) def test_unary_stream(self): server_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/ServerStreamingMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) self.assertSpanHasAttributes( span, { RPC_METHOD: "ServerStreamingMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) def test_stream_unary(self): client_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/ClientStreamingMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) self.assertSpanHasAttributes( span, { RPC_METHOD: "ClientStreamingMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) def test_stream_stream(self): bidirectional_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/BidirectionalStreamingMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) self.assertSpanHasAttributes( span, { RPC_METHOD: "BidirectionalStreamingMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) def test_error_simple(self): with self.assertRaises(grpc.RpcError): simple_method(self._stub, error=True) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) def test_error_stream_unary(self): with self.assertRaises(grpc.RpcError): client_streaming_method(self._stub, error=True) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) def test_error_unary_stream(self): with self.assertRaises(grpc.RpcError): server_streaming_method(self._stub, error=True) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) def test_error_stream_stream(self): with self.assertRaises(grpc.RpcError): bidirectional_streaming_method(self._stub, error=True) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) def test_client_interceptor_trace_context_propagation( self, ): # pylint: disable=no-self-use """ensure that client interceptor correctly inject trace context into all outgoing requests.""" previous_propagator = get_global_textmap() try: set_global_textmap(MockTextMapPropagator()) interceptor = OpenTelemetryClientInterceptor(trace.NoOpTracer()) carrier = tuple() def invoker(request, metadata): nonlocal carrier carrier = metadata return {} request = Request(client_id=1, request_data="data") interceptor.intercept_unary( request, {}, _UnaryClientInfo(full_method="/GRPCTestServer/SimpleMethod", timeout=None), invoker=invoker, ) assert len(carrier) == 2 assert carrier[0][0] == "mock-traceid" assert carrier[0][1] == "0" assert carrier[1][0] == "mock-spanid" assert carrier[1][1] == "0" finally: set_global_textmap(previous_propagator) def test_unary_unary_with_suppress_key(self): with suppress_instrumentation(): simple_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_unary_stream_with_suppress_key(self): with suppress_instrumentation(): server_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_stream_unary_with_suppress_key(self): with suppress_instrumentation(): client_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_stream_stream_with_suppress_key(self): with suppress_instrumentation(): bidirectional_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) test_client_interceptor_hooks.py000066400000000000000000000102231527070072300416670ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import grpc from opentelemetry import trace from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient from opentelemetry.test.test_base import TestBase from ._client import simple_method from ._server import create_test_server from .protobuf import test_server_pb2_grpc # User defined interceptor. Is used in the tests along with the opentelemetry client interceptor. class Interceptor( grpc.UnaryUnaryClientInterceptor, grpc.UnaryStreamClientInterceptor, grpc.StreamUnaryClientInterceptor, grpc.StreamStreamClientInterceptor, ): def __init__(self): pass def intercept_unary_unary(self, continuation, client_call_details, request): return self._intercept_call(continuation, client_call_details, request) def intercept_unary_stream(self, continuation, client_call_details, request): return self._intercept_call(continuation, client_call_details, request) def intercept_stream_unary(self, continuation, client_call_details, request_iterator): return self._intercept_call(continuation, client_call_details, request_iterator) def intercept_stream_stream(self, continuation, client_call_details, request_iterator): return self._intercept_call(continuation, client_call_details, request_iterator) @staticmethod def _intercept_call(continuation, client_call_details, request_or_iterator): return continuation(client_call_details, request_or_iterator) def request_hook(span, request): span.set_attribute("request_data", request.request_data) def response_hook(span, response): span.set_attribute("response_data", response.response_data) def request_hook_with_exception(_span, _request): raise Exception() # pylint: disable=broad-exception-raised def response_hook_with_exception(_span, _response): raise Exception() # pylint: disable=broad-exception-raised class TestHooks(TestBase): def setUp(self): super().setUp() self.server = create_test_server(25565) self.server.start() # use a user defined interceptor along with the opentelemetry client interceptor self.interceptors = [Interceptor()] def tearDown(self): super().tearDown() self.server.stop(None) def test_response_and_request_hooks(self): instrumentor = GrpcInstrumentorClient() try: instrumentor.instrument( request_hook=request_hook, response_hook=response_hook, ) channel = grpc.insecure_channel("localhost:25565") channel = grpc.intercept_channel(channel, *self.interceptors) stub = test_server_pb2_grpc.GRPCTestServerStub(channel) simple_method(stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertIn("request_data", span.attributes) self.assertEqual(span.attributes["request_data"], "data") self.assertIn("response_data", span.attributes) self.assertEqual(span.attributes["response_data"], "data") finally: instrumentor.uninstrument() def test_hooks_with_exception(self): instrumentor = GrpcInstrumentorClient() try: instrumentor.instrument( request_hook=request_hook_with_exception, response_hook=response_hook_with_exception, ) channel = grpc.insecure_channel("localhost:25565") channel = grpc.intercept_channel(channel, *self.interceptors) stub = test_server_pb2_grpc.GRPCTestServerStub(channel) simple_method(stub) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod") self.assertIs(span.kind, trace.SpanKind.CLIENT) finally: instrumentor.uninstrument() test_filters.py000066400000000000000000000224141527070072300362450ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import collections import grpc import pytest from opentelemetry.instrumentation.grpc import filters class _HandlerCallDetails( collections.namedtuple( "_HanlderCallDetails", ( "method", "invocation_metadata", ), ), grpc.HandlerCallDetails, ): pass class _UnaryClientInfo(collections.namedtuple("_UnaryClientInfo", ("full_method", "timeout"))): pass class _StreamClientInfo( collections.namedtuple( "_StreamClientInfo", ("full_method", "is_client_stream", "is_server_stream", "timeout"), ) ): pass @pytest.mark.parametrize( "test_case", [ ( True, "SimpleMethod", _HandlerCallDetails( method="SimpleMethod", invocation_metadata=[("tracer", "foo"), ("caller", "bar")], ), ), ( True, "SimpleMethod", grpc.aio.ClientCallDetails( method="SimpleMethod", timeout=3000, metadata=None, credentials=None, wait_for_ready=None, ), ), ( True, "SimpleMethod", grpc.aio.ClientCallDetails( method=b"SimpleMethod", timeout=3000, metadata=None, credentials=None, wait_for_ready=None, ), ), ( False, "SimpleMethod", grpc.aio.ClientCallDetails( method="NotSimpleMethod", timeout=3000, metadata=None, credentials=None, wait_for_ready=None, ), ), ( False, "SimpleMethod", _HandlerCallDetails( method="NotSimpleMethod", invocation_metadata=[("tracer", "foo"), ("caller", "bar")], ), ), ( True, "SimpleMethod", _UnaryClientInfo( full_method="/GRPCTestServer/SimpleMethod", timeout=3000, ), ), ( False, "SimpleMethod", _UnaryClientInfo( full_method="/GRPCTestServer/NotSimpleMethod", timeout=3000, ), ), ( True, "SimpleMethod", _StreamClientInfo( full_method="/GRPCTestServer/SimpleMethod", is_client_stream=True, is_server_stream=False, timeout=3000, ), ), ( False, "SimpleMethod", _StreamClientInfo( full_method="/GRPCTestServer/NotSimpleMethod", is_client_stream=True, is_server_stream=False, timeout=3000, ), ), ], ) def test_method_name(test_case): fn = filters.method_name(test_case[1]) assert test_case[0] == fn(test_case[2]) @pytest.mark.parametrize( "test_case", [ ( True, "Simple", _HandlerCallDetails( method="SimpleMethod", invocation_metadata=[("tracer", "foo"), ("caller", "bar")], ), ), ( False, "Simple", _HandlerCallDetails( method="NotSimpleMethod", invocation_metadata=[("tracer", "foo"), ("caller", "bar")], ), ), ( True, "Simple", _UnaryClientInfo( full_method="/GRPCTestServer/SimpleMethod", timeout=3000, ), ), ( False, "Simple", _UnaryClientInfo( full_method="/GRPCTestServer/NotSimpleMethod", timeout=3000, ), ), ( True, "Simple", _StreamClientInfo( full_method="/GRPCTestServer/SimpleMethod", is_client_stream=True, is_server_stream=False, timeout=3000, ), ), ( False, "Simple", _StreamClientInfo( full_method="/GRPCTestServer/NotSimpleMethod", is_client_stream=True, is_server_stream=False, timeout=3000, ), ), ], ) def test_method_prefix(test_case): fn = filters.method_prefix(test_case[1]) assert test_case[0] == fn(test_case[2]) @pytest.mark.parametrize( "test_case", [ ( True, "GRPCTestServer", _UnaryClientInfo( full_method="/GRPCTestServer/SimpleMethod", timeout=3000, ), ), ( False, "GRPCTestServer", _UnaryClientInfo( full_method="/GRPCRealServer/SimpleMethod", timeout=3000, ), ), ( True, "GRPCTestServer", _StreamClientInfo( full_method="/GRPCTestServer/SimpleMethod", is_client_stream=True, is_server_stream=False, timeout=3000, ), ), ( False, "GRPCTestServer", _StreamClientInfo( full_method="/GRPCRealServer/SimpleMethod", is_client_stream=True, is_server_stream=False, timeout=3000, ), ), ], ) def test_service_name(test_case): fn = filters.service_name(test_case[1]) assert test_case[0] == fn(test_case[2]) @pytest.mark.parametrize( "test_case", [ ( True, "GRPCTest", _UnaryClientInfo( full_method="/GRPCTestServer/SimpleMethod", timeout=3000, ), ), ( False, "GRPCTest", _UnaryClientInfo( full_method="/GRPCRealServer/SimpleMethod", timeout=3000, ), ), ( True, "GRPCTest", _StreamClientInfo( full_method="/GRPCTestServer/SimpleMethod", is_client_stream=True, is_server_stream=False, timeout=3000, ), ), ( False, "GRPCTest", _StreamClientInfo( full_method="/GRPCRealServer/SimpleMethod", is_client_stream=True, is_server_stream=False, timeout=3000, ), ), ], ) def test_service_prefix(test_case): fn = filters.service_prefix(test_case[1]) assert test_case[0] == fn(test_case[2]) @pytest.mark.parametrize( "test_case", [ ( True, _UnaryClientInfo( full_method="/grpc.health.v1.Health/Check", timeout=3000, ), ), ( False, _UnaryClientInfo( full_method="/GRPCRealServer/SimpleMethod", timeout=3000, ), ), ( True, _StreamClientInfo( full_method="/grpc.health.v1.Health/Check", is_client_stream=True, is_server_stream=False, timeout=3000, ), ), ( False, _StreamClientInfo( full_method="/GRPCRealServer/SimpleMethod", is_client_stream=True, is_server_stream=False, timeout=3000, ), ), ], ) def test_health_check(test_case): fn = filters.health_check() assert test_case[0] == fn(test_case[1]) @pytest.mark.parametrize( "test_case", [ ( True, filters.all_of( filters.method_name("SimpleMethod"), filters.service_name("GRPCTestServer"), ), _UnaryClientInfo( full_method="/GRPCTestServer/SimpleMethod", timeout=3000, ), ), ( True, filters.any_of( filters.method_name("NotSimpleMethod"), filters.service_name("GRPCTestServer"), ), _UnaryClientInfo( full_method="/GRPCTestServer/SimpleMethod", timeout=3000, ), ), ( True, filters.any_of( filters.service_name("GRPCMockServer"), filters.service_name("GRPCTestServer"), ), _UnaryClientInfo( full_method="/GRPCTestServer/SimpleMethod", timeout=3000, ), ), ( False, filters.negate(filters.method_name("SimpleMethod")), _UnaryClientInfo( full_method="/GRPCTestServer/SimpleMethod", timeout=3000, ), ), ], ) def test_all_any_negate(test_case): fn = test_case[1] assert test_case[0] == fn(test_case[2]) test_server_interceptor.py000066400000000000000000000625731527070072300405330ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint:disable=unused-argument # pylint:disable=no-self-use import contextlib import tempfile import threading from concurrent import futures from unittest import mock import grpc import opentelemetry.instrumentation.grpc from opentelemetry import trace from opentelemetry.instrumentation.grpc import ( GrpcInstrumentorServer, server_interceptor, ) from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.sdk import trace as trace_sdk from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_IP, NET_PEER_NAME, ) from opentelemetry.semconv._incubating.attributes.rpc_attributes import ( RPC_GRPC_STATUS_CODE, RPC_METHOD, RPC_SERVICE, RPC_SYSTEM, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import StatusCode from .protobuf.test_server_pb2 import Request, Response from .protobuf.test_server_pb2_grpc import ( GRPCTestServerServicer, add_GRPCTestServerServicer_to_server, ) class UnaryUnaryMethodHandler(grpc.RpcMethodHandler): def __init__(self, handler): self.request_streaming = False self.response_streaming = False self.request_deserializer = None self.response_serializer = None self.unary_unary = handler self.unary_stream = None self.stream_unary = None self.stream_stream = None class UnaryUnaryRpcHandler(grpc.GenericRpcHandler): def __init__(self, handler): self._unary_unary_handler = handler def service(self, handler_call_details): return UnaryUnaryMethodHandler(self._unary_unary_handler) class SuppressRpcInterceptor(grpc.ServerInterceptor): """An interceptor which runs the rest of the server interceptor chain with instrumentation suppressed.""" def intercept_service(self, continuation, handler_call_details): with suppress_instrumentation(): return continuation(handler_call_details) class Servicer(GRPCTestServerServicer): """Our test servicer""" # pylint:disable=C0103 def SimpleMethod(self, request, context): return Response( server_id=request.client_id, response_data=request.request_data, ) # pylint:disable=C0103 def ServerStreamingMethod(self, request, context): for data in ("one", "two", "three"): yield Response( server_id=request.client_id, response_data=data, ) class TestOpenTelemetryServerInterceptor(TestBase): net_peer_span_attributes = { NET_PEER_IP: "[::1]", NET_PEER_NAME: "localhost", } @contextlib.contextmanager def server(self, max_workers=1, interceptors=None): with futures.ThreadPoolExecutor(max_workers=max_workers) as executor: server = grpc.server( executor, options=(("grpc.so_reuseport", 0),), interceptors=interceptors or [], ) port = server.add_insecure_port("[::]:0") channel = grpc.insecure_channel(f"localhost:{port:d}") yield server, channel def test_instrumentor(self): def handler(request, context): return b"" grpc_server_instrumentor = GrpcInstrumentorServer() grpc_server_instrumentor.instrument() try: with self.server(max_workers=1) as (server, channel): server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) rpc_call = "TestServicer/handler" try: server.start() channel.unary_unary(rpc_call)(b"test") finally: server.stop(None) finally: grpc_server_instrumentor.uninstrument() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, rpc_call) self.assertIs(span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) # Check attributes self.assertSpanHasAttributes( span, { **self.net_peer_span_attributes, RPC_METHOD: "handler", RPC_SERVICE: "TestServicer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) def test_uninstrument(self): def handler(request, context): return b"" grpc_server_instrumentor = GrpcInstrumentorServer() grpc_server_instrumentor.instrument() grpc_server_instrumentor.uninstrument() with self.server(max_workers=1) as (server, channel): server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) rpc_call = "TestServicer/test" try: server.start() channel.unary_unary(rpc_call)(b"test") finally: server.stop(None) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) def test_create_span(self): """Check that the interceptor wraps calls with spans server-side.""" # Intercept gRPC calls... interceptor = server_interceptor() with self.server( max_workers=1, interceptors=[interceptor], ) as (server, channel): add_GRPCTestServerServicer_to_server(Servicer(), server) rpc_call = "/GRPCTestServer/SimpleMethod" request = Request(client_id=1, request_data="test") msg = request.SerializeToString() try: server.start() channel.unary_unary(rpc_call)(msg) finally: server.stop(None) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, rpc_call) self.assertIs(span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) # Check attributes self.assertSpanHasAttributes( span, { **self.net_peer_span_attributes, RPC_METHOD: "SimpleMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) def test_create_two_spans(self): """Verify that the interceptor captures sub spans within the given trace""" class TwoSpanServicer(GRPCTestServerServicer): # pylint:disable=C0103 def SimpleMethod(self, request, context): # create another span tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("child") as child: child.add_event("child event") return Response( server_id=request.client_id, response_data=request.request_data, ) # Intercept gRPC calls... interceptor = server_interceptor() # setup the server with self.server( max_workers=1, interceptors=[interceptor], ) as (server, channel): add_GRPCTestServerServicer_to_server(TwoSpanServicer(), server) # setup the RPC rpc_call = "/GRPCTestServer/SimpleMethod" request = Request(client_id=1, request_data="test") msg = request.SerializeToString() try: server.start() channel.unary_unary(rpc_call)(msg) finally: server.stop(None) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 2) child_span = spans_list[0] parent_span = spans_list[1] self.assertEqual(parent_span.name, rpc_call) self.assertIs(parent_span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(parent_span, opentelemetry.instrumentation.grpc) # Check attributes self.assertSpanHasAttributes( parent_span, { **self.net_peer_span_attributes, RPC_METHOD: "SimpleMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) # Check the child span self.assertEqual(child_span.name, "child") self.assertEqual(parent_span.context.trace_id, child_span.context.trace_id) def test_create_span_streaming(self): """Check that the interceptor wraps calls with spans server-side, on a streaming call.""" # Intercept gRPC calls... interceptor = server_interceptor() with self.server( max_workers=1, interceptors=[interceptor], ) as (server, channel): add_GRPCTestServerServicer_to_server(Servicer(), server) # setup the RPC rpc_call = "/GRPCTestServer/ServerStreamingMethod" request = Request(client_id=1, request_data="test") msg = request.SerializeToString() try: server.start() list(channel.unary_stream(rpc_call)(msg)) finally: server.stop(None) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, rpc_call) self.assertIs(span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) # Check attributes self.assertSpanHasAttributes( span, { **self.net_peer_span_attributes, RPC_METHOD: "ServerStreamingMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) def test_create_two_spans_streaming(self): """Verify that the interceptor captures sub spans in a streaming call, within the given trace""" class TwoSpanServicer(GRPCTestServerServicer): # pylint:disable=C0103 def ServerStreamingMethod(self, request, context): # create another span tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("child") as child: child.add_event("child event") for data in ("one", "two", "three"): yield Response( server_id=request.client_id, response_data=data, ) # Intercept gRPC calls... interceptor = server_interceptor() with self.server( max_workers=1, interceptors=[interceptor], ) as (server, channel): add_GRPCTestServerServicer_to_server(TwoSpanServicer(), server) # setup the RPC rpc_call = "/GRPCTestServer/ServerStreamingMethod" request = Request(client_id=1, request_data="test") msg = request.SerializeToString() try: server.start() list(channel.unary_stream(rpc_call)(msg)) finally: server.stop(None) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 2) child_span = spans_list[0] parent_span = spans_list[1] self.assertEqual(parent_span.name, rpc_call) self.assertIs(parent_span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(parent_span, opentelemetry.instrumentation.grpc) # Check attributes self.assertSpanHasAttributes( parent_span, { **self.net_peer_span_attributes, RPC_METHOD: "ServerStreamingMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) # Check the child span self.assertEqual(child_span.name, "child") self.assertEqual(parent_span.context.trace_id, child_span.context.trace_id) def test_suppress_instrumentation(self): """Check that no span is created for a call when instrumentation is suppressed.""" interceptor = server_interceptor() with self.server( max_workers=1, interceptors=[SuppressRpcInterceptor(), interceptor], ) as (server, channel): add_GRPCTestServerServicer_to_server(Servicer(), server) rpc_call = "/GRPCTestServer/SimpleMethod" request = Request(client_id=1, request_data="test") msg = request.SerializeToString() try: server.start() channel.unary_unary(rpc_call)(msg) finally: server.stop(None) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) def test_suppress_instrumentation_streaming(self): """Check that no span is created for a streaming call when instrumentation is suppressed.""" interceptor = server_interceptor() with self.server( max_workers=1, interceptors=[SuppressRpcInterceptor(), interceptor], ) as (server, channel): add_GRPCTestServerServicer_to_server(Servicer(), server) rpc_call = "/GRPCTestServer/ServerStreamingMethod" request = Request(client_id=1, request_data="test") msg = request.SerializeToString() try: server.start() list(channel.unary_stream(rpc_call)(msg)) finally: server.stop(None) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) def test_span_lifetime(self): """Check that the span is active for the duration of the call.""" interceptor = server_interceptor() # To capture the current span at the time the handler is called active_span_in_handler = None def handler(request, context): nonlocal active_span_in_handler active_span_in_handler = trace.get_current_span() return b"" with self.server( max_workers=1, interceptors=[interceptor], ) as (server, channel): server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) active_span_before_call = trace.get_current_span() try: server.start() channel.unary_unary("TestServicer/handler")(b"") finally: server.stop(None) active_span_after_call = trace.get_current_span() self.assertEqual(active_span_before_call, trace.INVALID_SPAN) self.assertEqual(active_span_after_call, trace.INVALID_SPAN) self.assertIsInstance(active_span_in_handler, trace_sdk.Span) self.assertIsNone(active_span_in_handler.parent) def test_sequential_server_spans(self): """Check that sequential RPCs get separate server spans.""" interceptor = server_interceptor() # Capture the currently active span in each thread active_spans_in_handler = [] def handler(request, context): active_spans_in_handler.append(trace.get_current_span()) return b"" with self.server( max_workers=1, interceptors=[interceptor], ) as (server, channel): server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) try: server.start() channel.unary_unary("TestServicer/handler")(b"") channel.unary_unary("TestServicer/handler")(b"") finally: server.stop(None) self.assertEqual(len(active_spans_in_handler), 2) # pylint:disable=unbalanced-tuple-unpacking span1, span2 = active_spans_in_handler # Spans should belong to separate traces self.assertNotEqual(span1.context.span_id, span2.context.span_id) self.assertNotEqual(span1.context.trace_id, span2.context.trace_id) for span in (span1, span2): # each should be a root span self.assertIsNone(span2.parent) # check attributes self.assertSpanHasAttributes( span, { **self.net_peer_span_attributes, RPC_METHOD: "handler", RPC_SERVICE: "TestServicer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) def test_concurrent_server_spans(self): """Check that concurrent RPC calls don't interfere with each other. This is the same check as test_sequential_server_spans except that the RPCs are concurrent. Two handlers are invoked at the same time on two separate threads. Each one should see a different active span and context. """ interceptor = server_interceptor() # Capture the currently active span in each thread active_spans_in_handler = [] latch = get_latch(2) def handler(request, context): latch() active_spans_in_handler.append(trace.get_current_span()) return b"" with self.server( max_workers=2, interceptors=[interceptor], ) as (server, channel): server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) try: server.start() # Interleave calls so spans are active on each thread at the same # time with futures.ThreadPoolExecutor(max_workers=2) as tpe: f1 = tpe.submit(channel.unary_unary("TestServicer/handler"), b"") f2 = tpe.submit(channel.unary_unary("TestServicer/handler"), b"") futures.wait((f1, f2)) finally: server.stop(None) self.assertEqual(len(active_spans_in_handler), 2) # pylint:disable=unbalanced-tuple-unpacking span1, span2 = active_spans_in_handler # Spans should belong to separate traces self.assertNotEqual(span1.context.span_id, span2.context.span_id) self.assertNotEqual(span1.context.trace_id, span2.context.trace_id) for span in (span1, span2): # each should be a root span self.assertIsNone(span2.parent) # check attributes self.assertSpanHasAttributes( span, { **self.net_peer_span_attributes, RPC_METHOD: "handler", RPC_SERVICE: "TestServicer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) def test_abort(self): """Check that we can catch an abort properly""" # Intercept gRPC calls... interceptor = server_interceptor() # our detailed failure message failure_message = "This is a test failure" # aborting RPC handlers def error_status_handler(request, context): context.abort(grpc.StatusCode.INTERNAL, failure_message) def unset_status_handler(request, context): context.abort(grpc.StatusCode.FAILED_PRECONDITION, failure_message) rpc_call_error = "TestServicer/error_status_handler" rpc_call_unset = "TestServicer/unset_status_handler" rpc_calls = { rpc_call_error: error_status_handler, rpc_call_unset: unset_status_handler, } for rpc_call, handler in rpc_calls.items(): with self.server( max_workers=1, interceptors=[interceptor], ) as (server, channel): server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) server.start() with self.assertRaises(Exception): channel.unary_unary(rpc_call)(b"") # unfortunately, these are just bare exceptions in grpc... server.stop(None) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 2) # check error span span = spans_list[0] self.assertEqual(span.name, rpc_call_error) self.assertIs(span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) # make sure this span errored, with the right status and detail self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual( span.status.description, f"{grpc.StatusCode.INTERNAL}:{failure_message}", ) # Check attributes self.assertSpanHasAttributes( span, { **self.net_peer_span_attributes, RPC_METHOD: "error_status_handler", RPC_SERVICE: "TestServicer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.INTERNAL.value[0], }, ) # check unset status span span = spans_list[1] self.assertEqual(span.name, rpc_call_unset) self.assertIs(span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) self.assertEqual(span.status.description, None) self.assertEqual(span.status.status_code, StatusCode.UNSET) # Check attributes self.assertSpanHasAttributes( span, { **self.net_peer_span_attributes, RPC_METHOD: "unset_status_handler", RPC_SERVICE: "TestServicer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.FAILED_PRECONDITION.value[0], }, ) def test_non_list_interceptors(self): """Check that we handle non-list interceptors correctly.""" grpc_server_instrumentor = GrpcInstrumentorServer() grpc_server_instrumentor.instrument() try: with self.server( max_workers=1, interceptors=(mock.MagicMock(),), ) as (server, channel): add_GRPCTestServerServicer_to_server(Servicer(), server) rpc_call = "/GRPCTestServer/SimpleMethod" request = Request(client_id=1, request_data="test") msg = request.SerializeToString() try: server.start() channel.unary_unary(rpc_call)(msg) finally: server.stop(None) finally: grpc_server_instrumentor.uninstrument() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, rpc_call) self.assertIs(span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) # Check attributes self.assertSpanHasAttributes( span, { **self.net_peer_span_attributes, RPC_METHOD: "SimpleMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) class TestOpenTelemetryServerInterceptorUnix( TestOpenTelemetryServerInterceptor, ): net_peer_span_attributes = {} @contextlib.contextmanager def server(self, max_workers=1, interceptors=None): with ( futures.ThreadPoolExecutor(max_workers=max_workers) as executor, tempfile.TemporaryDirectory() as tmp, ): server = grpc.server( executor, options=(("grpc.so_reuseport", 0),), interceptors=interceptors or [], ) sock = f"unix://{tmp}/grpc.sock" server.add_insecure_port(sock) channel = grpc.insecure_channel(sock) yield server, channel def get_latch(num): """Get a countdown latch function for use in n threads.""" cv = threading.Condition() count = 0 def countdown_latch(): """Block until n-1 other threads have called.""" nonlocal count cv.acquire() count += 1 cv.notify() cv.release() cv.acquire() while count < num: cv.wait() cv.release() return countdown_latch test_server_interceptor_filter.py000066400000000000000000000147251527070072300420740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-grpc/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint:disable=unused-argument # pylint:disable=no-self-use from concurrent import futures import grpc import opentelemetry.instrumentation.grpc from opentelemetry import trace from opentelemetry.instrumentation.grpc import ( GrpcInstrumentorServer, filters, server_interceptor, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_IP, NET_PEER_NAME, ) from opentelemetry.semconv._incubating.attributes.rpc_attributes import ( RPC_GRPC_STATUS_CODE, RPC_METHOD, RPC_SERVICE, RPC_SYSTEM, ) from opentelemetry.test.test_base import TestBase from .protobuf.test_server_pb2 import Request, Response from .protobuf.test_server_pb2_grpc import ( GRPCTestServerServicer, add_GRPCTestServerServicer_to_server, ) class UnaryUnaryMethodHandler(grpc.RpcMethodHandler): def __init__(self, handler): self.request_streaming = False self.response_streaming = False self.request_deserializer = None self.response_serializer = None self.unary_unary = handler self.unary_stream = None self.stream_unary = None self.stream_stream = None class UnaryUnaryRpcHandler(grpc.GenericRpcHandler): def __init__(self, handler): self._unary_unary_handler = handler def service(self, handler_call_details): return UnaryUnaryMethodHandler(self._unary_unary_handler) class Servicer(GRPCTestServerServicer): """Our test servicer""" # pylint:disable=C0103 def SimpleMethod(self, request, context): return Response( server_id=request.client_id, response_data=request.request_data, ) # pylint:disable=C0103 def ServerStreamingMethod(self, request, context): for data in ("one", "two", "three"): yield Response( server_id=request.client_id, response_data=data, ) class TestOpenTelemetryServerInterceptorFilterMethodName(TestBase): def test_instrumentor(self): def handler(request, context): return b"" grpc_server_instrumentor = GrpcInstrumentorServer(filter_=filters.method_name("handler")) grpc_server_instrumentor.instrument() with futures.ThreadPoolExecutor(max_workers=1) as executor: server = grpc.server( executor, options=(("grpc.so_reuseport", 0),), ) server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) port = server.add_insecure_port("[::]:0") channel = grpc.insecure_channel(f"localhost:{port:d}") rpc_call = "TestServicer/handler" try: server.start() channel.unary_unary(rpc_call)(b"test") finally: server.stop(None) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, rpc_call) self.assertIs(span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) # Check attributes self.assertSpanHasAttributes( span, { NET_PEER_IP: "[::1]", NET_PEER_NAME: "localhost", RPC_METHOD: "handler", RPC_SERVICE: "TestServicer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) grpc_server_instrumentor.uninstrument() def test_uninstrument(self): def handler(request, context): return b"" grpc_server_instrumentor = GrpcInstrumentorServer(filter_=filters.method_name("SimpleMethod")) grpc_server_instrumentor.instrument() grpc_server_instrumentor.uninstrument() with futures.ThreadPoolExecutor(max_workers=1) as executor: server = grpc.server( executor, options=(("grpc.so_reuseport", 0),), ) server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),)) port = server.add_insecure_port("[::]:0") channel = grpc.insecure_channel(f"localhost:{port:d}") rpc_call = "TestServicer/test" try: server.start() channel.unary_unary(rpc_call)(b"test") finally: server.stop(None) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) def test_create_span(self): """Check that the interceptor wraps calls with spans server-side.""" # Intercept gRPC calls... interceptor = server_interceptor(filter_=filters.method_name("SimpleMethod")) with futures.ThreadPoolExecutor(max_workers=1) as executor: server = grpc.server( executor, options=(("grpc.so_reuseport", 0),), interceptors=[interceptor], ) add_GRPCTestServerServicer_to_server(Servicer(), server) port = server.add_insecure_port("[::]:0") channel = grpc.insecure_channel(f"localhost:{port:d}") rpc_call = "/GRPCTestServer/SimpleMethod" request = Request(client_id=1, request_data="test") msg = request.SerializeToString() try: server.start() channel.unary_unary(rpc_call)(msg) finally: server.stop(None) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, rpc_call) self.assertIs(span.kind, trace.SpanKind.SERVER) # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.grpc) # Check attributes self.assertSpanHasAttributes( span, { NET_PEER_IP: "[::1]", NET_PEER_NAME: "localhost", RPC_METHOD: "SimpleMethod", RPC_SERVICE: "GRPCTestServer", RPC_SYSTEM: "grpc", RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[0], }, ) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx/000077500000000000000000000000001527070072300323125ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx/LICENSE000066400000000000000000000261351527070072300333260ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx/README.rst000066400000000000000000000165631527070072300340140ustar00rootroot00000000000000OpenTelemetry HTTPX Instrumentation =================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-httpx.svg :target: https://pypi.org/project/opentelemetry-instrumentation-httpx/ This library allows tracing HTTP requests made by the `httpx `_ and `httpx2 `_ libraries. If both libraries are installed, use ``HTTPXClientInstrumentor`` for ``httpx`` clients and ``HTTPX2ClientInstrumentor`` for ``httpx2`` clients. The instrumentors can be enabled independently. Installation ------------ :: pip install opentelemetry-instrumentation-httpx Usage ----- Instrumenting all clients ************************* When using the instrumentor, all clients will automatically trace requests. .. code-block:: python import httpx import asyncio from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor url = "https://example.com" HTTPXClientInstrumentor().instrument() with httpx.Client() as client: response = client.get(url) async def get(url): async with httpx.AsyncClient() as client: response = await client.get(url) asyncio.run(get(url)) The same package also supports `httpx2 `_ using the ``HTTPX2ClientInstrumentor``: .. code-block:: python import httpx2 import asyncio from opentelemetry.instrumentation.httpx import HTTPX2ClientInstrumentor url = "https://example.com" HTTPX2ClientInstrumentor().instrument() with httpx2.Client() as client: response = client.get(url) async def get(url): async with httpx2.AsyncClient() as client: response = await client.get(url) asyncio.run(get(url)) Instrumenting single clients **************************** If you only want to instrument requests for specific client instances, you can use the `HTTPXClientInstrumentor.instrument_client` method. .. code-block:: python import httpx import asyncio from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor url = "https://example.com" with httpx.Client() as client: HTTPXClientInstrumentor.instrument_client(client) response = client.get(url) async def get(url): async with httpx.AsyncClient() as client: HTTPXClientInstrumentor.instrument_client(client) response = await client.get(url) asyncio.run(get(url)) For ``httpx2`` clients, use ``HTTPX2ClientInstrumentor.instrument_client``: .. code-block:: python import httpx2 from opentelemetry.instrumentation.httpx import HTTPX2ClientInstrumentor with httpx2.Client() as client: HTTPX2ClientInstrumentor.instrument_client(client) response = client.get("https://example.com") Uninstrument ************ If you need to uninstrument clients, there are two options available. .. code-block:: python import httpx from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor HTTPXClientInstrumentor().instrument() client = httpx.Client() # Uninstrument a specific client HTTPXClientInstrumentor.uninstrument_client(client) # Uninstrument all clients HTTPXClientInstrumentor().uninstrument() Using transports directly ************************* If you don't want to use the instrumentor class, you can use the transport classes directly. .. code-block:: python import httpx import asyncio from opentelemetry.instrumentation.httpx import ( AsyncOpenTelemetryTransport, SyncOpenTelemetryTransport, ) url = "https://example.com" transport = httpx.HTTPTransport() telemetry_transport = SyncOpenTelemetryTransport(transport) with httpx.Client(transport=telemetry_transport) as client: response = client.get(url) transport = httpx.AsyncHTTPTransport() telemetry_transport = AsyncOpenTelemetryTransport(transport) async def get(url): async with httpx.AsyncClient(transport=telemetry_transport) as client: response = await client.get(url) asyncio.run(get(url)) For ``httpx2`` transports, use ``SyncOpenTelemetryTransportHttpx2`` and ``AsyncOpenTelemetryTransportHttpx2``: .. code-block:: python import httpx2 from opentelemetry.instrumentation.httpx import SyncOpenTelemetryTransportHttpx2 transport = httpx2.HTTPTransport() telemetry_transport = SyncOpenTelemetryTransportHttpx2(transport) with httpx2.Client(transport=telemetry_transport) as client: response = client.get("https://example.com") Request and response hooks *************************** The instrumentation supports specifying request and response hooks. These are functions that get called back by the instrumentation right after a span is created for a request and right before the span is finished while processing a response. .. note:: The request hook receives the raw arguments provided to the transport layer. The response hook receives the raw return values from the transport layer. The hooks can be configured as follows: .. code-block:: python from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor def request_hook(span, request): # method, url, headers, stream, extensions = request pass def response_hook(span, request, response): # method, url, headers, stream, extensions = request # status_code, headers, stream, extensions = response pass async def async_request_hook(span, request): # method, url, headers, stream, extensions = request pass async def async_response_hook(span, request, response): # method, url, headers, stream, extensions = request # status_code, headers, stream, extensions = response pass HTTPXClientInstrumentor().instrument( request_hook=request_hook, response_hook=response_hook, async_request_hook=async_request_hook, async_response_hook=async_response_hook ) Or if you are using the transport classes directly: .. code-block:: python import httpx from opentelemetry.instrumentation.httpx import SyncOpenTelemetryTransport, AsyncOpenTelemetryTransport def request_hook(span, request): # method, url, headers, stream, extensions = request pass def response_hook(span, request, response): # method, url, headers, stream, extensions = request # status_code, headers, stream, extensions = response pass async def async_request_hook(span, request): # method, url, headers, stream, extensions = request pass async def async_response_hook(span, request, response): # method, url, headers, stream, extensions = request # status_code, headers, stream, extensions = response pass transport = httpx.HTTPTransport() telemetry_transport = SyncOpenTelemetryTransport( transport, request_hook=request_hook, response_hook=response_hook ) async_transport = httpx.AsyncHTTPTransport() async_telemetry_transport = AsyncOpenTelemetryTransport( async_transport, request_hook=async_request_hook, response_hook=async_response_hook ) References ---------- * `OpenTelemetry HTTPX Instrumentation `_ * `OpenTelemetry Project `_ pyproject.toml000066400000000000000000000034101527070072300351450ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-httpx" dynamic = ["version"] description = "OpenTelemetry HTTPX Instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-util-http == 0.66b2", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [] instruments-any = [ "httpx >= 0.18.0", "httpx2 >= 2.0.0", ] [project.entry-points.opentelemetry_instrumentor] httpx = "opentelemetry.instrumentation.httpx:HTTPXClientInstrumentor" httpx2 = "opentelemetry.instrumentation.httpx:HTTPX2ClientInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-httpx" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/httpx/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx/src/000077500000000000000000000000001527070072300331015ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300357165ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx/srcinstrumentation/000077500000000000000000000000001527070072300411615ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetryhttpx/000077500000000000000000000000001527070072300423305ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation__init__.py000066400000000000000000002041541527070072300444470ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines """ Usage ----- Instrumenting all clients ************************* When using the instrumentor, all clients will automatically trace requests. .. code-block:: python import httpx import asyncio from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor url = "https://example.com" HTTPXClientInstrumentor().instrument() with httpx.Client() as client: response = client.get(url) async def get(url): async with httpx.AsyncClient() as client: response = await client.get(url) asyncio.run(get(url)) When instrumenting ``httpx2`` clients, use ``HTTPX2ClientInstrumentor``: .. code-block:: python import httpx2 import asyncio from opentelemetry.instrumentation.httpx import HTTPX2ClientInstrumentor url = "https://example.com" HTTPX2ClientInstrumentor().instrument() with httpx2.Client() as client: response = client.get(url) async def get(url): async with httpx2.AsyncClient() as client: response = await client.get(url) asyncio.run(get(url)) Instrumenting single clients **************************** If you only want to instrument requests for specific client instances, you can use the `instrument_client` method. .. code-block:: python import httpx import asyncio from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor url = "https://example.com" with httpx.Client() as client: HTTPXClientInstrumentor.instrument_client(client) response = client.get(url) async def get(url): async with httpx.AsyncClient() as client: HTTPXClientInstrumentor.instrument_client(client) response = await client.get(url) asyncio.run(get(url)) For ``httpx2`` clients, use ``HTTPX2ClientInstrumentor.instrument_client``: .. code-block:: python import httpx2 from opentelemetry.instrumentation.httpx import HTTPX2ClientInstrumentor with httpx2.Client() as client: HTTPX2ClientInstrumentor.instrument_client(client) response = client.get("https://example.com") Uninstrument ************ If you need to uninstrument clients, there are two options available. .. code-block:: python import httpx from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor HTTPXClientInstrumentor().instrument() client = httpx.Client() # Uninstrument a specific client HTTPXClientInstrumentor.uninstrument_client(client) # Uninstrument all clients HTTPXClientInstrumentor().uninstrument() Using transports directly ************************* If you don't want to use the instrumentor class, you can use the transport classes directly. .. code-block:: python import httpx import asyncio from opentelemetry.instrumentation.httpx import ( AsyncOpenTelemetryTransport, SyncOpenTelemetryTransport, ) url = "https://example.com" transport = httpx.HTTPTransport() telemetry_transport = SyncOpenTelemetryTransport(transport) with httpx.Client(transport=telemetry_transport) as client: response = client.get(url) transport = httpx.AsyncHTTPTransport() telemetry_transport = AsyncOpenTelemetryTransport(transport) async def get(url): async with httpx.AsyncClient(transport=telemetry_transport) as client: response = await client.get(url) asyncio.run(get(url)) For ``httpx2`` transports, use ``SyncOpenTelemetryTransportHttpx2`` and ``AsyncOpenTelemetryTransportHttpx2``: .. code-block:: python import httpx2 from opentelemetry.instrumentation.httpx import ( SyncOpenTelemetryTransportHttpx2, ) transport = httpx2.HTTPTransport() telemetry_transport = SyncOpenTelemetryTransportHttpx2(transport) with httpx2.Client(transport=telemetry_transport) as client: response = client.get("https://example.com") Request and response hooks *************************** The instrumentation supports specifying request and response hooks. These are functions that get called back by the instrumentation right after a span is created for a request and right before the span is finished while processing a response. .. note:: The request hook receives the raw arguments provided to the transport layer. The response hook receives the raw return values from the transport layer. The hooks can be configured as follows: .. code-block:: python from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor def request_hook(span, request): # method, url, headers, stream, extensions = request pass def response_hook(span, request, response): # method, url, headers, stream, extensions = request # status_code, headers, stream, extensions = response pass async def async_request_hook(span, request): # method, url, headers, stream, extensions = request pass async def async_response_hook(span, request, response): # method, url, headers, stream, extensions = request # status_code, headers, stream, extensions = response pass HTTPXClientInstrumentor().instrument( request_hook=request_hook, response_hook=response_hook, async_request_hook=async_request_hook, async_response_hook=async_response_hook, ) Or if you are using the transport classes directly: .. code-block:: python import httpx from opentelemetry.instrumentation.httpx import ( SyncOpenTelemetryTransport, AsyncOpenTelemetryTransport, ) def request_hook(span, request): # method, url, headers, stream, extensions = request pass def response_hook(span, request, response): # method, url, headers, stream, extensions = request # status_code, headers, stream, extensions = response pass async def async_request_hook(span, request): # method, url, headers, stream, extensions = request pass async def async_response_hook(span, request, response): # method, url, headers, stream, extensions = request # status_code, headers, stream, extensions = response pass transport = httpx.HTTPTransport() telemetry_transport = SyncOpenTelemetryTransport(transport, request_hook=request_hook, response_hook=response_hook) async_transport = httpx.AsyncHTTPTransport() async_telemetry_transport = AsyncOpenTelemetryTransport( async_transport, request_hook=async_request_hook, response_hook=async_response_hook, ) Configuration ------------- Exclude lists ************* To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_HTTPX_EXCLUDED_URLS`` (or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the URLs. For example, :: export OTEL_PYTHON_HTTPX_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. Capture HTTP request and response headers ***************************************** You can configure the agent to capture specified HTTP headers as span attributes, according to the `semantic conventions `_. Request headers *************** To capture HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST`` to a comma delimited list of HTTP header names. For example using the environment variable, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST="content-type,custom_request_header" will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. Request header names in HttpX are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST="Accept.*,X-.*" Would match all request headers that start with ``Accept`` and ``X-``. To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST=".*" The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a single item list containing all the header values. For example: ``http.request.header.custom_request_header = ["", ""]`` Response headers **************** To capture HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE`` to a comma delimited list of HTTP header names. For example using the environment variable, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE="content-type,custom_response_header" will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. Response header names in HttpX are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE="Content.*,X-.*" Would match all response headers that start with ``Content`` and ``X-``. To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE=".*" The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a list containing the header values. For example: ``http.response.header.custom_response_header = ["", ""]`` Sanitizing headers ****************** In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be matched in a case-insensitive manner. For example using the environment variable, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. Note: The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. API --- """ from __future__ import annotations import logging import typing from collections import defaultdict from collections.abc import MutableMapping from functools import partial from importlib import import_module from inspect import iscoroutinefunction from timeit import default_timer from types import TracebackType from wrapt import ( wrap_function_wrapper, # pyright: ignore[reportUnknownVariableType] ) from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, _client_duration_attrs_new, _client_duration_attrs_old, _filter_semconv_duration_attrs, _get_schema_url, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _report_new, _report_old, _set_http_host_client, _set_http_method, _set_http_net_peer_name_client, _set_http_network_protocol_version, _set_http_peer_port_client, _set_http_scheme, _set_http_status_code, _set_http_url, _set_status, _StabilityMode, ) from opentelemetry.instrumentation.httpx.package import ( _instruments_httpx, _instruments_httpx2, ) from opentelemetry.instrumentation.httpx.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import ( http_status_to_status_code, is_http_instrumentation_enabled, unwrap, ) from opentelemetry.metrics import Histogram, MeterProvider, get_meter from opentelemetry.propagate import inject from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.network_attributes import ( NETWORK_PEER_ADDRESS, NETWORK_PEER_PORT, ) from opentelemetry.semconv.metrics import ( MetricInstruments, # pyright: ignore[reportDeprecated] ) from opentelemetry.semconv.metrics.http_metrics import ( HTTP_CLIENT_REQUEST_DURATION, ) from opentelemetry.trace import SpanKind, Tracer, TracerProvider, get_tracer from opentelemetry.trace.span import Span from opentelemetry.trace.status import StatusCode from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, ExcludeList, get_custom_header_attributes, get_custom_headers, get_excluded_urls, normalise_request_header_name, normalise_response_header_name, redact_url, sanitize_method, ) from opentelemetry.util.types import AttributeValue if typing.TYPE_CHECKING: from typing_extensions import TypeIs try: import httpx except ImportError: import httpx2 as httpx class _HTTPXModule(typing.Protocol): Request: type[httpx.Request] Response: type[httpx.Response] Headers: type[httpx.Headers] # Declared as a callable rather than type[httpx.URL] because the # URL constructor of httpx < 0.20.0 additionally accepts the raw # URL tuple its transport API passes around. URL: typing.Callable[ [str | httpx.URL | tuple[bytes, bytes, int | None, bytes]], httpx.URL, ] BaseTransport: type[httpx.BaseTransport] AsyncBaseTransport: type[httpx.AsyncBaseTransport] HTTPTransport: type[httpx.HTTPTransport] AsyncHTTPTransport: type[httpx.AsyncHTTPTransport] _logger = logging.getLogger(__name__) # The old HTTP semantic conventions use a metric name that is only available # from the deprecated ``MetricInstruments`` class. _HTTP_CLIENT_DURATION_OLD = ( MetricInstruments.HTTP_CLIENT_DURATION # pyright: ignore[reportDeprecated] ) def _try_import(name: str) -> _HTTPXModule | None: try: return typing.cast("_HTTPXModule", import_module(name)) except ImportError: return None _httpx_module = _try_import("httpx") _httpx2_module = _try_import("httpx2") RequestHook = typing.Callable[[Span, "RequestInfo"], None] ResponseHook = typing.Callable[[Span, "RequestInfo", "ResponseInfo"], None] AsyncRequestHook = typing.Callable[[Span, "RequestInfo"], typing.Awaitable[typing.Any]] AsyncResponseHook = typing.Callable[[Span, "RequestInfo", "ResponseInfo"], typing.Awaitable[typing.Any]] def _is_async_request_hook( hook: RequestHook | AsyncRequestHook | None, ) -> TypeIs[AsyncRequestHook]: return iscoroutinefunction(hook) def _is_sync_request_hook( hook: RequestHook | AsyncRequestHook | None, ) -> TypeIs[RequestHook]: return callable(hook) and not iscoroutinefunction(hook) def _is_async_response_hook( hook: ResponseHook | AsyncResponseHook | None, ) -> TypeIs[AsyncResponseHook]: return iscoroutinefunction(hook) def _is_sync_response_hook( hook: ResponseHook | AsyncResponseHook | None, ) -> TypeIs[ResponseHook]: return callable(hook) and not iscoroutinefunction(hook) class RequestInfo(typing.NamedTuple): method: bytes url: httpx.URL | tuple[bytes, bytes, int | None, bytes] headers: httpx.Headers | None stream: httpx.SyncByteStream | httpx.AsyncByteStream | None extensions: MutableMapping[str, typing.Any] | None class ResponseInfo(typing.NamedTuple): status_code: int headers: httpx.Headers | None stream: httpx.SyncByteStream | httpx.AsyncByteStream extensions: MutableMapping[str, typing.Any] | None def _get_default_span_name(method: str) -> str: method = sanitize_method(method.strip()) if method == "_OTHER": method = "HTTP" return method def _prepare_headers(headers: httpx.Headers | None, module: _HTTPXModule) -> httpx.Headers: return module.Headers(headers) def _extract_parameters( args: tuple[typing.Any, ...], kwargs: dict[str, typing.Any], module: _HTTPXModule, ) -> tuple[ bytes, httpx.URL | tuple[bytes, bytes, int | None, bytes], httpx.Headers | None, httpx.SyncByteStream | httpx.AsyncByteStream | None, MutableMapping[str, typing.Any] | None, ]: if isinstance(args[0], module.Request): # In httpx >= 0.20.0, handle_request receives a Request object request = args[0] method = request.method.encode() url = module.URL(str(request.url)) headers = request.headers stream = request.stream extensions = request.extensions else: # In httpx < 0.20.0, handle_request receives the parameters separately method = args[0] url = args[1] headers = kwargs.get("headers", args[2] if len(args) > 2 else None) stream = kwargs.get("stream", args[3] if len(args) > 3 else None) extensions = kwargs.get("extensions", args[4] if len(args) > 4 else None) return method, url, headers, stream, extensions def _normalize_url( url: httpx.URL | tuple[bytes, bytes, int | None, bytes], ) -> str: if isinstance(url, tuple): scheme, host, port, path = [part.decode() if isinstance(part, bytes) else part for part in url] return f"{scheme}://{host}:{port}{path}" if port else f"{scheme}://{host}{path}" return str(url) def _inject_propagation_headers( headers: httpx.Headers | None, args: tuple[typing.Any, ...], kwargs: dict[str, typing.Any], module: _HTTPXModule, ) -> None: _headers = _prepare_headers(headers, module) inject(_headers) if isinstance(args[0], module.Request): request = args[0] request.headers = _headers else: kwargs["headers"] = _headers.raw def _normalize_headers( headers: httpx.Headers | dict[str, list[str] | str] | list[tuple[bytes, bytes]] | None, module: _HTTPXModule, ) -> dict[str, list[str]]: normalized_headers: defaultdict[str, list[str]] = defaultdict(list) if isinstance(headers, module.Headers): for key in headers.keys(): normalized_headers[key.lower()].extend(headers.get_list(key, split_commas=True)) elif isinstance(headers, dict): for key, value in headers.items(): if isinstance(value, list): normalized_headers[key.lower()].extend(value) else: normalized_headers[key.lower()].append(value) elif isinstance(headers, list): for key, value in headers: normalized_headers[key.decode("latin-1").lower()].append(value.decode("latin-1")) return dict(normalized_headers) def _extract_response( response: httpx.Response | tuple[typing.Any, ...], ) -> tuple[ int, httpx.Headers, httpx.SyncByteStream | httpx.AsyncByteStream, MutableMapping[str, typing.Any], str, ]: if isinstance(response, tuple): # In httpx < 0.20.0, handle_request returns # (status_code, headers, stream, extensions) status_code, headers, stream, extensions = response http_version = extensions.get("http_version", b"HTTP/1.1").decode("ascii", errors="ignore") else: status_code = response.status_code headers = response.headers stream = response.stream extensions = response.extensions http_version = response.http_version return (status_code, headers, stream, extensions, http_version) def _apply_request_client_attributes_to_span( span_attributes: dict[str, AttributeValue], metric_attributes: dict[str, AttributeValue], url: str | httpx.URL | tuple[bytes, bytes, int | None, bytes], method_original: str, semconv: _StabilityMode, module: _HTTPXModule, headers: httpx.Headers | dict[str, list[str] | str] | None = None, captured_headers: list[str] | None = None, sensitive_headers: list[str] | None = None, ) -> None: url = module.URL(url) # http semconv transition: http.method -> http.request.method _set_http_method( span_attributes, method_original, sanitize_method(method_original), semconv, ) # http semconv transition: http.url -> url.full _set_http_url(span_attributes, redact_url(str(url)), semconv) # Set HTTP method in metric labels _set_http_method( metric_attributes, method_original, sanitize_method(method_original), semconv, ) span_attributes.update( get_custom_header_attributes( _normalize_headers(headers, module), captured_headers, sensitive_headers, normalise_request_header_name, ) ) if _report_old(semconv): # TODO: Support opt-in for url.scheme in new semconv _set_http_scheme(metric_attributes, url.scheme, semconv) if _report_new(semconv): if url.host: # http semconv transition: http.host -> server.address _set_http_host_client(span_attributes, url.host, semconv) # Add metric labels _set_http_host_client(metric_attributes, url.host, semconv) _set_http_net_peer_name_client(metric_attributes, url.host, semconv) # http semconv transition: net.sock.peer.addr -> network.peer.address span_attributes[NETWORK_PEER_ADDRESS] = url.host if url.port: # http semconv transition: net.sock.peer.port -> network.peer.port _set_http_peer_port_client(span_attributes, url.port, semconv) span_attributes[NETWORK_PEER_PORT] = url.port # Add metric labels _set_http_peer_port_client(metric_attributes, url.port, semconv) def _apply_response_client_attributes_to_span( span: Span, status_code: int, http_version: str, semconv: _StabilityMode, module: _HTTPXModule, headers: httpx.Headers | dict[str, list[str] | str] | None = None, captured_headers: list[str] | None = None, sensitive_headers: list[str] | None = None, ) -> None: # http semconv transition: http.status_code -> http.response.status_code # TODO: use _set_status when it's stable for http clients span_attributes: dict[str, AttributeValue] = {} _set_http_status_code( span_attributes, status_code, semconv, ) http_status_code = http_status_to_status_code(status_code) span.set_status(http_status_code) span.set_attributes( get_custom_header_attributes( _normalize_headers(headers, module), captured_headers, sensitive_headers, normalise_response_header_name, ) ) if http_status_code == StatusCode.ERROR and _report_new(semconv): # http semconv transition: new error.type span_attributes[ERROR_TYPE] = str(status_code) if http_version and _report_new(semconv): # http semconv transition: http.flavor -> network.protocol.version _set_http_network_protocol_version( span_attributes, http_version.replace("HTTP/", ""), semconv, ) for key, val in span_attributes.items(): span.set_attribute(key, val) def _apply_response_client_attributes_to_metrics( span: Span, metric_attributes: dict[str, AttributeValue], status_code: int, http_version: str, semconv: _StabilityMode, ) -> None: """Apply response attributes to metric attributes.""" # Set HTTP status code in metric attributes _set_status( span, metric_attributes, status_code, str(status_code), server_span=False, sem_conv_opt_in_mode=semconv, ) if http_version and _report_new(semconv): _set_http_network_protocol_version( metric_attributes, http_version.replace("HTTP/", ""), semconv, ) class _SyncOpenTelemetryTransportBase: """Sync transport class that will trace all requests made with a client. Args: transport: SyncHTTPTransport instance to wrap tracer_provider: Tracer provider to use meter_provider: Meter provider to use request_hook: A hook that receives the span and request that is called right after the span is created response_hook: A hook that receives the span, request, and response that is called right before the span ends """ _module: _HTTPXModule def __init__( self, transport: httpx.BaseTransport, tracer_provider: TracerProvider | None = None, meter_provider: MeterProvider | None = None, request_hook: RequestHook | None = None, response_hook: ResponseHook | None = None, ): _OpenTelemetrySemanticConventionStability._initialize() self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) schema_url = _get_schema_url(self._sem_conv_opt_in_mode) self._transport = transport self._tracer = get_tracer( __name__, instrumenting_library_version=__version__, tracer_provider=tracer_provider, schema_url=schema_url, ) meter = get_meter( __name__, __version__, meter_provider, schema_url, ) self._duration_histogram_old = None if _report_old(self._sem_conv_opt_in_mode): self._duration_histogram_old = meter.create_histogram( name=_HTTP_CLIENT_DURATION_OLD, unit="ms", description="measures the duration of the outbound HTTP request", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, ) self._duration_histogram_new = None if _report_new(self._sem_conv_opt_in_mode): self._duration_histogram_new = meter.create_histogram( name=HTTP_CLIENT_REQUEST_DURATION, unit="s", description="Duration of HTTP client requests.", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) self._request_hook = request_hook self._response_hook = response_hook self._excluded_urls = get_excluded_urls("HTTPX") self._captured_request_headers = get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST) self._captured_response_headers = get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE) self._sensitive_headers = get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS) def __enter__(self) -> _SyncOpenTelemetryTransportBase: self._transport.__enter__() return self def __exit__( self, exc_type: type[BaseException] | None = None, exc_value: BaseException | None = None, traceback: TracebackType | None = None, ) -> None: self._transport.__exit__(exc_type, exc_value, traceback) # pylint: disable=R0914 def handle_request( self, *args: typing.Any, **kwargs: typing.Any, ) -> httpx.Response: """Add request info to span.""" if not is_http_instrumentation_enabled(): return self._transport.handle_request(*args, **kwargs) method, url, headers, stream, extensions = _extract_parameters(args, kwargs, self._module) if self._excluded_urls and self._excluded_urls.url_disabled(_normalize_url(url)): return self._transport.handle_request(*args, **kwargs) method_original = method.decode() span_name = _get_default_span_name(method_original) span_attributes: dict[str, AttributeValue] = {} metric_attributes: dict[str, AttributeValue] = {} # apply http client response attributes according to semconv _apply_request_client_attributes_to_span( span_attributes, metric_attributes, url, method_original, self._sem_conv_opt_in_mode, self._module, headers, self._captured_request_headers, self._sensitive_headers, ) request_info = RequestInfo(method, url, headers, stream, extensions) with self._tracer.start_as_current_span(span_name, kind=SpanKind.CLIENT, attributes=span_attributes) as span: exception = None response: httpx.Response | tuple[typing.Any, ...] | None = None if callable(self._request_hook): self._request_hook(span, request_info) _inject_propagation_headers(headers, args, kwargs, self._module) start_time = default_timer() try: response = self._transport.handle_request(*args, **kwargs) except Exception as exc: # pylint: disable=W0703 exception = exc response = getattr(exc, "response", None) finally: elapsed_time = max(default_timer() - start_time, 0) if isinstance(response, (self._module.Response, tuple)): status_code, headers, stream, extensions, http_version = _extract_response(response) # Always apply response attributes to metrics _apply_response_client_attributes_to_metrics( span, metric_attributes, status_code, http_version, self._sem_conv_opt_in_mode, ) if span.is_recording(): # apply http client response attributes according to semconv _apply_response_client_attributes_to_span( span, status_code, http_version, self._sem_conv_opt_in_mode, self._module, headers, self._captured_response_headers, self._sensitive_headers, ) if callable(self._response_hook): self._response_hook( span, request_info, ResponseInfo(status_code, headers, stream, extensions), ) if exception: if span.is_recording() and _report_new(self._sem_conv_opt_in_mode): span.set_attribute(ERROR_TYPE, type(exception).__qualname__) metric_attributes[ERROR_TYPE] = type(exception).__qualname__ raise exception.with_traceback(exception.__traceback__) if self._duration_histogram_old is not None: duration_attrs_old = _filter_semconv_duration_attrs( metric_attributes, _client_duration_attrs_old, _client_duration_attrs_new, _StabilityMode.DEFAULT, ) self._duration_histogram_old.record( max(round(elapsed_time * 1000), 0), attributes=duration_attrs_old, ) if self._duration_histogram_new is not None: duration_attrs_new = _filter_semconv_duration_attrs( metric_attributes, _client_duration_attrs_old, _client_duration_attrs_new, _StabilityMode.HTTP, ) self._duration_histogram_new.record(elapsed_time, attributes=duration_attrs_new) # ``response`` can only be ``None`` when the wrapped transport raised, # in which case the exception was re-raised above; the tuple form is # only produced by httpx < 0.20.0, whose transport API expects it. return typing.cast("httpx.Response", response) def close(self) -> None: self._transport.close() class _AsyncOpenTelemetryTransportBase: """Async transport class that will trace all requests made with a client. Args: transport: AsyncHTTPTransport instance to wrap tracer_provider: Tracer provider to use meter_provider: Meter provider to use request_hook: A hook that receives the span and request that is called right after the span is created response_hook: A hook that receives the span, request, and response that is called right before the span ends """ _module: _HTTPXModule def __init__( self, transport: httpx.AsyncBaseTransport, tracer_provider: TracerProvider | None = None, meter_provider: MeterProvider | None = None, request_hook: AsyncRequestHook | None = None, response_hook: AsyncResponseHook | None = None, ): _OpenTelemetrySemanticConventionStability._initialize() self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) schema_url = _get_schema_url(self._sem_conv_opt_in_mode) self._transport = transport self._tracer = get_tracer( __name__, instrumenting_library_version=__version__, tracer_provider=tracer_provider, schema_url=schema_url, ) meter = get_meter( __name__, __version__, meter_provider, schema_url, ) self._duration_histogram_old = None if _report_old(self._sem_conv_opt_in_mode): self._duration_histogram_old = meter.create_histogram( name=_HTTP_CLIENT_DURATION_OLD, unit="ms", description="measures the duration of the outbound HTTP request", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, ) self._duration_histogram_new = None if _report_new(self._sem_conv_opt_in_mode): self._duration_histogram_new = meter.create_histogram( name=HTTP_CLIENT_REQUEST_DURATION, unit="s", description="Duration of HTTP client requests.", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) self._request_hook = request_hook self._response_hook = response_hook self._excluded_urls = get_excluded_urls("HTTPX") self._captured_request_headers = get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST) self._captured_response_headers = get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE) self._sensitive_headers = get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS) async def __aenter__(self) -> _AsyncOpenTelemetryTransportBase: await self._transport.__aenter__() return self async def __aexit__( self, exc_type: type[BaseException] | None = None, exc_value: BaseException | None = None, traceback: TracebackType | None = None, ) -> None: await self._transport.__aexit__(exc_type, exc_value, traceback) # pylint: disable=R0914 async def handle_async_request(self, *args: typing.Any, **kwargs: typing.Any) -> httpx.Response: """Add request info to span.""" if not is_http_instrumentation_enabled(): return await self._transport.handle_async_request(*args, **kwargs) method, url, headers, stream, extensions = _extract_parameters(args, kwargs, self._module) if self._excluded_urls and self._excluded_urls.url_disabled(_normalize_url(url)): return await self._transport.handle_async_request(*args, **kwargs) method_original = method.decode() span_name = _get_default_span_name(method_original) span_attributes: dict[str, AttributeValue] = {} metric_attributes: dict[str, AttributeValue] = {} # apply http client response attributes according to semconv _apply_request_client_attributes_to_span( span_attributes, metric_attributes, url, method_original, self._sem_conv_opt_in_mode, self._module, headers, self._captured_request_headers, self._sensitive_headers, ) request_info = RequestInfo(method, url, headers, stream, extensions) with self._tracer.start_as_current_span(span_name, kind=SpanKind.CLIENT, attributes=span_attributes) as span: exception = None response: httpx.Response | tuple[typing.Any, ...] | None = None if callable(self._request_hook): await self._request_hook(span, request_info) _inject_propagation_headers(headers, args, kwargs, self._module) start_time = default_timer() try: response = await self._transport.handle_async_request(*args, **kwargs) except Exception as exc: # pylint: disable=W0703 exception = exc response = getattr(exc, "response", None) finally: elapsed_time = max(default_timer() - start_time, 0) if isinstance(response, (self._module.Response, tuple)): status_code, headers, stream, extensions, http_version = _extract_response(response) # Always apply response attributes to metrics _apply_response_client_attributes_to_metrics( span, metric_attributes, status_code, http_version, self._sem_conv_opt_in_mode, ) if span.is_recording(): # apply http client response attributes according to semconv _apply_response_client_attributes_to_span( span, status_code, http_version, self._sem_conv_opt_in_mode, self._module, headers, self._captured_response_headers, self._sensitive_headers, ) if callable(self._response_hook): await self._response_hook( span, request_info, ResponseInfo(status_code, headers, stream, extensions), ) if exception: if span.is_recording() and _report_new(self._sem_conv_opt_in_mode): span.set_attribute(ERROR_TYPE, type(exception).__qualname__) metric_attributes[ERROR_TYPE] = type(exception).__qualname__ raise exception.with_traceback(exception.__traceback__) if self._duration_histogram_old is not None: duration_attrs_old = _filter_semconv_duration_attrs( metric_attributes, _client_duration_attrs_old, _client_duration_attrs_new, _StabilityMode.DEFAULT, ) self._duration_histogram_old.record( max(round(elapsed_time * 1000), 0), attributes=duration_attrs_old, ) if self._duration_histogram_new is not None: duration_attrs_new = _filter_semconv_duration_attrs( metric_attributes, _client_duration_attrs_old, _client_duration_attrs_new, _StabilityMode.HTTP, ) self._duration_histogram_new.record(elapsed_time, attributes=duration_attrs_new) # ``response`` can only be ``None`` when the wrapped transport raised, # in which case the exception was re-raised above; the tuple form is # only produced by httpx < 0.20.0, whose transport API expects it. return typing.cast("httpx.Response", response) async def aclose(self) -> None: await self._transport.aclose() class _BaseHTTPXClientInstrumentor(BaseInstrumentor): # pylint: disable=protected-access """Base instrumentor for httpx API-compatible clients. See `BaseInstrumentor` """ _module: typing.ClassVar[_HTTPXModule | None] _module_name: typing.ClassVar[str] _instrumentation_dependencies: typing.ClassVar[tuple[str, ...]] def instrumentation_dependencies(self) -> typing.Collection[str]: return self._instrumentation_dependencies # pylint: disable=too-many-locals def _instrument(self, **kwargs: typing.Any) -> None: """Instrument the configured httpx API-compatible clients. Args: **kwargs: Optional arguments ``tracer_provider``: a TracerProvider, defaults to global ``meter_provider``: a MeterProvider, defaults to global ``request_hook``: A ``httpx.Client`` or ``httpx2.Client`` hook that receives the span and request that is called right after the span is created. ``response_hook``: A ``httpx.Client`` or ``httpx2.Client`` hook that receives the span, request, and response that is called right before the span ends. ``async_request_hook``: Async request hook for ``httpx.AsyncClient`` or ``httpx2.AsyncClient``. ``async_response_hook``: Async response hook for ``httpx.AsyncClient`` or ``httpx2.AsyncClient``. """ module = self._module if module is None: raise ModuleNotFoundError(f"{self._module_name} must be installed") tracer_provider = kwargs.get("tracer_provider") meter_provider = kwargs.get("meter_provider") request_hook = kwargs.get("request_hook") response_hook = kwargs.get("response_hook") async_request_hook = kwargs.get("async_request_hook") async_request_hook = async_request_hook if iscoroutinefunction(async_request_hook) else None async_response_hook = kwargs.get("async_response_hook") async_response_hook = async_response_hook if iscoroutinefunction(async_response_hook) else None excluded_urls = get_excluded_urls("HTTPX") captured_request_headers: list[str] = get_custom_headers( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST ) captured_response_headers: list[str] = get_custom_headers( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE ) sensitive_headers: list[str] = get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS) _OpenTelemetrySemanticConventionStability._initialize() sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) schema_url = _get_schema_url(sem_conv_opt_in_mode) tracer = get_tracer( __name__, instrumenting_library_version=__version__, tracer_provider=tracer_provider, schema_url=schema_url, ) meter = get_meter( __name__, __version__, meter_provider, schema_url, ) duration_histogram_old = None if _report_old(sem_conv_opt_in_mode): duration_histogram_old = meter.create_histogram( name=_HTTP_CLIENT_DURATION_OLD, unit="ms", description="measures the duration of the outbound HTTP request", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, ) duration_histogram_new = None if _report_new(sem_conv_opt_in_mode): duration_histogram_new = meter.create_histogram( name=HTTP_CLIENT_REQUEST_DURATION, unit="s", description="Duration of HTTP client requests.", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) wrap_function_wrapper( self._module_name, "HTTPTransport.handle_request", partial( self._handle_request_wrapper, module=module, tracer=tracer, duration_histogram_old=duration_histogram_old, duration_histogram_new=duration_histogram_new, sem_conv_opt_in_mode=sem_conv_opt_in_mode, request_hook=request_hook, response_hook=response_hook, excluded_urls=excluded_urls, captured_request_headers=captured_request_headers, captured_response_headers=captured_response_headers, sensitive_headers=sensitive_headers, ), ) wrap_function_wrapper( self._module_name, "AsyncHTTPTransport.handle_async_request", partial( self._handle_async_request_wrapper, module=module, tracer=tracer, duration_histogram_old=duration_histogram_old, duration_histogram_new=duration_histogram_new, sem_conv_opt_in_mode=sem_conv_opt_in_mode, async_request_hook=async_request_hook, async_response_hook=async_response_hook, excluded_urls=excluded_urls, captured_request_headers=captured_request_headers, captured_response_headers=captured_response_headers, sensitive_headers=sensitive_headers, ), ) def _uninstrument(self, **kwargs: typing.Any) -> None: module = self._module if module is None: raise ModuleNotFoundError(f"{self._module_name} must be installed") unwrap(module.HTTPTransport, "handle_request") unwrap(module.AsyncHTTPTransport, "handle_async_request") @staticmethod def _handle_request_wrapper( # pylint: disable=too-many-locals wrapped: typing.Callable[..., typing.Any], instance: httpx.HTTPTransport, args: tuple[typing.Any, ...], kwargs: dict[str, typing.Any], *, module: _HTTPXModule, tracer: Tracer, duration_histogram_old: Histogram | None, duration_histogram_new: Histogram | None, sem_conv_opt_in_mode: _StabilityMode, request_hook: RequestHook | None, response_hook: ResponseHook | None, excluded_urls: ExcludeList | None, captured_request_headers: list[str] | None = None, captured_response_headers: list[str] | None = None, sensitive_headers: list[str] | None = None, ) -> typing.Any: if not is_http_instrumentation_enabled(): return wrapped(*args, **kwargs) method, url, headers, stream, extensions = _extract_parameters(args, kwargs, module) if excluded_urls and excluded_urls.url_disabled(_normalize_url(url)): return wrapped(*args, **kwargs) method_original = method.decode() span_name = _get_default_span_name(method_original) span_attributes: dict[str, AttributeValue] = {} metric_attributes: dict[str, AttributeValue] = {} # apply http client response attributes according to semconv _apply_request_client_attributes_to_span( span_attributes, metric_attributes, url, method_original, sem_conv_opt_in_mode, module, headers, captured_request_headers, sensitive_headers, ) request_info = RequestInfo(method, url, headers, stream, extensions) with tracer.start_as_current_span(span_name, kind=SpanKind.CLIENT, attributes=span_attributes) as span: exception = None response: httpx.Response | tuple[typing.Any, ...] | None = None if callable(request_hook): request_hook(span, request_info) _inject_propagation_headers(headers, args, kwargs, module) start_time = default_timer() try: response = wrapped(*args, **kwargs) except Exception as exc: # pylint: disable=W0703 exception = exc response = getattr(exc, "response", None) finally: elapsed_time = max(default_timer() - start_time, 0) if isinstance(response, (module.Response, tuple)): status_code, headers, stream, extensions, http_version = _extract_response(response) # Always apply response attributes to metrics _apply_response_client_attributes_to_metrics( span, metric_attributes, status_code, http_version, sem_conv_opt_in_mode, ) if span.is_recording(): # apply http client response attributes according to semconv _apply_response_client_attributes_to_span( span, status_code, http_version, sem_conv_opt_in_mode, module, headers, captured_response_headers, sensitive_headers, ) if callable(response_hook): response_hook( span, request_info, ResponseInfo(status_code, headers, stream, extensions), ) if exception: if span.is_recording() and _report_new(sem_conv_opt_in_mode): span.set_attribute(ERROR_TYPE, type(exception).__qualname__) metric_attributes[ERROR_TYPE] = type(exception).__qualname__ raise exception.with_traceback(exception.__traceback__) if duration_histogram_old is not None: duration_attrs_old = _filter_semconv_duration_attrs( metric_attributes, _client_duration_attrs_old, _client_duration_attrs_new, _StabilityMode.DEFAULT, ) duration_histogram_old.record( max(round(elapsed_time * 1000), 0), attributes=duration_attrs_old, ) if duration_histogram_new is not None: duration_attrs_new = _filter_semconv_duration_attrs( metric_attributes, _client_duration_attrs_old, _client_duration_attrs_new, _StabilityMode.HTTP, ) duration_histogram_new.record(elapsed_time, attributes=duration_attrs_new) return response @staticmethod async def _handle_async_request_wrapper( # pylint: disable=too-many-locals wrapped: typing.Callable[..., typing.Awaitable[typing.Any]], instance: httpx.AsyncHTTPTransport, args: tuple[typing.Any, ...], kwargs: dict[str, typing.Any], *, module: _HTTPXModule, tracer: Tracer, duration_histogram_old: Histogram | None, duration_histogram_new: Histogram | None, sem_conv_opt_in_mode: _StabilityMode, async_request_hook: AsyncRequestHook | None, async_response_hook: AsyncResponseHook | None, excluded_urls: ExcludeList | None, captured_request_headers: list[str] | None = None, captured_response_headers: list[str] | None = None, sensitive_headers: list[str] | None = None, ) -> typing.Any: if not is_http_instrumentation_enabled(): return await wrapped(*args, **kwargs) method, url, headers, stream, extensions = _extract_parameters(args, kwargs, module) if excluded_urls and excluded_urls.url_disabled(_normalize_url(url)): return await wrapped(*args, **kwargs) method_original = method.decode() span_name = _get_default_span_name(method_original) span_attributes: dict[str, AttributeValue] = {} metric_attributes: dict[str, AttributeValue] = {} # apply http client response attributes according to semconv _apply_request_client_attributes_to_span( span_attributes, metric_attributes, url, method_original, sem_conv_opt_in_mode, module, headers, captured_request_headers, sensitive_headers, ) request_info = RequestInfo(method, url, headers, stream, extensions) with tracer.start_as_current_span(span_name, kind=SpanKind.CLIENT, attributes=span_attributes) as span: exception = None response: httpx.Response | tuple[typing.Any, ...] | None = None if callable(async_request_hook): await async_request_hook(span, request_info) _inject_propagation_headers(headers, args, kwargs, module) start_time = default_timer() try: response = await wrapped(*args, **kwargs) except Exception as exc: # pylint: disable=W0703 exception = exc response = getattr(exc, "response", None) finally: elapsed_time = max(default_timer() - start_time, 0) if isinstance(response, (module.Response, tuple)): status_code, headers, stream, extensions, http_version = _extract_response(response) # Always apply response attributes to metrics _apply_response_client_attributes_to_metrics( span, metric_attributes, status_code, http_version, sem_conv_opt_in_mode, ) if span.is_recording(): # apply http client response attributes according to semconv _apply_response_client_attributes_to_span( span, status_code, http_version, sem_conv_opt_in_mode, module, headers, captured_response_headers, sensitive_headers, ) if callable(async_response_hook): await async_response_hook( span, request_info, ResponseInfo(status_code, headers, stream, extensions), ) if exception: if span.is_recording() and _report_new(sem_conv_opt_in_mode): span.set_attribute(ERROR_TYPE, type(exception).__qualname__) raise exception.with_traceback(exception.__traceback__) if duration_histogram_old is not None: duration_attrs_old = _filter_semconv_duration_attrs( metric_attributes, _client_duration_attrs_old, _client_duration_attrs_new, _StabilityMode.DEFAULT, ) duration_histogram_old.record( max(round(elapsed_time * 1000), 0), attributes=duration_attrs_old, ) if duration_histogram_new is not None: duration_attrs_new = _filter_semconv_duration_attrs( metric_attributes, _client_duration_attrs_old, _client_duration_attrs_new, _StabilityMode.HTTP, ) duration_histogram_new.record(elapsed_time, attributes=duration_attrs_new) return response # pylint: disable=too-many-branches,too-many-locals @classmethod def instrument_client( cls, client: httpx.Client | httpx.AsyncClient, tracer_provider: TracerProvider | None = None, meter_provider: MeterProvider | None = None, request_hook: RequestHook | AsyncRequestHook | None = None, response_hook: ResponseHook | AsyncResponseHook | None = None, ) -> None: """Instrument an httpx API-compatible Client or AsyncClient. Args: client: The Client or AsyncClient instance tracer_provider: A TracerProvider, defaults to global meter_provider: A MeterProvider, defaults to global request_hook: A hook that receives the span and request that is called right after the span is created response_hook: A hook that receives the span, request, and response that is called right before the span ends """ module = cls._module if module is None: raise ModuleNotFoundError(f"{cls._module_name} must be installed") if getattr(client, "_is_instrumented_by_opentelemetry", False): _logger.warning("Attempting to instrument Httpx client while already instrumented") return _OpenTelemetrySemanticConventionStability._initialize() sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) schema_url = _get_schema_url(sem_conv_opt_in_mode) tracer = get_tracer( __name__, instrumenting_library_version=__version__, tracer_provider=tracer_provider, schema_url=schema_url, ) meter = get_meter( __name__, __version__, meter_provider, schema_url, ) duration_histogram_old = None if _report_old(sem_conv_opt_in_mode): duration_histogram_old = meter.create_histogram( name=_HTTP_CLIENT_DURATION_OLD, unit="ms", description="measures the duration of the outbound HTTP request", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, ) duration_histogram_new = None if _report_new(sem_conv_opt_in_mode): duration_histogram_new = meter.create_histogram( name=HTTP_CLIENT_REQUEST_DURATION, unit="s", description="Duration of HTTP client requests.", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) sync_request_hook: RequestHook | None = None async_request_hook: AsyncRequestHook | None = None if _is_async_request_hook(request_hook): async_request_hook = request_hook elif _is_sync_request_hook(request_hook): sync_request_hook = request_hook sync_response_hook: ResponseHook | None = None async_response_hook: AsyncResponseHook | None = None if _is_async_response_hook(response_hook): async_response_hook = response_hook elif _is_sync_response_hook(response_hook): sync_response_hook = response_hook excluded_urls = get_excluded_urls("HTTPX") captured_request_headers: list[str] = get_custom_headers( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST ) captured_response_headers: list[str] = get_custom_headers( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE ) sensitive_headers: list[str] = get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS) if hasattr(client._transport, "handle_request"): wrap_function_wrapper( client._transport, "handle_request", partial( cls._handle_request_wrapper, module=module, tracer=tracer, duration_histogram_old=duration_histogram_old, duration_histogram_new=duration_histogram_new, sem_conv_opt_in_mode=sem_conv_opt_in_mode, request_hook=sync_request_hook, response_hook=sync_response_hook, excluded_urls=excluded_urls, captured_request_headers=captured_request_headers, captured_response_headers=captured_response_headers, sensitive_headers=sensitive_headers, ), ) for transport in client._mounts.values(): if hasattr(transport, "handle_request"): wrap_function_wrapper( transport, "handle_request", partial( cls._handle_request_wrapper, module=module, tracer=tracer, duration_histogram_old=duration_histogram_old, duration_histogram_new=duration_histogram_new, sem_conv_opt_in_mode=sem_conv_opt_in_mode, request_hook=sync_request_hook, response_hook=sync_response_hook, excluded_urls=excluded_urls, captured_request_headers=captured_request_headers, captured_response_headers=captured_response_headers, sensitive_headers=sensitive_headers, ), ) setattr(client, "_is_instrumented_by_opentelemetry", True) if hasattr(client._transport, "handle_async_request"): wrap_function_wrapper( client._transport, "handle_async_request", partial( cls._handle_async_request_wrapper, module=module, tracer=tracer, duration_histogram_old=duration_histogram_old, duration_histogram_new=duration_histogram_new, sem_conv_opt_in_mode=sem_conv_opt_in_mode, async_request_hook=async_request_hook, async_response_hook=async_response_hook, excluded_urls=excluded_urls, captured_request_headers=captured_request_headers, captured_response_headers=captured_response_headers, sensitive_headers=sensitive_headers, ), ) for transport in client._mounts.values(): if hasattr(transport, "handle_async_request"): wrap_function_wrapper( transport, "handle_async_request", partial( cls._handle_async_request_wrapper, module=module, tracer=tracer, duration_histogram_old=duration_histogram_old, duration_histogram_new=duration_histogram_new, sem_conv_opt_in_mode=sem_conv_opt_in_mode, async_request_hook=async_request_hook, async_response_hook=async_response_hook, excluded_urls=excluded_urls, captured_request_headers=captured_request_headers, captured_response_headers=captured_response_headers, sensitive_headers=sensitive_headers, ), ) setattr(client, "_is_instrumented_by_opentelemetry", True) @staticmethod def uninstrument_client(client: httpx.Client | httpx.AsyncClient) -> None: """Disables instrumentation for the given client instance Args: client: The Client or AsyncClient instance """ if hasattr(client._transport, "handle_request"): unwrap(client._transport, "handle_request") for transport in client._mounts.values(): unwrap(transport, "handle_request") setattr(client, "_is_instrumented_by_opentelemetry", False) elif hasattr(client._transport, "handle_async_request"): unwrap(client._transport, "handle_async_request") for transport in client._mounts.values(): unwrap(transport, "handle_async_request") setattr(client, "_is_instrumented_by_opentelemetry", False) if _httpx_module is not None: class SyncOpenTelemetryTransport(_SyncOpenTelemetryTransportBase, _httpx_module.BaseTransport): """Sync transport class that traces requests made with httpx.""" _module = _httpx_module class AsyncOpenTelemetryTransport(_AsyncOpenTelemetryTransportBase, _httpx_module.AsyncBaseTransport): """Async transport class that traces requests made with httpx.""" _module = _httpx_module class HTTPXClientInstrumentor(_BaseHTTPXClientInstrumentor): """An instrumentor for httpx Client and AsyncClient.""" _module = _httpx_module _module_name = "httpx" _instrumentation_dependencies = _instruments_httpx if _httpx2_module is not None: class SyncOpenTelemetryTransportHttpx2(_SyncOpenTelemetryTransportBase, _httpx2_module.BaseTransport): """Sync transport class that traces requests made with httpx2.""" _module = _httpx2_module class AsyncOpenTelemetryTransportHttpx2(_AsyncOpenTelemetryTransportBase, _httpx2_module.AsyncBaseTransport): """Async transport class that traces requests made with httpx2.""" _module = _httpx2_module class HTTPX2ClientInstrumentor(_BaseHTTPXClientInstrumentor): """An instrumentor for httpx2 Client and AsyncClient.""" _module = _httpx2_module _module_name = "httpx2" _instrumentation_dependencies = _instruments_httpx2 package.py000066400000000000000000000004551527070072300443010ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments_httpx = ("httpx >= 0.18.0",) _instruments_httpx2 = ("httpx2 >= 2.0.0",) _instruments = () _instruments_any = (*_instruments_httpx, *_instruments_httpx2) _supports_metrics = True _semconv_status = "migration" py.typed000066400000000000000000000000001527070072300440150ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpxversion.py000066400000000000000000000001441527070072300443660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements-0-wrapt1.txt000066400000000000000000000000511527070072300400410ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx-r test-requirements-0.txt wrapt==1.17.3 test-requirements-0-wrapt2.txt000066400000000000000000000000501527070072300400410ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx-r test-requirements-0.txt wrapt==2.1.2 test-requirements-0.txt000066400000000000000000000006401527070072300366310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpxanyio==3.7.1 asgiref==3.8.1 certifi==2024.7.4 exceptiongroup==1.2.0 h11==0.12.0 httpcore==0.13.7 httpx==0.18.2 idna==3.7 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 respx==0.17.1 rfc3986==1.5.0 sniffio==1.3.1 tomli==2.0.1 typing_extensions==4.12.2 zipp==3.19.2 -e opentelemetry-instrumentation -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-httpx test-requirements-1-wrapt1.txt000066400000000000000000000000511527070072300400420ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx-r test-requirements-1.txt wrapt==1.17.3 test-requirements-1-wrapt2.txt000066400000000000000000000000501527070072300400420ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx-r test-requirements-1.txt wrapt==2.1.2 test-requirements-1.txt000066400000000000000000000006161527070072300366350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpxanyio==4.3.0 asgiref==3.8.1 certifi==2024.7.4 exceptiongroup==1.2.0 h11==0.16 httpcore==1.0.9 httpx==0.28.1 idna==3.7 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 respx==0.22.0 sniffio==1.3.1 tomli==2.0.1 typing_extensions==4.12.2 zipp==3.19.2 -e opentelemetry-instrumentation -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-httpx test-requirements-2-wrapt1.txt000066400000000000000000000000511527070072300400430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx-r test-requirements-2.txt wrapt==1.17.3 test-requirements-2-wrapt2.txt000066400000000000000000000000501527070072300400430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx-r test-requirements-2.txt wrapt==2.1.2 test-requirements-2.txt000066400000000000000000000002241527070072300366310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpxhttpx2==2.3.0 pytest==7.4.4 -e opentelemetry-instrumentation -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-httpx test-requirements-3-wrapt1.txt000066400000000000000000000000511527070072300400440ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx-r test-requirements-3.txt wrapt==1.17.3 test-requirements-3-wrapt2.txt000066400000000000000000000000501527070072300400440ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx-r test-requirements-3.txt wrapt==2.1.2 test-requirements-3.txt000066400000000000000000000002601527070072300366320ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpxhttpx==0.28.1 httpx2==2.3.0 pytest==7.4.4 respx==0.22.0 -e opentelemetry-instrumentation -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-httpx opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx/tests/000077500000000000000000000000001527070072300334545ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300354740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx/teststest_httpx2_integration.py000066400000000000000000000232741527070072300406520ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=arguments-differ,invalid-name,no-self-use,possibly-used-before-assignment import asyncio import importlib.util import threading import unittest from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from unittest import mock from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_METHOD, HTTP_STATUS_CODE, HTTP_URL, ) from opentelemetry.test.test_base import TestBase httpx2_installed = importlib.util.find_spec("httpx2") is not None if httpx2_installed: import httpx2 from opentelemetry.instrumentation.httpx import ( AsyncOpenTelemetryTransportHttpx2, HTTPX2ClientInstrumentor, SyncOpenTelemetryTransportHttpx2, ) class _TestHandler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" def do_GET(self): body = b"Hello!" self.send_response(200) self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, fmt, *args): pass @unittest.skipIf(not httpx2_installed, "httpx2 is not installed") class TestHTTPX2Instrumentor(TestBase): # NOTE: Unlike the httpx tests which use respx for mocking, these tests use # a local server for global instrumentation and httpx2.MockTransport for # client-level instrumentation. At the time of writing, respx does not yet # have stable httpx2 support (see # https://github.com/lundberg/respx/issues/316 and PR #317). # pytest-httpx2 v1.0.0 exists but uses a fixture-based API that does not fit # this unittest-style suite. MockTransport is built in to httpx2 and has zero # external dependencies; the local server is used where we need to exercise # HTTPTransport.handle_request directly. # # These tests are intentionally focused on proving that httpx2 reaches each # public instrumentation path. The detailed span attributes, metrics, # semantic convention modes, header capture, error paths, and suppression # behavior are still covered by the original httpx suite because both # instrumentors share the same wrapper and transport implementation. def setUp(self): super().setUp() self.env_patch = mock.patch.dict( "os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "default"}, ) self.env_patch.start() _OpenTelemetrySemanticConventionStability._initialized = False self.server = ThreadingHTTPServer(("127.0.0.1", 0), _TestHandler) self.server_thread = threading.Thread( target=self.server.serve_forever, daemon=True, ) self.server_thread.start() host, port = self.server.server_address self.url = f"http://{host}:{port}/status/200" def tearDown(self): HTTPX2ClientInstrumentor().uninstrument() self.server.shutdown() self.server.server_close() self.server_thread.join(timeout=5) self.env_patch.stop() super().tearDown() def _mock_transport(self, status_code=200): def handler(request): return httpx2.Response( status_code, text="Hello!", extensions={"http_version": b"HTTP/1.1"}, ) return httpx2.MockTransport(handler) def _assert_basic_span(self, url=None): spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "GET") span_attributes = dict(span.attributes) self.assertEqual(span_attributes[HTTP_METHOD], "GET") self.assertEqual(span_attributes[HTTP_URL], url or self.url) self.assertEqual(span_attributes[HTTP_STATUS_CODE], 200) return span def test_sync_global_instrumentation(self): HTTPX2ClientInstrumentor().instrument( tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) with httpx2.Client() as client: response = client.get(self.url) self.assertEqual(response.text, "Hello!") self._assert_basic_span() def test_async_global_instrumentation(self): async def do_request(): HTTPX2ClientInstrumentor().instrument( tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) async with httpx2.AsyncClient() as client: return await client.get(self.url) response = asyncio.run(do_request()) self.assertEqual(response.text, "Hello!") self._assert_basic_span() def test_sync_instrument_client(self): url = "http://mock/status/200" client = httpx2.Client(transport=self._mock_transport()) HTTPX2ClientInstrumentor.instrument_client( client, tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) response = client.get(url) self.assertEqual(response.text, "Hello!") self._assert_basic_span(url) HTTPX2ClientInstrumentor.uninstrument_client(client) client.close() def test_async_instrument_client(self): async def do_request(): url = "http://mock/status/200" client = httpx2.AsyncClient( transport=httpx2.MockTransport( lambda request: httpx2.Response( 200, text="Hello!", extensions={"http_version": b"HTTP/1.1"}, ) ) ) HTTPX2ClientInstrumentor.instrument_client( client, tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) response = await client.get(url) HTTPX2ClientInstrumentor.uninstrument_client(client) await client.aclose() return url, response url, response = asyncio.run(do_request()) self.assertEqual(response.text, "Hello!") self._assert_basic_span(url) def test_uninstrument_client(self): url = "http://mock/status/200" client = httpx2.Client(transport=self._mock_transport()) HTTPX2ClientInstrumentor.instrument_client( client, tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) HTTPX2ClientInstrumentor.uninstrument_client(client) response = client.get(url) self.assertEqual(response.text, "Hello!") self.assertEqual(self.memory_exporter.get_finished_spans(), ()) client.close() def test_sync_transport_wrapper(self): url = "http://mock/status/200" transport = SyncOpenTelemetryTransportHttpx2( self._mock_transport(), tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) with httpx2.Client(transport=transport) as client: response = client.get(url) self.assertEqual(response.text, "Hello!") self._assert_basic_span(url) def test_async_transport_wrapper(self): async def do_request(): url = "http://mock/status/200" transport = AsyncOpenTelemetryTransportHttpx2( httpx2.MockTransport( lambda request: httpx2.Response( 200, text="Hello!", extensions={"http_version": b"HTTP/1.1"}, ) ), tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) async with httpx2.AsyncClient(transport=transport) as client: return url, await client.get(url) url, response = asyncio.run(do_request()) self.assertEqual(response.text, "Hello!") self._assert_basic_span(url) def test_hooks(self): url = "http://mock/status/200" def request_hook(span, request): span.set_attribute("request_hook", request.method.decode()) def response_hook(span, request, response): span.set_attribute("response_hook", response.status_code) client = httpx2.Client(transport=self._mock_transport()) HTTPX2ClientInstrumentor.instrument_client( client, tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, request_hook=request_hook, response_hook=response_hook, ) response = client.get(url) self.assertEqual(response.text, "Hello!") span = self._assert_basic_span(url) self.assertEqual(span.attributes["request_hook"], "GET") self.assertEqual(span.attributes["response_hook"], 200) HTTPX2ClientInstrumentor.uninstrument_client(client) client.close() def test_excluded_urls(self): self.env_patch.stop() with mock.patch.dict( "os.environ", {"OTEL_PYTHON_HTTPX_EXCLUDED_URLS": self.url}, ): HTTPX2ClientInstrumentor().instrument( tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) with httpx2.Client() as client: response = client.get(self.url) self.env_patch.start() self.assertEqual(response.text, "Hello!") self.assertEqual(self.memory_exporter.get_finished_spans(), ()) test_httpx_integration.py000066400000000000000000002425211527070072300405660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-httpx/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines import abc import asyncio import inspect import typing import unittest from unittest import mock import httpx import respx try: # wrapt 2.0.0+ from wrapt import BaseObjectProxy # pylint: disable=no-name-in-module except ImportError: from wrapt import ObjectProxy as BaseObjectProxy import opentelemetry.instrumentation.httpx from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, _StabilityMode, ) from opentelemetry.instrumentation.httpx import ( AsyncOpenTelemetryTransport, HTTPXClientInstrumentor, SyncOpenTelemetryTransport, _apply_request_client_attributes_to_span, ) from opentelemetry.instrumentation.utils import suppress_http_instrumentation from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.sdk import resources from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_FLAVOR, HTTP_HOST, HTTP_METHOD, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_URL, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_REQUEST_METHOD_ORIGINAL, HTTP_RESPONSE_STATUS_CODE, ) from opentelemetry.semconv.attributes.network_attributes import ( NETWORK_PEER_ADDRESS, NETWORK_PEER_PORT, NETWORK_PROTOCOL_VERSION, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.semconv.attributes.url_attributes import URL_FULL from opentelemetry.semconv.schemas import Schemas from opentelemetry.test.mock_textmap import MockTextMapPropagator from opentelemetry.test.test_base import TestBase from opentelemetry.trace import StatusCode if typing.TYPE_CHECKING: from opentelemetry.instrumentation.httpx import ( AsyncRequestHook, AsyncResponseHook, RequestHook, RequestInfo, ResponseHook, ResponseInfo, ) from opentelemetry.metrics import MeterProvider from opentelemetry.sdk.trace.export import SpanExporter from opentelemetry.trace import TracerProvider from opentelemetry.trace.span import Span HTTP_RESPONSE_BODY = "http.response.body" SCHEMA_URL = Schemas.V1_21_0.value def _is_url_tuple(request: "RequestInfo"): """Determine if request url format is for httpx versions < 0.20.0.""" return isinstance(request[1], tuple) and len(request[1]) == 4 def _async_call(coro: typing.Coroutine) -> asyncio.Task: return asyncio.run(coro) def _response_hook(span, request: "RequestInfo", response: "ResponseInfo"): assert _is_url_tuple(request) or isinstance(request.url, httpx.URL) span.set_attribute( HTTP_RESPONSE_BODY, "".join([part.decode() for part in response[2]]), ) async def _async_response_hook(span: "Span", request: "RequestInfo", response: "ResponseInfo"): assert _is_url_tuple(request) or isinstance(request.url, httpx.URL) span.set_attribute( HTTP_RESPONSE_BODY, "".join([part.decode() async for part in response[2]]), ) def _request_hook(span: "Span", request: "RequestInfo"): assert _is_url_tuple(request) or isinstance(request.url, httpx.URL) url = httpx.URL(request[1]) span.update_name("GET" + str(url)) async def _async_request_hook(span: "Span", request: "RequestInfo"): assert _is_url_tuple(request) or isinstance(request.url, httpx.URL) url = httpx.URL(request[1]) span.update_name("GET" + str(url)) def _no_update_request_hook(span: "Span", request: "RequestInfo"): return 123 async def _async_no_update_request_hook(span: "Span", request: "RequestInfo"): return 123 # pylint: disable=too-many-public-methods SCOPE = "opentelemetry.instrumentation.httpx" # Using this wrapper class to have a base class for the tests while also not # angering pylint or mypy when calling methods not in the class when only # subclassing abc.ABC. class BaseTestCases: class BaseTest(TestBase, metaclass=abc.ABCMeta): # pylint: disable=no-member URL = "http://mock/status/200" response_hook = staticmethod(_response_hook) request_hook = staticmethod(_request_hook) no_update_request_hook = staticmethod(_no_update_request_hook) # TODO: make this more explicit to tests # pylint: disable=invalid-name def setUp(self): super().setUp() test_name = "" if hasattr(self, "_testMethodName"): test_name = self._testMethodName sem_conv_mode = "default" if "new_semconv" in test_name: sem_conv_mode = "http" elif "both_semconv" in test_name: sem_conv_mode = "http/dup" self.env_patch = mock.patch.dict( "os.environ", { OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) self.env_patch.start() _OpenTelemetrySemanticConventionStability._initialized = False respx.start() respx.get(self.URL).mock( httpx.Response( 200, text="Hello!", extensions={"http_version": b"HTTP/1.1"}, ) ) # pylint: disable=invalid-name def tearDown(self): super().tearDown() self.env_patch.stop() respx.stop() HTTPXClientInstrumentor().uninstrument() def assert_span(self, exporter: "SpanExporter" = None, num_spans: int = 1): if exporter is None: exporter = self.memory_exporter span_list = exporter.get_finished_spans() self.assertEqual(num_spans, len(span_list)) if num_spans == 0: return None if num_spans == 1: return span_list[0] return span_list def assert_metrics(self, num_metrics: int = 1): metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), num_metrics) return metrics @abc.abstractmethod def perform_request( self, url: str, method: str = "GET", headers: dict[str, str] = None, client: httpx.Client | httpx.AsyncClient | None = None, ): pass def test_basic(self): result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "GET") self.assertEqual( dict(span.attributes), { HTTP_METHOD: "GET", HTTP_URL: self.URL, HTTP_STATUS_CODE: 200, }, ) self.assertIs(span.status.status_code, trace.StatusCode.UNSET) self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.httpx) def test_basic_metrics(self): self.perform_request(self.URL) metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 1) duration_data_point = metrics[0].data.data_points[0] self.assertEqual(duration_data_point.count, 1) self.assertEqual( dict(duration_data_point.attributes), { HTTP_STATUS_CODE: 200, HTTP_METHOD: "GET", HTTP_SCHEME: "http", }, ) self.assertEqual(duration_data_point.count, 1) self.assertTrue(duration_data_point.min >= 0) self.assertTrue(duration_data_point.max >= 0) self.assertTrue(duration_data_point.sum >= 0) self.assertEqual( duration_data_point.explicit_bounds, HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, ) def test_nonstandard_http_method(self): respx.route(method="NONSTANDARD").mock(return_value=httpx.Response(405)) self.perform_request(self.URL, method="NONSTANDARD") span = self.assert_span() self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "HTTP") self.assertEqual( dict(span.attributes), { HTTP_METHOD: "_OTHER", HTTP_URL: self.URL, HTTP_STATUS_CODE: 405, }, ) self.assertIs(span.status.status_code, trace.StatusCode.ERROR) self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.httpx) # Validate metrics metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 1) duration_data_point = metrics[0].data.data_points[0] self.assertEqual(duration_data_point.count, 1) self.assertEqual( dict(duration_data_point.attributes), { HTTP_STATUS_CODE: 405, HTTP_METHOD: "_OTHER", HTTP_SCHEME: "http", }, ) def test_nonstandard_http_method_new_semconv(self): respx.route(method="NONSTANDARD").mock(return_value=httpx.Response(405)) self.perform_request(self.URL, method="NONSTANDARD") span = self.assert_span() self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "HTTP") self.assertEqual( dict(span.attributes), { HTTP_REQUEST_METHOD: "_OTHER", URL_FULL: self.URL, SERVER_ADDRESS: "mock", NETWORK_PEER_ADDRESS: "mock", HTTP_RESPONSE_STATUS_CODE: 405, NETWORK_PROTOCOL_VERSION: "1.1", ERROR_TYPE: "405", HTTP_REQUEST_METHOD_ORIGINAL: "NONSTANDARD", }, ) self.assertIs(span.status.status_code, trace.StatusCode.ERROR) self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.httpx) # Validate metrics metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 1) duration_data_point = metrics[0].data.data_points[0] self.assertEqual(duration_data_point.count, 1) self.assertEqual( dict(duration_data_point.attributes), { HTTP_REQUEST_METHOD: "_OTHER", SERVER_ADDRESS: "mock", HTTP_RESPONSE_STATUS_CODE: 405, NETWORK_PROTOCOL_VERSION: "1.1", ERROR_TYPE: "405", }, ) self.assertEqual( duration_data_point.explicit_bounds, HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) def test_basic_new_semconv(self): url = "http://mock:8080/status/200" respx.get(url).mock( httpx.Response( 200, text="Hello!", extensions={"http_version": b"HTTP/1.1"}, ) ) result = self.perform_request(url) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "GET") self.assertEqual( span.instrumentation_scope.schema_url, SCHEMA_URL, ) self.assertEqual( dict(span.attributes), { HTTP_REQUEST_METHOD: "GET", URL_FULL: url, SERVER_ADDRESS: "mock", NETWORK_PEER_ADDRESS: "mock", HTTP_RESPONSE_STATUS_CODE: 200, NETWORK_PROTOCOL_VERSION: "1.1", SERVER_PORT: 8080, NETWORK_PEER_PORT: 8080, }, ) self.assertIs(span.status.status_code, trace.StatusCode.UNSET) self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.httpx) # Validate metrics metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 1) duration_data_point = metrics[0].data.data_points[0] self.assertEqual(duration_data_point.count, 1) self.assertEqual( dict(duration_data_point.attributes), { SERVER_ADDRESS: "mock", HTTP_REQUEST_METHOD: "GET", HTTP_RESPONSE_STATUS_CODE: 200, NETWORK_PROTOCOL_VERSION: "1.1", SERVER_PORT: 8080, }, ) def test_basic_both_semconv(self): url = "http://mock:8080/status/200" # 8080 because httpx returns None for common ports (http, https, wss) respx.get(url).mock(httpx.Response(200, text="Hello!")) result = self.perform_request(url) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "GET") self.assertEqual( span.instrumentation_scope.schema_url, SCHEMA_URL, ) self.assertEqual( dict(span.attributes), { HTTP_METHOD: "GET", HTTP_REQUEST_METHOD: "GET", HTTP_URL: url, URL_FULL: url, HTTP_HOST: "mock", SERVER_ADDRESS: "mock", NETWORK_PEER_ADDRESS: "mock", NET_PEER_PORT: 8080, HTTP_STATUS_CODE: 200, HTTP_RESPONSE_STATUS_CODE: 200, HTTP_FLAVOR: "1.1", NETWORK_PROTOCOL_VERSION: "1.1", SERVER_PORT: 8080, NETWORK_PEER_PORT: 8080, }, ) self.assertIs(span.status.status_code, trace.StatusCode.UNSET) self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.httpx) # Validate metrics metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 2) # Old convention self.assertEqual( dict(metrics[0].data.data_points[0].attributes), { HTTP_FLAVOR: "1.1", HTTP_HOST: "mock", HTTP_METHOD: "GET", HTTP_SCHEME: "http", NET_PEER_NAME: "mock", NET_PEER_PORT: 8080, HTTP_STATUS_CODE: 200, }, ) self.assertEqual(metrics[0].name, "http.client.duration") # New convention self.assertEqual( dict(metrics[1].data.data_points[0].attributes), { HTTP_REQUEST_METHOD: "GET", SERVER_ADDRESS: "mock", HTTP_RESPONSE_STATUS_CODE: 200, NETWORK_PROTOCOL_VERSION: "1.1", SERVER_PORT: 8080, }, ) self.assertEqual(metrics[1].name, "http.client.request.duration") def test_basic_multiple(self): self.perform_request(self.URL) self.perform_request(self.URL) self.assert_span(num_spans=2) def test_not_foundbasic(self): url_404 = "http://mock/status/404" with respx.mock: respx.get(url_404).mock(httpx.Response(404)) result = self.perform_request(url_404) self.assertEqual(result.status_code, 404) span = self.assert_span() self.assertEqual(span.attributes.get(HTTP_STATUS_CODE), 404) self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) # Validate metrics metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 1) duration_data_point = metrics[0].data.data_points[0] self.assertEqual( duration_data_point.attributes.get(HTTP_STATUS_CODE), 404, ) def test_not_foundbasic_new_semconv(self): url_404 = "http://mock/status/404" with respx.mock: respx.get(url_404).mock(httpx.Response(404)) result = self.perform_request(url_404) self.assertEqual(result.status_code, 404) span = self.assert_span() self.assertEqual(span.attributes.get(HTTP_RESPONSE_STATUS_CODE), 404) # new in semconv self.assertEqual(span.attributes.get(ERROR_TYPE), "404") self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) # Validate metrics metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 1) duration_data_point = metrics[0].data.data_points[0] self.assertEqual( duration_data_point.attributes.get(HTTP_RESPONSE_STATUS_CODE), 404, ) self.assertEqual(duration_data_point.attributes.get(ERROR_TYPE), "404") def test_not_foundbasic_both_semconv(self): url_404 = "http://mock/status/404" with respx.mock: respx.get(url_404).mock(httpx.Response(404)) result = self.perform_request(url_404) self.assertEqual(result.status_code, 404) span = self.assert_span() self.assertEqual(span.attributes.get(HTTP_STATUS_CODE), 404) self.assertEqual(span.attributes.get(HTTP_RESPONSE_STATUS_CODE), 404) self.assertEqual(span.attributes.get(ERROR_TYPE), "404") self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) # Validate metrics metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 2) # Old convention self.assertEqual( metrics[0].data.data_points[0].attributes.get(HTTP_STATUS_CODE), 404, ) self.assertEqual(metrics[0].data.data_points[0].attributes.get(ERROR_TYPE), None) # New convention self.assertEqual( metrics[1].data.data_points[0].attributes.get(HTTP_RESPONSE_STATUS_CODE), 404, ) self.assertEqual( metrics[1].data.data_points[0].attributes.get(ERROR_TYPE), "404", ) def test_suppress_instrumentation(self): with suppress_http_instrumentation(): result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") self.assert_span(num_spans=0) def test_distributed_context(self): previous_propagator = get_global_textmap() try: set_global_textmap(MockTextMapPropagator()) result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") span = self.assert_span() headers = dict(respx.calls.last.request.headers) self.assertIn(MockTextMapPropagator.TRACE_ID_KEY, headers) self.assertEqual( str(span.get_span_context().trace_id), headers[MockTextMapPropagator.TRACE_ID_KEY], ) self.assertIn(MockTextMapPropagator.SPAN_ID_KEY, headers) self.assertEqual( str(span.get_span_context().span_id), headers[MockTextMapPropagator.SPAN_ID_KEY], ) finally: set_global_textmap(previous_propagator) def test_requests_500_error(self): respx.get(self.URL).mock(httpx.Response(500)) self.perform_request(self.URL) span = self.assert_span() self.assertEqual( dict(span.attributes), { HTTP_METHOD: "GET", HTTP_URL: self.URL, HTTP_STATUS_CODE: 500, }, ) self.assertEqual(span.status.status_code, StatusCode.ERROR) def test_requests_basic_exception(self): with respx.mock, self.assertRaises(Exception): respx.get(self.URL).mock(side_effect=Exception) self.perform_request(self.URL) span = self.assert_span() self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertIn("Exception", span.status.description) self.assertEqual(span.events[0].attributes["exception.type"], "Exception") self.assertIsNone(span.attributes.get(ERROR_TYPE)) def test_requests_basic_exception_new_semconv(self): with respx.mock, self.assertRaises(Exception): respx.get(self.URL).mock(side_effect=Exception) self.perform_request(self.URL) span = self.assert_span() self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertIn("Exception", span.status.description) self.assertEqual(span.events[0].attributes["exception.type"], "Exception") self.assertEqual(span.attributes.get(ERROR_TYPE), "Exception") def test_requests_basic_exception_both_semconv(self): with respx.mock, self.assertRaises(Exception): respx.get(self.URL).mock(side_effect=Exception) self.perform_request(self.URL) span = self.assert_span() self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertIn("Exception", span.status.description) self.assertEqual(span.events[0].attributes["exception.type"], "Exception") self.assertEqual(span.attributes.get(ERROR_TYPE), "Exception") def test_requests_timeout_exception_new_semconv(self): url = "http://mock:8080/exception" with respx.mock, self.assertRaises(httpx.TimeoutException): respx.get(url).mock(side_effect=httpx.TimeoutException) self.perform_request(url) span = self.assert_span() self.assertEqual( dict(span.attributes), { HTTP_REQUEST_METHOD: "GET", URL_FULL: url, SERVER_ADDRESS: "mock", SERVER_PORT: 8080, NETWORK_PEER_PORT: 8080, NETWORK_PEER_ADDRESS: "mock", ERROR_TYPE: "TimeoutException", }, ) self.assertEqual(span.status.status_code, StatusCode.ERROR) def test_requests_timeout_exception_both_semconv(self): url = "http://mock:8080/exception" with respx.mock, self.assertRaises(httpx.TimeoutException): respx.get(url).mock(side_effect=httpx.TimeoutException) self.perform_request(url) span = self.assert_span() self.assertEqual( dict(span.attributes), { HTTP_METHOD: "GET", HTTP_REQUEST_METHOD: "GET", HTTP_URL: url, URL_FULL: url, HTTP_HOST: "mock", SERVER_ADDRESS: "mock", NETWORK_PEER_ADDRESS: "mock", NET_PEER_PORT: 8080, SERVER_PORT: 8080, NETWORK_PEER_PORT: 8080, ERROR_TYPE: "TimeoutException", }, ) self.assertEqual(span.status.status_code, StatusCode.ERROR) def test_requests_timeout_exception(self): with respx.mock, self.assertRaises(httpx.TimeoutException): respx.get(self.URL).mock(side_effect=httpx.TimeoutException) self.perform_request(self.URL) span = self.assert_span() self.assertEqual(span.status.status_code, StatusCode.ERROR) def test_invalid_url(self): url = "invalid://nope/" with respx.mock, self.assertRaises(httpx.UnsupportedProtocol): respx.post("invalid://nope").pass_through() self.perform_request(url, method="POST") span = self.assert_span() self.assertEqual(span.name, "POST") self.assertEqual(span.attributes[HTTP_METHOD], "POST") self.assertEqual(span.attributes[HTTP_URL], url) self.assertEqual(span.status.status_code, StatusCode.ERROR) def test_if_headers_equals_none(self): result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") self.assert_span() def test_ignores_excluded_urls(self): for env_var in ( "OTEL_PYTHON_HTTPX_EXCLUDED_URLS", "OTEL_PYTHON_EXCLUDED_URLS", ): with self.subTest(env_var=env_var): with mock.patch.dict("os.environ", {env_var: self.URL}, clear=True): client = self.create_client() HTTPXClientInstrumentor().instrument_client(client=client) self.perform_request(self.URL, client=client) self.assert_span(num_spans=0) self.assert_metrics(num_metrics=0) HTTPXClientInstrumentor().uninstrument_client(client) class BaseManualTest(BaseTest, metaclass=abc.ABCMeta): @abc.abstractmethod def create_transport( self, tracer_provider: typing.Optional["TracerProvider"] = None, meter_provider: typing.Optional["MeterProvider"] = None, request_hook: typing.Optional["RequestHook"] = None, response_hook: typing.Optional["ResponseHook"] = None, **kwargs, ): pass @abc.abstractmethod def create_client( self, transport: SyncOpenTelemetryTransport | AsyncOpenTelemetryTransport | None = None, **kwargs, ): pass def test_default_client(self): client = self.create_client(transport=None) result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") self.assert_span(num_spans=0) result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") self.assert_span() def test_custom_tracer_provider(self): resource = resources.Resource.create({}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result transport = self.create_transport(tracer_provider=tracer_provider) client = self.create_client(transport) result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") span = self.assert_span(exporter=exporter) self.assertIs(span.resource, resource) def test_custom_meter_provider(self): meter_provider, memory_reader = self.create_meter_provider() transport = self.create_transport(meter_provider=meter_provider) client = self.create_client(transport) self.perform_request(self.URL, client=client) metrics = memory_reader.get_metrics_data().resource_metrics[0] self.assertEqual(len(metrics.scope_metrics), 1) data_point = metrics.scope_metrics[0].metrics[0].data.data_points[0] self.assertEqual(data_point.count, 1) def _run_disabled_tracing_metrics_attributes_test( self, url: str, expected_attributes: dict, expected_number_of_metrics: int = 1, ) -> None: with mock.patch("opentelemetry.trace.INVALID_SPAN") as mock_span: client = self.create_client(self.create_transport(tracer_provider=trace.NoOpTracerProvider())) mock_span.is_recording.return_value = False self.perform_request(url, client=client) self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) metrics = self.assert_metrics(num_metrics=expected_number_of_metrics) duration_data_point = metrics[0].data.data_points[0] self.assertEqual(duration_data_point.count, 1) self.assertEqual( dict(duration_data_point.attributes), expected_attributes, ) def test_metrics_have_response_attributes_with_disabled_tracing( self, ) -> None: """Test that metrics have response attributes when tracing is disabled for old semantic conventions.""" self._run_disabled_tracing_metrics_attributes_test( url="http://mock:8080/status/200", expected_attributes={ HTTP_STATUS_CODE: 200, HTTP_METHOD: "GET", HTTP_SCHEME: "http", }, expected_number_of_metrics=1, ) def test_metrics_have_response_attributes_with_disabled_tracing_both_semconv( self, ) -> None: """Test that metrics have response attributes when tracing is disabled for both semantic conventions.""" self._run_disabled_tracing_metrics_attributes_test( url="http://mock:8080/status/200", expected_attributes={ HTTP_STATUS_CODE: 200, HTTP_METHOD: "GET", HTTP_SCHEME: "http", HTTP_FLAVOR: "1.1", HTTP_HOST: "mock", NET_PEER_NAME: "mock", NET_PEER_PORT: 8080, }, expected_number_of_metrics=2, ) def test_metrics_have_response_attributes_with_disabled_tracing_new_semconv( self, ) -> None: """Test that metrics have response attributes when tracing is disabled with new semantic conventions.""" self._run_disabled_tracing_metrics_attributes_test( url="http://mock:8080/status/200", expected_attributes={ SERVER_ADDRESS: "mock", HTTP_REQUEST_METHOD: "GET", HTTP_RESPONSE_STATUS_CODE: 200, NETWORK_PROTOCOL_VERSION: "1.1", SERVER_PORT: 8080, }, expected_number_of_metrics=1, ) def test_response_hook(self): transport = self.create_transport( tracer_provider=self.tracer_provider, response_hook=self.response_hook, ) client = self.create_client(transport) result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertEqual( dict(span.attributes), { HTTP_METHOD: "GET", HTTP_URL: self.URL, HTTP_STATUS_CODE: 200, HTTP_RESPONSE_BODY: "Hello!", }, ) def test_request_hook(self): transport = self.create_transport(request_hook=self.request_hook) client = self.create_client(transport) result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertEqual(span.name, "GET" + self.URL) def test_request_hook_no_span_change(self): transport = self.create_transport(request_hook=self.no_update_request_hook) client = self.create_client(transport) result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertEqual(span.name, "GET") def test_not_recording(self): with mock.patch("opentelemetry.trace.INVALID_SPAN") as mock_span: transport = self.create_transport(tracer_provider=trace.NoOpTracerProvider()) client = self.create_client(transport) mock_span.is_recording.return_value = False result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") self.assert_span(None, 0) self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) @respx.mock def test_not_recording_not_set_attribute_in_exception_new_semconv( self, ): respx.get(self.URL).mock(side_effect=httpx.TimeoutException) with mock.patch("opentelemetry.trace.INVALID_SPAN") as mock_span: transport = self.create_transport(tracer_provider=trace.NoOpTracerProvider()) client = self.create_client(transport) mock_span.is_recording.return_value = False try: self.perform_request(self.URL, client=client) except httpx.TimeoutException: pass self.assert_span(None, 0) self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) @respx.mock def test_client_mounts_with_instrumented_transport(self): https_url = "https://mock/status/200" respx.get(https_url).mock(httpx.Response(200)) proxy_mounts = { "http://": self.create_transport(proxy=httpx.Proxy("http://localhost:8080")), "https://": self.create_transport(proxy=httpx.Proxy("http://localhost:8443")), } client1 = self.create_client(mounts=proxy_mounts) client2 = self.create_client(mounts=proxy_mounts) self.perform_request(self.URL, client=client1) self.perform_request(https_url, client=client2) spans = self.assert_span(num_spans=2) self.assertEqual(spans[0].attributes[HTTP_URL], self.URL) self.assertEqual(spans[1].attributes[HTTP_URL], https_url) def test_ignores_excluded_urls(self): for env_var in ( "OTEL_PYTHON_HTTPX_EXCLUDED_URLS", "OTEL_PYTHON_EXCLUDED_URLS", ): with self.subTest(env_var=env_var): with mock.patch.dict("os.environ", {env_var: self.URL}, clear=True): client = self.create_client() HTTPXClientInstrumentor().instrument_client(client=client) self.perform_request(self.URL, client=client) self.assert_span(num_spans=0) self.assert_metrics(num_metrics=0) HTTPXClientInstrumentor().uninstrument_client(client=client) def test_request_header_capture(self): test_cases = [ { "name": "single_header", "env_vars": { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST": "X-Custom-Header", }, "request_headers": {"X-Custom-Header": "custom-value"}, "expected_attrs": { "http.request.header.x_custom_header": ("custom-value",), }, }, { "name": "multiple_headers", "env_vars": { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST": "X-First-Header,X-Second-Header", }, "request_headers": { "X-First-Header": "value1", "X-Second-Header": "value2", }, "expected_attrs": { "http.request.header.x_first_header": ("value1",), "http.request.header.x_second_header": ("value2",), }, }, { "name": "regex_pattern", "env_vars": { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST": "X-Test-.*", }, "request_headers": { "X-Test-One": "one", "X-Test-Two": "two", "X-Not-Matched": "ignored", }, "expected_attrs": { "http.request.header.x_test_one": ("one",), "http.request.header.x_test_two": ("two",), }, "unexpected_attrs": ["http.request.header.x_not_matched"], }, { "name": "wildcard_all_headers", "env_vars": { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST": ".*", }, "request_headers": { "X-Any-Header": "any-value", }, "expected_attrs": { "http.request.header.x_any_header": ("any-value",), }, }, { "name": "case_insensitive", "env_vars": { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST": "x-lowercase-config", }, "request_headers": {"X-Lowercase-Config": "mixed-case"}, "expected_attrs": { "http.request.header.x_lowercase_config": ("mixed-case",), }, }, ] for test_case in test_cases: with self.subTest(name=test_case["name"]): self.memory_exporter.clear() with mock.patch.dict( "os.environ", test_case["env_vars"], ): client = self.create_client() HTTPXClientInstrumentor().instrument_client(client) self.perform_request( self.URL, headers=test_case["request_headers"], client=client, ) span = self.assert_span() for attr_name, expected_value in test_case["expected_attrs"].items(): self.assertEqual( span.attributes.get(attr_name), expected_value, f"Expected {attr_name} to be {expected_value}", ) for attr_name in test_case.get("unexpected_attrs", []): self.assertIsNone( span.attributes.get(attr_name), f"Expected {attr_name} to not be present", ) HTTPXClientInstrumentor().uninstrument_client(client) def test_response_header_capture(self): test_cases = [ { "name": "single_header", "env_vars": { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE": "X-Response-Header", }, "response_headers": {"X-Response-Header": "response-value"}, "expected_attrs": { "http.response.header.x_response_header": ("response-value",), }, }, { "name": "multiple_headers", "env_vars": { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE": "X-Response-One,X-Response-Two", }, "response_headers": { "X-Response-One": "value1", "X-Response-Two": "value2", }, "expected_attrs": { "http.response.header.x_response_one": ("value1",), "http.response.header.x_response_two": ("value2",), }, }, { "name": "regex_pattern", "env_vars": { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE": "X-Custom-.*", }, "response_headers": { "X-Custom-First": "first", "X-Custom-Second": "second", "Content-Type": "text/plain", }, "expected_attrs": { "http.response.header.x_custom_first": ("first",), "http.response.header.x_custom_second": ("second",), }, "unexpected_attrs": ["http.response.header.content_type"], }, { "name": "wildcard_all_headers", "env_vars": { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE": ".*", }, "response_headers": {"X-Any-Response": "any-value"}, "expected_attrs": { "http.response.header.x_any_response": ("any-value",), }, }, ] for test_case in test_cases: with self.subTest(name=test_case["name"]): self.memory_exporter.clear() respx.get(self.URL).mock( httpx.Response( 200, text="Test", headers=test_case["response_headers"], ) ) with mock.patch.dict("os.environ", test_case["env_vars"]): client = self.create_client() HTTPXClientInstrumentor().instrument_client(client) self.perform_request(self.URL, client=client) span = self.assert_span() for attr_name, expected_value in test_case["expected_attrs"].items(): self.assertEqual( span.attributes.get(attr_name), expected_value, f"Expected {attr_name} to be {expected_value}", ) for attr_name in test_case.get("unexpected_attrs", []): self.assertIsNone( span.attributes.get(attr_name), f"Expected {attr_name} to not be present", ) HTTPXClientInstrumentor().uninstrument_client(client) def test_header_sanitization(self): test_cases = [ { "name": "request_header_sanitization", "env_vars": { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST": "Authorization,X-Api-Key,X-Safe-Header", "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS": "authorization,.*api-key.*", }, "request_headers": { "Authorization": "Bearer secret", "X-Api-Key": "secret-key", "X-Safe-Header": "safe-value", }, "response_headers": {}, "expected_attrs": { "http.request.header.authorization": ("[REDACTED]",), "http.request.header.x_api_key": ("[REDACTED]",), "http.request.header.x_safe_header": ("safe-value",), }, }, { "name": "response_header_sanitization", "env_vars": { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE": "X-Secret-Token,X-Normal-Header", "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS": ".*secret.*", }, "request_headers": {}, "response_headers": { "X-Secret-Token": "secret-value", "X-Normal-Header": "normal-value", }, "expected_attrs": { "http.response.header.x_secret_token": ("[REDACTED]",), "http.response.header.x_normal_header": ("normal-value",), }, }, { "name": "both_request_and_response_sanitization", "env_vars": { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST": "X-Secret-Request", "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE": "X-Secret-Response", "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS": ".*secret.*", }, "request_headers": {"X-Secret-Request": "req-secret"}, "response_headers": {"X-Secret-Response": "resp-secret"}, "expected_attrs": { "http.request.header.x_secret_request": ("[REDACTED]",), "http.response.header.x_secret_response": ("[REDACTED]",), }, }, ] for test_case in test_cases: with self.subTest(name=test_case["name"]): self.memory_exporter.clear() respx.get(self.URL).mock( httpx.Response( 200, text="Test", headers=test_case.get("response_headers") or {}, ) ) with mock.patch.dict("os.environ", test_case["env_vars"]): client = self.create_client() HTTPXClientInstrumentor().instrument_client(client) self.perform_request( self.URL, headers=test_case.get("request_headers") or None, client=client, ) span = self.assert_span() for attr_name, expected_value in test_case["expected_attrs"].items(): self.assertEqual( span.attributes.get(attr_name), expected_value, f"Expected {attr_name} to be {expected_value}", ) HTTPXClientInstrumentor().uninstrument_client(client) def test_no_headers_captured_when_not_configured(self): respx.get(self.URL).mock( httpx.Response( 200, text="Test", headers={"X-Custom-Response": "value"}, ) ) client = self.create_client() HTTPXClientInstrumentor().instrument_client(client) self.perform_request( self.URL, headers={"X-Custom-Request": "value"}, client=client, ) span = self.assert_span() self.assertIsNone(span.attributes.get("http.request.header.x_custom_request")) self.assertIsNone(span.attributes.get("http.response.header.x_custom_response")) HTTPXClientInstrumentor().uninstrument_client(client) def test_both_request_and_response_headers_captured(self): """Test capturing both request and response headers simultaneously.""" respx.get(self.URL).mock( httpx.Response( 200, text="Test", headers={"X-Response-Id": "response-123"}, ) ) with mock.patch.dict( "os.environ", { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST": "X-Request-Id", "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE": "X-Response-Id", }, ): client = self.create_client() HTTPXClientInstrumentor().instrument_client(client) self.perform_request( self.URL, headers={"X-Request-Id": "req-456"}, client=client, ) span = self.assert_span() self.assertEqual( span.attributes.get("http.request.header.x_request_id"), ("req-456",), ) self.assertEqual( span.attributes.get("http.response.header.x_response_id"), ("response-123",), ) HTTPXClientInstrumentor().uninstrument_client(client) def test_header_capture_via_transport(self): test_cases = [ { "name": "request_header_capture", "env_vars": { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST": "X-Transport-Request", }, "request_headers": {"X-Transport-Request": "req-value"}, "response_headers": {}, "expected_attrs": { "http.request.header.x_transport_request": ("req-value",), }, }, { "name": "response_header_capture", "env_vars": { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE": "X-Transport-Response", }, "request_headers": {}, "response_headers": {"X-Transport-Response": "resp-value"}, "expected_attrs": { "http.response.header.x_transport_response": ("resp-value",), }, }, { "name": "header_sanitization", "env_vars": { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST": "X-Secret", "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE": "X-Secret", "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS": "x-secret", }, "request_headers": {"X-Secret": "secret-request"}, "response_headers": {"X-Secret": "secret-response"}, "expected_attrs": { "http.request.header.x_secret": ("[REDACTED]",), "http.response.header.x_secret": ("[REDACTED]",), }, }, ] for test_case in test_cases: with self.subTest(name=test_case["name"]): self.memory_exporter.clear() respx.get(self.URL).mock( httpx.Response( 200, text="Test", headers=test_case.get("response_headers") or {}, ) ) with mock.patch.dict("os.environ", test_case["env_vars"]): transport = self.create_transport() client = self.create_client(transport) self.perform_request( self.URL, headers=test_case.get("request_headers") or None, client=client, ) span = self.assert_span() for attr_name, expected_value in test_case["expected_attrs"].items(): self.assertEqual( span.attributes.get(attr_name), expected_value, f"Expected {attr_name} to be {expected_value}", ) @mock.patch.dict("os.environ", {"NO_PROXY": ""}, clear=True) class BaseInstrumentorTest(BaseTest, metaclass=abc.ABCMeta): @abc.abstractmethod def create_client( self, transport: SyncOpenTelemetryTransport | AsyncOpenTelemetryTransport | None = None, **kwargs, ): pass @abc.abstractmethod def create_proxy_transport(self, url: str): pass @abc.abstractmethod def get_transport_handler(self, transport): pass def setUp(self): super().setUp() self.client = self.create_client() HTTPXClientInstrumentor().instrument_client(self.client) def tearDown(self): # TODO: uninstrument() is required in order to avoid leaks for instrumentations # but we should audit the single tests and fix any missing uninstrumentation HTTPXClientInstrumentor().uninstrument() def create_proxy_mounts(self): return { "http://": self.create_proxy_transport("http://localhost:8080"), "https://": self.create_proxy_transport("http://localhost:8080"), } def assert_proxy_mounts(self, mounts, num_mounts, transport_type=None): self.assertEqual(len(mounts), num_mounts) for transport in mounts: with self.subTest(transport): if transport is None: continue if transport_type: self.assertIsInstance( transport, transport_type, ) else: handler = self.get_transport_handler(transport) self.assertTrue(isinstance(handler, BaseObjectProxy) and handler.__wrapped__) def test_custom_tracer_provider(self): resource = resources.Resource.create({}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result HTTPXClientInstrumentor().instrument(tracer_provider=tracer_provider) client = self.create_client() result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") span = self.assert_span(exporter=exporter) self.assertIs(span.resource, resource) def test_response_hook(self): response_hook_key = ( "async_response_hook" if inspect.iscoroutinefunction(self.response_hook) else "response_hook" ) response_hook_kwargs = {response_hook_key: self.response_hook} HTTPXClientInstrumentor().instrument( tracer_provider=self.tracer_provider, **response_hook_kwargs, ) client = self.create_client() result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertEqual( dict(span.attributes), { HTTP_METHOD: "GET", HTTP_URL: self.URL, HTTP_STATUS_CODE: 200, HTTP_RESPONSE_BODY: "Hello!", }, ) def test_response_hook_sync_async_kwargs(self): HTTPXClientInstrumentor().instrument( tracer_provider=self.tracer_provider, response_hook=_response_hook, async_response_hook=_async_response_hook, ) client = self.create_client() result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertEqual( dict(span.attributes), { HTTP_METHOD: "GET", HTTP_URL: self.URL, HTTP_STATUS_CODE: 200, HTTP_RESPONSE_BODY: "Hello!", }, ) def test_request_hook(self): request_hook_key = ( "async_request_hook" if inspect.iscoroutinefunction(self.request_hook) else "request_hook" ) request_hook_kwargs = {request_hook_key: self.request_hook} HTTPXClientInstrumentor().instrument( tracer_provider=self.tracer_provider, **request_hook_kwargs, ) client = self.create_client() result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertEqual(span.name, "GET" + self.URL) def test_request_hook_sync_async_kwargs(self): HTTPXClientInstrumentor().instrument( tracer_provider=self.tracer_provider, request_hook=_request_hook, async_request_hook=_async_request_hook, ) client = self.create_client() result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertEqual(span.name, "GET" + self.URL) def test_request_hook_no_span_update(self): HTTPXClientInstrumentor().instrument( tracer_provider=self.tracer_provider, request_hook=self.no_update_request_hook, ) client = self.create_client() result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertEqual(span.name, "GET") def test_not_recording(self): with mock.patch("opentelemetry.trace.INVALID_SPAN") as mock_span: HTTPXClientInstrumentor().instrument(tracer_provider=trace.NoOpTracerProvider()) client = self.create_client() mock_span.is_recording.return_value = False result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") self.assert_span(None, 0) self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) def test_suppress_instrumentation_new_client(self): HTTPXClientInstrumentor().instrument() with suppress_http_instrumentation(): client = self.create_client() result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") self.assert_span(num_spans=0) def test_instrument_client_called_on_the_instance(self): client = self.create_client() HTTPXClientInstrumentor().instrument_client(client) result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") self.assert_span(num_spans=1) def test_instrument_client_called_on_the_class(self): client = self.create_client() HTTPXClientInstrumentor.instrument_client(client) result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") self.assert_span(num_spans=1) def test_instrumentation_without_client(self): HTTPXClientInstrumentor().instrument() results = [ httpx.get(self.URL), httpx.request("GET", self.URL), ] with httpx.stream("GET", self.URL) as stream: stream.read() results.append(stream) spans = self.assert_span(num_spans=len(results)) for idx, res in enumerate(results): with self.subTest(idx=idx, res=res): self.assertEqual(res.text, "Hello!") self.assertEqual( spans[idx].attributes[HTTP_URL], self.URL, ) def test_uninstrument(self): HTTPXClientInstrumentor().instrument() HTTPXClientInstrumentor().uninstrument() client = self.create_client() result = self.perform_request(self.URL, client=client) result_no_client = httpx.get(self.URL) self.assertEqual(result.text, "Hello!") self.assertEqual(result_no_client.text, "Hello!") self.assert_span(num_spans=0) def test_uninstrument_client(self): HTTPXClientInstrumentor().uninstrument_client(self.client) result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") self.assert_span(num_spans=0) def test_uninstrument_new_client(self): HTTPXClientInstrumentor().instrument() client1 = self.create_client() HTTPXClientInstrumentor().uninstrument_client(client1) result = self.perform_request(self.URL, client=client1) self.assertEqual(result.text, "Hello!") self.assert_span(num_spans=0) # Test that other clients as well as instance client is still # instrumented client2 = self.create_client() result = self.perform_request(self.URL, client=client2) self.assertEqual(result.text, "Hello!") self.assert_span() self.memory_exporter.clear() result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") self.assert_span() @mock.patch.dict("os.environ", {"NO_PROXY": "http://mock/status/200"}, clear=True) def test_instrument_with_no_proxy(self): proxy_mounts = self.create_proxy_mounts() HTTPXClientInstrumentor().instrument() client = self.create_client(mounts=proxy_mounts) result = self.perform_request(self.URL, client=client) self.assert_span(num_spans=1) self.assertEqual(result.text, "Hello!") self.assert_proxy_mounts( client._mounts.values(), 3, ) def test_instrument_proxy(self): proxy_mounts = self.create_proxy_mounts() HTTPXClientInstrumentor().instrument() client = self.create_client(mounts=proxy_mounts) self.perform_request(self.URL, client=client) self.assert_span(num_spans=1) self.assert_proxy_mounts( client._mounts.values(), 2, ) @mock.patch.dict("os.environ", {"NO_PROXY": "http://mock/status/200"}, clear=True) def test_instrument_client_with_no_proxy(self): proxy_mounts = self.create_proxy_mounts() client = self.create_client(mounts=proxy_mounts) self.assert_proxy_mounts( client._mounts.values(), 3, (httpx.HTTPTransport, httpx.AsyncHTTPTransport), ) HTTPXClientInstrumentor.instrument_client(client) result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") self.assert_span(num_spans=1) self.assert_proxy_mounts( client._mounts.values(), 3, ) HTTPXClientInstrumentor.uninstrument_client(client) def test_instrument_client_with_proxy(self): proxy_mounts = self.create_proxy_mounts() client = self.create_client(mounts=proxy_mounts) self.assert_proxy_mounts( client._mounts.values(), 2, (httpx.HTTPTransport, httpx.AsyncHTTPTransport), ) HTTPXClientInstrumentor().instrument_client(client) result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") self.assert_span(num_spans=1) self.assert_proxy_mounts( client._mounts.values(), 2, ) HTTPXClientInstrumentor().uninstrument_client(client) def test_uninstrument_client_with_proxy(self): proxy_mounts = self.create_proxy_mounts() HTTPXClientInstrumentor().instrument() client = self.create_client(mounts=proxy_mounts) self.assert_proxy_mounts( client._mounts.values(), 2, ) HTTPXClientInstrumentor().uninstrument_client(client) result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") self.assert_span(num_spans=0) self.assert_proxy_mounts( client._mounts.values(), 2, (httpx.HTTPTransport, httpx.AsyncHTTPTransport), ) # Test that other clients as well as instance client is still # instrumented client2 = self.create_client() result = self.perform_request(self.URL, client=client2) self.assertEqual(result.text, "Hello!") self.assert_span() self.memory_exporter.clear() result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") self.assert_span() def test_ignores_excluded_urls(self): for env_var in ( "OTEL_PYTHON_HTTPX_EXCLUDED_URLS", "OTEL_PYTHON_EXCLUDED_URLS", ): with self.subTest(env_var=env_var): client = self.create_client() with mock.patch.dict("os.environ", {env_var: self.URL}, clear=True): HTTPXClientInstrumentor().instrument_client(client=client) self.perform_request(self.URL, client=client) self.assert_span(num_spans=0) self.assert_metrics(num_metrics=0) HTTPXClientInstrumentor().uninstrument_client(client) def test_header_capture_with_instrument(self): test_cases = [ { "name": "request_header_capture", "env_vars": { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST": "X-Request", }, "request_headers": {"X-Request": "req-value"}, "response_headers": {}, "expected_attrs": { "http.request.header.x_request": ("req-value",), }, }, { "name": "response_header_capture", "env_vars": { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE": "X-Response", }, "request_headers": {}, "response_headers": {"X-Response": "resp-value"}, "expected_attrs": { "http.response.header.x_response": ("resp-value",), }, }, { "name": "header_capture_and_sanitization", "env_vars": { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST": "X-Public-Request,X-Secret-Request", "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE": "X-Public-Response,X-Secret-Response", "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS": ".*secret.*", }, "request_headers": { "X-Public-Request": "public-req", "X-Secret-Request": "secret-req", }, "response_headers": { "X-Public-Response": "public-resp", "X-Secret-Response": "secret-resp", }, "expected_attrs": { "http.request.header.x_public_request": ("public-req",), "http.request.header.x_secret_request": ("[REDACTED]",), "http.response.header.x_public_response": ("public-resp",), "http.response.header.x_secret_response": ("[REDACTED]",), }, }, ] for test_case in test_cases: with self.subTest(name=test_case["name"]): self.memory_exporter.clear() HTTPXClientInstrumentor().uninstrument() respx.get(self.URL).mock( httpx.Response( 200, text="Test", headers=test_case.get("response_headers") or {}, ) ) with mock.patch.dict("os.environ", test_case["env_vars"]): HTTPXClientInstrumentor().instrument() client = self.create_client() self.perform_request( self.URL, headers=test_case.get("request_headers") or None, client=client, ) span = self.assert_span() for attr_name, expected_value in test_case["expected_attrs"].items(): self.assertEqual( span.attributes.get(attr_name), expected_value, f"Expected {attr_name} to be {expected_value}", ) class TestSyncIntegration(BaseTestCases.BaseManualTest): def setUp(self): super().setUp() self.transport = self.create_transport() self.client = self.create_client(self.transport) def tearDown(self): super().tearDown() self.client.close() def create_transport( self, tracer_provider: typing.Optional["TracerProvider"] = None, meter_provider: typing.Optional["MeterProvider"] = None, request_hook: typing.Optional["RequestHook"] = None, response_hook: typing.Optional["ResponseHook"] = None, **kwargs, ): transport = httpx.HTTPTransport(**kwargs) telemetry_transport = SyncOpenTelemetryTransport( transport, tracer_provider=tracer_provider, meter_provider=meter_provider, request_hook=request_hook, response_hook=response_hook, ) return telemetry_transport def create_client( self, transport: SyncOpenTelemetryTransport | None = None, **kwargs, ): return httpx.Client(transport=transport, **kwargs) def perform_request( self, url: str, method: str = "GET", headers: dict[str, str] = None, client: httpx.Client | httpx.AsyncClient | None = None, ): if client is None: return self.client.request(method, url, headers=headers) return client.request(method, url, headers=headers) def test_basic(self): self.perform_request(self.URL) self.assert_span(num_spans=1) self.assert_metrics(num_metrics=1) def test_remove_sensitive_params(self): new_url = "http://username:password@mock/status/200?sig=secret" self.perform_request(new_url) span = self.assert_span() actual_url = span.attributes[HTTP_URL] if "@" in actual_url: # If credentials are present, they must be redacted self.assertEqual( span.attributes[HTTP_URL], "http://REDACTED:REDACTED@mock/status/200?sig=REDACTED", ) else: # If credentials are removed completely, the query string should still be redacted self.assertIn( "http://mock/status/200?sig=REDACTED", actual_url, f"Basic URL structure is incorrect: {actual_url}", ) class TestAsyncIntegration(BaseTestCases.BaseManualTest): response_hook = staticmethod(_async_response_hook) request_hook = staticmethod(_async_request_hook) no_update_request_hook = staticmethod(_async_no_update_request_hook) def setUp(self): super().setUp() self.transport = self.create_transport() self.client = self.create_client(self.transport) def create_transport( self, tracer_provider: typing.Optional["TracerProvider"] = None, meter_provider: typing.Optional["MeterProvider"] = None, request_hook: typing.Optional["AsyncRequestHook"] = None, response_hook: typing.Optional["AsyncResponseHook"] = None, **kwargs, ): transport = httpx.AsyncHTTPTransport(**kwargs) telemetry_transport = AsyncOpenTelemetryTransport( transport, tracer_provider=tracer_provider, meter_provider=meter_provider, request_hook=request_hook, response_hook=response_hook, ) return telemetry_transport def create_client( self, transport: AsyncOpenTelemetryTransport | None = None, **kwargs, ): return httpx.AsyncClient(transport=transport, **kwargs) def perform_request( self, url: str, method: str = "GET", headers: dict[str, str] = None, client: httpx.Client | httpx.AsyncClient | None = None, ): async def _perform_request(): nonlocal client nonlocal method if client is None: client = self.client async with client as _client: return await _client.request(method, url, headers=headers) return _async_call(_perform_request()) def test_basic_multiple(self): # We need to create separate clients because in httpx >= 0.19, # closing the client after "with" means the second http call fails self.perform_request(self.URL, client=self.create_client(self.transport)) self.perform_request(self.URL, client=self.create_client(self.transport)) self.assert_span(num_spans=2) self.assert_metrics(num_metrics=1) def test_remove_sensitive_params(self): new_url = "http://username:password@mock/status/200?Signature=secret" self.perform_request(new_url) span = self.assert_span() actual_url = span.attributes[HTTP_URL] if "@" in actual_url: self.assertEqual( span.attributes[HTTP_URL], "http://REDACTED:REDACTED@mock/status/200?Signature=REDACTED", ) else: self.assertIn( "http://mock/status/200?Signature=REDACTED", actual_url, f"If credentials are removed, the query string still should be redacted {actual_url}", ) class TestSyncInstrumentationIntegration(BaseTestCases.BaseInstrumentorTest): def create_client( self, transport: SyncOpenTelemetryTransport | None = None, **kwargs, ): return httpx.Client(**kwargs) def perform_request( self, url: str, method: str = "GET", headers: dict[str, str] = None, client: httpx.Client | httpx.AsyncClient | None = None, ): if client is None: return self.client.request(method, url, headers=headers) return client.request(method, url, headers=headers) def create_proxy_transport(self, url): return httpx.HTTPTransport(proxy=httpx.Proxy(url)) def get_transport_handler(self, transport): return getattr(transport, "handle_request", None) def test_can_instrument_subclassed_client(self): class CustomClient(httpx.Client): pass client = CustomClient() self.assertFalse(isinstance(client._transport.handle_request, BaseObjectProxy)) HTTPXClientInstrumentor().instrument() self.assertTrue(isinstance(client._transport.handle_request, BaseObjectProxy)) class TestAsyncInstrumentationIntegration(BaseTestCases.BaseInstrumentorTest): response_hook = staticmethod(_async_response_hook) request_hook = staticmethod(_async_request_hook) no_update_request_hook = staticmethod(_async_no_update_request_hook) def setUp(self): super().setUp() self.client2 = self.create_client() HTTPXClientInstrumentor().instrument_client(self.client2) def create_client( self, transport: AsyncOpenTelemetryTransport | None = None, **kwargs, ): return httpx.AsyncClient(**kwargs) def perform_request( self, url: str, method: str = "GET", headers: dict[str, str] = None, client: httpx.Client | httpx.AsyncClient | None = None, ): async def _perform_request(): nonlocal client nonlocal method if client is None: client = self.client async with client as _client: return await _client.request(method, url, headers=headers) return _async_call(_perform_request()) def create_proxy_transport(self, url): return httpx.AsyncHTTPTransport(proxy=httpx.Proxy(url)) def get_transport_handler(self, transport): return getattr(transport, "handle_async_request", None) def test_basic_multiple(self): # We need to create separate clients because in httpx >= 0.19, # closing the client after "with" means the second http call fails self.perform_request(self.URL, client=self.client) self.perform_request(self.URL, client=self.client2) self.assert_span(num_spans=2) self.assert_metrics(num_metrics=1) def test_async_response_hook_does_nothing_if_not_coroutine(self): HTTPXClientInstrumentor().instrument( tracer_provider=self.tracer_provider, async_response_hook=_response_hook, ) client = self.create_client() result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertEqual( dict(span.attributes), { HTTP_METHOD: "GET", HTTP_URL: self.URL, HTTP_STATUS_CODE: 200, }, ) def test_async_request_hook_does_nothing_if_not_coroutine(self): HTTPXClientInstrumentor().instrument( tracer_provider=self.tracer_provider, async_request_hook=_request_hook, ) client = self.create_client() result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertEqual(span.name, "GET") def test_can_instrument_subclassed_async_client(self): class CustomAsyncClient(httpx.AsyncClient): pass client = CustomAsyncClient() self.assertFalse(isinstance(client._transport.handle_async_request, BaseObjectProxy)) HTTPXClientInstrumentor().instrument() self.assertTrue(isinstance(client._transport.handle_async_request, BaseObjectProxy)) class TestRawURLTupleAttributes(unittest.TestCase): """httpx < 0.20 passes raw URL tuples to the transport layer.""" def _apply_attributes(self, url_tuple): module = opentelemetry.instrumentation.httpx._httpx_module try: module.URL(url_tuple) except TypeError: self.skipTest("this httpx version does not accept raw URL tuples") span_attributes = {} metric_attributes = {} _apply_request_client_attributes_to_span( span_attributes, metric_attributes, url_tuple, "GET", _StabilityMode.DEFAULT, module, ) return span_attributes def test_ipv6_host_is_bracketed(self): span_attributes = self._apply_attributes((b"http", b"::1", 8080, b"/foo")) self.assertEqual(span_attributes[HTTP_URL], "http://[::1]:8080/foo") def test_port_zero_is_kept(self): span_attributes = self._apply_attributes((b"http", b"example.com", 0, b"/")) self.assertEqual(span_attributes[HTTP_URL], "http://example.com:0/") opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2/000077500000000000000000000000001527070072300323205ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2/LICENSE000066400000000000000000000261351527070072300333340ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2/README.rst000066400000000000000000000012141527070072300340050ustar00rootroot00000000000000OpenTelemetry jinja2 integration ================================ |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-jinja2.svg :target: https://pypi.org/project/opentelemetry-instrumentation-jinja2/ Installation ------------ :: pip install opentelemetry-instrumentation-jinja2 References ---------- * `OpenTelemetry jinja2 integration `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031031527070072300351520ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-jinja2" dynamic = ["version"] description = "OpenTelemetry jinja2 instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [ "jinja2 >= 2.7, < 4.0", ] [project.entry-points.opentelemetry_instrumentor] jinja2 = "opentelemetry.instrumentation.jinja2:Jinja2Instrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-jinja2" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/jinja2/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2/src/000077500000000000000000000000001527070072300331075ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300357245ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2/srcinstrumentation/000077500000000000000000000000001527070072300411675ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetryjinja2/000077500000000000000000000000001527070072300423445ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation__init__.py000066400000000000000000000114711527070072300444610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Usage ----- The OpenTelemetry ``jinja2`` integration traces templates loading, compilation and rendering. Usage ----- .. code-block:: python from jinja2 import Environment, FileSystemLoader from opentelemetry.instrumentation.jinja2 import Jinja2Instrumentor Jinja2Instrumentor().instrument() env = Environment(loader=FileSystemLoader("templates")) template = env.get_template("mytemplate.html") API --- """ # pylint: disable=no-value-for-parameter from __future__ import annotations import logging from collections.abc import Callable, Collection from types import CodeType from typing import Any, TypeVar import jinja2 from jinja2.environment import Template from wrapt import wrap_function_wrapper as _wrap from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.jinja2.package import _instruments from opentelemetry.instrumentation.jinja2.version import __version__ from opentelemetry.instrumentation.utils import unwrap from opentelemetry.trace import SpanKind, Tracer, get_tracer logger = logging.getLogger(__name__) ATTRIBUTE_JINJA2_TEMPLATE_NAME = "jinja2.template_name" ATTRIBUTE_JINJA2_TEMPLATE_PATH = "jinja2.template_path" DEFAULT_TEMPLATE_NAME = "" R = TypeVar("R") def _with_tracer_wrapper( func: Callable[[Tracer, Callable[..., R], Any, list[Any], dict[str, Any]], R], ) -> Callable[[Tracer], Callable[[Callable[..., R], Any, list[Any], dict[str, Any]], R]]: """Helper for providing tracer for wrapper functions.""" def _with_tracer( tracer: Tracer, ) -> Callable[[Callable[..., R], Any, list[Any], dict[str, Any]], R]: def wrapper( wrapped: Callable[..., R], instance: Any, args: list[Any], kwargs: dict[str, Any], ) -> R: return func(tracer, wrapped, instance, args, kwargs) return wrapper return _with_tracer @_with_tracer_wrapper def _wrap_render( tracer: Tracer, wrapped: Callable[..., Any], instance: Template, args: list[Any], kwargs: dict[str, Any], ): """Wrap `Template.render()` or `Template.generate()`""" with tracer.start_as_current_span( "jinja2.render", kind=SpanKind.INTERNAL, ) as span: if span.is_recording(): template_name = instance.name or DEFAULT_TEMPLATE_NAME span.set_attribute(ATTRIBUTE_JINJA2_TEMPLATE_NAME, template_name) return wrapped(*args, **kwargs) @_with_tracer_wrapper def _wrap_compile( tracer: Tracer, wrapped: Callable[..., CodeType], _, args: list[Any], kwargs: dict[str, Any], ) -> CodeType: with tracer.start_as_current_span( "jinja2.compile", kind=SpanKind.INTERNAL, ) as span: if span.is_recording(): template_name = args[1] if len(args) > 1 else kwargs.get("name", DEFAULT_TEMPLATE_NAME) span.set_attribute(ATTRIBUTE_JINJA2_TEMPLATE_NAME, template_name) return wrapped(*args, **kwargs) @_with_tracer_wrapper def _wrap_load_template( tracer: Tracer, wrapped: Callable[..., Template], _, args: list[Any], kwargs: dict[str, Any], ) -> Template: with tracer.start_as_current_span( "jinja2.load", kind=SpanKind.INTERNAL, ) as span: if span.is_recording(): template_name = kwargs.get("name", args[0]) span.set_attribute(ATTRIBUTE_JINJA2_TEMPLATE_NAME, template_name) template = None try: template = wrapped(*args, **kwargs) return template finally: if template and span.is_recording(): span.set_attribute(ATTRIBUTE_JINJA2_TEMPLATE_PATH, template.filename) class Jinja2Instrumentor(BaseInstrumentor): """An instrumentor for jinja2 See `BaseInstrumentor` """ def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs: Any): tracer_provider = kwargs.get("tracer_provider") tracer = get_tracer( __name__, __version__, tracer_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) _wrap(jinja2, "environment.Template.render", _wrap_render(tracer)) _wrap(jinja2, "environment.Template.generate", _wrap_render(tracer)) _wrap(jinja2, "environment.Environment.compile", _wrap_compile(tracer)) _wrap( jinja2, "environment.Environment._load_template", _wrap_load_template(tracer), ) def _uninstrument(self, **kwargs: Any): unwrap(jinja2.Template, "render") unwrap(jinja2.Template, "generate") unwrap(jinja2.Environment, "compile") unwrap(jinja2.Environment, "_load_template") package.py000066400000000000000000000001671527070072300443150ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("jinja2 >= 2.7, < 4.0",) py.typed000066400000000000000000000000001527070072300440310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2version.py000066400000000000000000000001441527070072300444020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000004541527070072300365050ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2asgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 Jinja2==3.1.6 MarkupSafe==2.0.1 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-jinja2 opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2/tests/000077500000000000000000000000001527070072300334625ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300355020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2/teststemplates/000077500000000000000000000000001527070072300354015ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2/testsbase.html000066400000000000000000000000531527070072300371770ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2/tests/templatesMessage: {% block content %}{% endblock %} template.html000066400000000000000000000001131527070072300400750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2/tests/templates{% extends 'base.html' %} {% block content %}Hello {{name}}!{% endblock %} test_jinja2.py000066400000000000000000000175141527070072300362010ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-jinja2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os from unittest import mock import jinja2 from packaging import version from opentelemetry import trace as trace_api from opentelemetry.instrumentation.jinja2 import Jinja2Instrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.trace import get_tracer TEST_DIR = os.path.dirname(os.path.realpath(__file__)) TMPL_DIR = os.path.join(TEST_DIR, "templates") class TestJinja2Instrumentor(TestBase): def setUp(self): super().setUp() Jinja2Instrumentor().instrument() # prevent cache effects when using Template('code...') if version.parse(jinja2.__version__) >= version.parse("3.0.0"): # by clearing functools.lru_cache jinja2.environment.get_spontaneous_environment.cache_clear() else: # by clearing jinja2.utils.LRUCache jinja2.environment._spontaneous_environments.clear() # pylint: disable=no-member self.tracer = get_tracer(__name__) def tearDown(self): super().tearDown() Jinja2Instrumentor().uninstrument() def test_render_inline_template_with_root(self): with self.tracer.start_as_current_span("root"): template = jinja2.environment.Template("Hello {{name}}!") self.assertEqual(template.render(name="Jinja"), "Hello Jinja!") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) # pylint:disable=unbalanced-tuple-unpacking render, template, root = spans[:3] self.assertIs(render.parent, root.get_span_context()) self.assertIs(template.parent, root.get_span_context()) self.assertIsNone(root.parent) def test_render_not_recording(self): mock_tracer = mock.Mock() mock_span = mock.Mock() mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span with mock.patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer jinja2.environment.Template("Hello {{name}}!") self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) def test_render_inline_template(self): template = jinja2.environment.Template("Hello {{name}}!") self.assertEqual(template.render(name="Jinja"), "Hello Jinja!") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # pylint:disable=unbalanced-tuple-unpacking template, render = spans self.assertEqual(template.name, "jinja2.compile") self.assertIs(template.kind, trace_api.SpanKind.INTERNAL) self.assertEqual( template.attributes, {"jinja2.template_name": ""}, ) self.assertEqual(render.name, "jinja2.render") self.assertIs(render.kind, trace_api.SpanKind.INTERNAL) self.assertEqual( render.attributes, {"jinja2.template_name": ""}, ) def test_generate_inline_template_with_root(self): with self.tracer.start_as_current_span("root"): template = jinja2.environment.Template("Hello {{name}}!") self.assertEqual("".join(template.generate(name="Jinja")), "Hello Jinja!") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) # pylint:disable=unbalanced-tuple-unpacking template, generate, root = spans self.assertIs(generate.parent, root.get_span_context()) self.assertIs(template.parent, root.get_span_context()) self.assertIsNone(root.parent) def test_generate_inline_template(self): template = jinja2.environment.Template("Hello {{name}}!") self.assertEqual("".join(template.generate(name="Jinja")), "Hello Jinja!") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # pylint:disable=unbalanced-tuple-unpacking template, generate = spans[:2] self.assertEqual(template.name, "jinja2.compile") self.assertIs(template.kind, trace_api.SpanKind.INTERNAL) self.assertEqual( template.attributes, {"jinja2.template_name": ""}, ) self.assertEqual(generate.name, "jinja2.render") self.assertIs(generate.kind, trace_api.SpanKind.INTERNAL) self.assertEqual( generate.attributes, {"jinja2.template_name": ""}, ) def test_file_template_with_root(self): with self.tracer.start_as_current_span("root"): loader = jinja2.loaders.FileSystemLoader(TMPL_DIR) env = jinja2.Environment(loader=loader, autoescape=True) template = env.get_template("template.html") self.assertEqual(template.render(name="Jinja"), "Message: Hello Jinja!") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 6) # pylint:disable=unbalanced-tuple-unpacking compile2, load2, compile1, load1, render, root = spans self.assertIs(compile2.parent, load2.get_span_context()) self.assertIs(load2.parent, root.get_span_context()) self.assertIs(compile1.parent, load1.get_span_context()) self.assertIs(load1.parent, render.get_span_context()) self.assertIs(render.parent, root.get_span_context()) self.assertIsNone(root.parent) def test_file_template(self): loader = jinja2.loaders.FileSystemLoader(TMPL_DIR) env = jinja2.Environment(loader=loader, autoescape=True) template = env.get_template("template.html") self.assertEqual(template.render(name="Jinja"), "Message: Hello Jinja!") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 5) # pylint:disable=unbalanced-tuple-unpacking compile2, load2, compile1, load1, render = spans self.assertEqual(compile2.name, "jinja2.compile") self.assertEqual(load2.name, "jinja2.load") self.assertEqual(compile1.name, "jinja2.compile") self.assertEqual(load1.name, "jinja2.load") self.assertEqual(render.name, "jinja2.render") self.assertEqual( compile2.attributes, {"jinja2.template_name": "template.html"}, ) self.assertEqual( load2.attributes, { "jinja2.template_name": "template.html", "jinja2.template_path": os.path.join(TMPL_DIR, "template.html"), }, ) self.assertEqual( compile1.attributes, {"jinja2.template_name": "base.html"}, ) self.assertEqual( load1.attributes, { "jinja2.template_name": "base.html", "jinja2.template_path": os.path.join(TMPL_DIR, "base.html"), }, ) self.assertEqual( render.attributes, {"jinja2.template_name": "template.html"}, ) def test_uninstrumented(self): Jinja2Instrumentor().uninstrument() jinja2.environment.Template("Hello {{name}}!") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) Jinja2Instrumentor().instrument() def test_no_op_tracer_provider(self): self.memory_exporter.clear() Jinja2Instrumentor().uninstrument() Jinja2Instrumentor().instrument(tracer_provider=trace_api.NoOpTracerProvider()) template = jinja2.environment.Template("Hello {{name}}!") self.assertEqual(template.render(name="Jinja"), "Hello Jinja!") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-python/000077500000000000000000000000001527070072300335375ustar00rootroot00000000000000LICENSE000066400000000000000000000261351527070072300344740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-python Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000013001527070072300351410ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-pythonOpenTelemetry kafka-python integration ====================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-kafka-python.svg :target: https://pypi.org/project/opentelemetry-instrumentation-kafka-python/ Installation ------------ :: pip install opentelemetry-instrumentation-kafka-python References ---------- * `OpenTelemetry kafka-python Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031531527070072300363760ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-python[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-kafka-python" dynamic = ["version"] description = "OpenTelemetry Kafka-Python instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.5", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", ] [project.optional-dependencies] instruments = [ "kafka-python >= 2.0, < 4.0", ] [project.entry-points.opentelemetry_instrumentor] kafka = "opentelemetry.instrumentation.kafka:KafkaInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-kafka-python" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/kafka/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-python/src/000077500000000000000000000000001527070072300343265ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300371435ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-python/srcinstrumentation/000077500000000000000000000000001527070072300424065ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetrykafka/000077500000000000000000000000001527070072300434635ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry/instrumentation__init__.py000066400000000000000000000104721527070072300456000ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry/instrumentation/kafka# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Instrument kafka-python to report instrumentation-kafka produced and consumed messages Usage ----- .. code:: python from opentelemetry.instrumentation.kafka import KafkaInstrumentor from kafka import KafkaProducer, KafkaConsumer # Instrument kafka KafkaInstrumentor().instrument() # report a span of type producer with the default settings producer = KafkaProducer(bootstrap_servers=['localhost:9092']) producer.send('my-topic', b'raw_bytes') def process_msg(message): print(message) # report a span of type consumer with the default settings consumer = KafkaConsumer('my-topic', group_id='my-group', bootstrap_servers=['localhost:9092']) for message in consumer: # process message process_msg(message) The ``_instrument()`` method accepts the following keyword args: - **tracer_provider** (TracerProvider) - an optional tracer provider - **produce_hook** (Callable) - a function with extra user-defined logic to be performed before sending the message Function signature: .. code:: python def produce_hook(span: Span, args, kwargs): ... - **consume_hook** (Callable) - a function with extra user-defined logic to be performed after consuming a message Function signature: .. code:: python def consume_hook(span: Span, record: kafka.record.ABCRecord, args, kwargs): ... For example: .. code:: python from opentelemetry.instrumentation.kafka import KafkaInstrumentor from kafka import KafkaProducer, KafkaConsumer def produce_hook(span, args, kwargs): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_produce_hook", "some-value") def consume_hook(span, record, args, kwargs): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_consume_hook", "some-value") # instrument kafka with produce and consume hooks KafkaInstrumentor().instrument(produce_hook=produce_hook, consume_hook=consume_hook) # Using kafka as normal now will automatically generate spans, # including user custom attributes added from the hooks producer = KafkaProducer(bootstrap_servers=['localhost:9092']) producer.send('my-topic', b'raw_bytes') def process_msg(message): print(message) consumer = KafkaConsumer('my-topic', group_id='my-group', bootstrap_servers=['localhost:9092']) for message in consumer: # process message process_msg(message) API --- """ from collections.abc import Collection import kafka from wrapt import wrap_function_wrapper from opentelemetry import trace from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.kafka.package import _instruments from opentelemetry.instrumentation.kafka.utils import _wrap_next, _wrap_send from opentelemetry.instrumentation.kafka.version import __version__ from opentelemetry.instrumentation.utils import unwrap class KafkaInstrumentor(BaseInstrumentor): """An instrumentor for kafka module See `BaseInstrumentor` """ def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): """Instruments the kafka module Args: **kwargs: Optional arguments ``tracer_provider``: a TracerProvider, defaults to global. ``produce_hook``: a callable to be executed just before producing a message ``consume_hook``: a callable to be executed just after consuming a message """ tracer_provider = kwargs.get("tracer_provider") produce_hook = kwargs.get("produce_hook") consume_hook = kwargs.get("consume_hook") tracer = trace.get_tracer( __name__, __version__, tracer_provider=tracer_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) wrap_function_wrapper(kafka.KafkaProducer, "send", _wrap_send(tracer, produce_hook)) wrap_function_wrapper( kafka.KafkaConsumer, "__next__", _wrap_next(tracer, consume_hook), ) def _uninstrument(self, **kwargs): unwrap(kafka.KafkaProducer, "send") unwrap(kafka.KafkaConsumer, "__next__") package.py000066400000000000000000000001751527070072300454330ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry/instrumentation/kafka# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("kafka-python >= 2.0, < 4.0",) utils.py000066400000000000000000000141741527070072300452040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry/instrumentation/kafka# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json from collections.abc import Callable from logging import getLogger from kafka.record.abc import ABCRecord from opentelemetry import context, propagate, trace from opentelemetry.propagators import textmap from opentelemetry.semconv._incubating.attributes import messaging_attributes from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import Tracer from opentelemetry.trace.span import Span _LOG = getLogger(__name__) class KafkaPropertiesExtractor: @staticmethod def extract_bootstrap_servers(instance): return instance.config.get("bootstrap_servers") @staticmethod def _extract_argument(key, position, default_value, args, kwargs): if len(args) > position: return args[position] return kwargs.get(key, default_value) @staticmethod def extract_send_topic(args, kwargs): """extract topic from `send` method arguments in KafkaProducer class""" return KafkaPropertiesExtractor._extract_argument("topic", 0, "unknown", args, kwargs) @staticmethod def extract_send_headers(args, kwargs): """extract headers from `send` method arguments in KafkaProducer class""" return KafkaPropertiesExtractor._extract_argument("headers", 3, None, args, kwargs) @staticmethod def extract_send_partition(future) -> int | None: """Extract the assigned partition from the future returned by `send`. `send()` resolves the partition internally (randomly for keyless messages), so it must be read back from the future rather than recomputed with the partitioner. """ try: return future._produce_future.topic_partition[1] except (AttributeError, IndexError, TypeError) as exception: _LOG.debug("Unable to extract partition: %s", exception) return None ProduceHookT = Callable[[Span, list, dict], None] | None ConsumeHookT = Callable[[Span, ABCRecord, list, dict], None] | None class KafkaContextGetter(textmap.Getter[textmap.CarrierT]): def get(self, carrier: textmap.CarrierT, key: str) -> list[str] | None: if carrier is None: return None for item_key, value in carrier: if item_key == key: if value is not None: return [value.decode()] return None def keys(self, carrier: textmap.CarrierT) -> list[str]: if carrier is None: return [] return [key for (key, value) in carrier] class KafkaContextSetter(textmap.Setter[textmap.CarrierT]): def set(self, carrier: textmap.CarrierT, key: str, value: str) -> None: if carrier is None or key is None: return if value: value = value.encode() carrier.append((key, value)) _kafka_getter = KafkaContextGetter() _kafka_setter = KafkaContextSetter() def _enrich_span( span, bootstrap_servers: list[str], topic: str, partition: int | None, ): if span.is_recording(): span.set_attribute(messaging_attributes.MESSAGING_SYSTEM, "kafka") span.set_attribute(SpanAttributes.MESSAGING_DESTINATION, topic) if partition is not None: span.set_attribute(SpanAttributes.MESSAGING_KAFKA_PARTITION, partition) span.set_attribute(SpanAttributes.MESSAGING_URL, json.dumps(bootstrap_servers)) def _get_span_name(operation: str, topic: str): return f"{topic} {operation}" def _wrap_send(tracer: Tracer, produce_hook: ProduceHookT) -> Callable: def _traced_send(func, instance, args, kwargs): headers = KafkaPropertiesExtractor.extract_send_headers(args, kwargs) if headers is None: headers = [] kwargs["headers"] = headers topic = KafkaPropertiesExtractor.extract_send_topic(args, kwargs) bootstrap_servers = KafkaPropertiesExtractor.extract_bootstrap_servers(instance) span_name = _get_span_name("send", topic) with tracer.start_as_current_span(span_name, kind=trace.SpanKind.PRODUCER) as span: propagate.inject( headers, context=trace.set_span_in_context(span), setter=_kafka_setter, ) try: if callable(produce_hook): produce_hook(span, args, kwargs) except Exception as hook_exception: # pylint: disable=W0703 _LOG.exception(hook_exception) future = func(*args, **kwargs) partition = KafkaPropertiesExtractor.extract_send_partition(future) _enrich_span(span, bootstrap_servers, topic, partition) return future return _traced_send def _create_consumer_span( tracer, consume_hook, record, extracted_context, bootstrap_servers, args, kwargs, ): span_name = _get_span_name("receive", record.topic) with tracer.start_as_current_span( span_name, context=extracted_context, kind=trace.SpanKind.CONSUMER, ) as span: new_context = trace.set_span_in_context(span, extracted_context) token = context.attach(new_context) _enrich_span(span, bootstrap_servers, record.topic, record.partition) try: if callable(consume_hook): consume_hook(span, record, args, kwargs) except Exception as hook_exception: # pylint: disable=W0703 _LOG.exception(hook_exception) if token: context.detach(token) def _wrap_next( tracer: Tracer, consume_hook: ConsumeHookT, ) -> Callable: def _traced_next(func, instance, args, kwargs): record = func(*args, **kwargs) if record: bootstrap_servers = KafkaPropertiesExtractor.extract_bootstrap_servers(instance) extracted_context = propagate.extract(record.headers, getter=_kafka_getter) _create_consumer_span( tracer, consume_hook, record, extracted_context, bootstrap_servers, args, kwargs, ) return record return _traced_next version.py000066400000000000000000000001441527070072300455210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry/instrumentation/kafka# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.in000066400000000000000000000003151527070072300375070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-pythonkafka-python>=2.0.3,<4.0.0 # 2.0.3 is the earliest version to support python > 3.11 pytest~=7.4 wrapt~=2.2.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-kafka-python test-requirements.latest.txt000066400000000000000000000032711527070072300412170ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-python# This file was autogenerated by uv via the following command: # uv pip compile --python 3.10 --universal --no-emit-package opentelemetry-api --no-emit-package opentelemetry-semantic-conventions -c dev-requirements.txt instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.in -o instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.latest.txt -e instrumentation/opentelemetry-instrumentation-kafka-python # via -r instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.in -e opentelemetry-instrumentation # via # -r instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.in # opentelemetry-instrumentation-kafka-python colorama==0.4.6 ; sys_platform == 'win32' # via pytest exceptiongroup==1.3.1 ; python_full_version < '3.11' # via pytest iniconfig==2.3.0 # via pytest kafka-python==3.0.8 # via -r instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.in packaging==26.2 # via # opentelemetry-instrumentation # pytest pluggy==1.6.0 # via pytest pytest==7.4.4 # via # -c dev-requirements.txt # -r instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.in tomli==2.4.1 ; python_full_version < '3.11' # via pytest typing-extensions==4.16.0 # via # exceptiongroup # opentelemetry-api # opentelemetry-semantic-conventions wrapt==2.2.2 # via # -r instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.in # opentelemetry-instrumentation # The following packages were excluded from the output: # opentelemetry-api # opentelemetry-semantic-conventions test-requirements.oldest.txt000066400000000000000000000034311527070072300412130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-python# This file was autogenerated by uv via the following command: # uv pip compile --python 3.10 --universal --resolution lowest-direct --no-emit-package opentelemetry-api --no-emit-package opentelemetry-semantic-conventions -c dev-requirements.txt instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.in -o instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.oldest.txt -e instrumentation/opentelemetry-instrumentation-kafka-python # via -r instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.in -e opentelemetry-instrumentation # via # -r instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.in # opentelemetry-instrumentation-kafka-python colorama==0.4.6 ; sys_platform == 'win32' # via pytest exceptiongroup==1.3.1 ; python_full_version < '3.11' # via pytest iniconfig==2.3.0 # via pytest kafka-python==2.0.3 # via -r instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.in packaging==18.0 # via # opentelemetry-instrumentation # pytest pluggy==1.6.0 # via pytest pyparsing==3.3.2 # via packaging pytest==7.4.4 # via # -c dev-requirements.txt # -r instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.in six==1.17.0 # via packaging tomli==2.4.1 ; python_full_version < '3.11' # via pytest typing-extensions==4.16.0 # via # exceptiongroup # opentelemetry-api # opentelemetry-semantic-conventions wrapt==2.2.2 # via # -r instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.in # opentelemetry-instrumentation # The following packages were excluded from the output: # opentelemetry-api # opentelemetry-semantic-conventions tests/000077500000000000000000000000001527070072300346225ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-python__init__.py000066400000000000000000000000001527070072300367210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-python/teststest_instrumentation.py000066400000000000000000000020171527070072300414760ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-python/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from kafka import KafkaConsumer, KafkaProducer from wrapt import BoundFunctionWrapper from opentelemetry.instrumentation.kafka import KafkaInstrumentor from opentelemetry.instrumentation.kafka.package import _instruments class TestKafka(TestCase): def test_instrument_api(self) -> None: instrumentation = KafkaInstrumentor() instrumentation.instrument() self.assertTrue(isinstance(KafkaProducer.send, BoundFunctionWrapper)) self.assertTrue(isinstance(KafkaConsumer.__next__, BoundFunctionWrapper)) instrumentation.uninstrument() self.assertFalse(isinstance(KafkaProducer.send, BoundFunctionWrapper)) self.assertFalse(isinstance(KafkaConsumer.__next__, BoundFunctionWrapper)) def test_instrumentation_dependencies(self) -> None: instrumentation = KafkaInstrumentor() self.assertEqual(instrumentation.instrumentation_dependencies(), _instruments) test_utils.py000066400000000000000000000277611527070072300374100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-kafka-python/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=unnecessary-dunder-call from unittest import TestCase, mock from kafka.producer.future import FutureProduceResult, FutureRecordMetadata from opentelemetry.instrumentation.kafka.utils import ( KafkaPropertiesExtractor, _create_consumer_span, _enrich_span, _get_span_name, _kafka_getter, _kafka_setter, _wrap_next, _wrap_send, ) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind, StatusCode class TestUtils(TestCase): def setUp(self) -> None: super().setUp() self.topic_name = "test_topic" self.args = [self.topic_name] self.headers = [] self.kwargs = {"partition": 0, "headers": self.headers} @mock.patch("opentelemetry.instrumentation.kafka.utils.KafkaPropertiesExtractor.extract_bootstrap_servers") @mock.patch("opentelemetry.instrumentation.kafka.utils.KafkaPropertiesExtractor.extract_send_partition") @mock.patch("opentelemetry.instrumentation.kafka.utils._enrich_span") @mock.patch("opentelemetry.trace.set_span_in_context") @mock.patch("opentelemetry.propagate.inject") def test_wrap_send_with_topic_as_arg( self, inject: mock.MagicMock, set_span_in_context: mock.MagicMock, enrich_span: mock.MagicMock, extract_send_partition: mock.MagicMock, extract_bootstrap_servers: mock.MagicMock, ) -> None: self.wrap_send_helper( inject, set_span_in_context, enrich_span, extract_send_partition, extract_bootstrap_servers, ) @mock.patch("opentelemetry.instrumentation.kafka.utils.KafkaPropertiesExtractor.extract_bootstrap_servers") @mock.patch("opentelemetry.instrumentation.kafka.utils.KafkaPropertiesExtractor.extract_send_partition") @mock.patch("opentelemetry.instrumentation.kafka.utils._enrich_span") @mock.patch("opentelemetry.trace.set_span_in_context") @mock.patch("opentelemetry.propagate.inject") def test_wrap_send_with_topic_as_kwarg( self, inject: mock.MagicMock, set_span_in_context: mock.MagicMock, enrich_span: mock.MagicMock, extract_send_partition: mock.MagicMock, extract_bootstrap_servers: mock.MagicMock, ) -> None: self.args = [] self.kwargs["topic"] = self.topic_name self.wrap_send_helper( inject, set_span_in_context, enrich_span, extract_send_partition, extract_bootstrap_servers, ) def wrap_send_helper( self, inject: mock.MagicMock, set_span_in_context: mock.MagicMock, enrich_span: mock.MagicMock, extract_send_partition: mock.MagicMock, extract_bootstrap_servers: mock.MagicMock, ) -> None: tracer = mock.MagicMock() produce_hook = mock.MagicMock() original_send_callback = mock.MagicMock() kafka_producer = mock.MagicMock() expected_span_name = _get_span_name("send", self.topic_name) wrapped_send = _wrap_send(tracer, produce_hook) retval = wrapped_send(original_send_callback, kafka_producer, self.args, self.kwargs) extract_bootstrap_servers.assert_called_once_with(kafka_producer) # The partition is read back from the future returned by send(), not # estimated from the call arguments. extract_send_partition.assert_called_once_with(original_send_callback.return_value) tracer.start_as_current_span.assert_called_once_with(expected_span_name, kind=SpanKind.PRODUCER) span = tracer.start_as_current_span().__enter__.return_value enrich_span.assert_called_once_with( span, extract_bootstrap_servers.return_value, self.topic_name, extract_send_partition.return_value, ) set_span_in_context.assert_called_once_with(span) context = set_span_in_context.return_value inject.assert_called_once_with(self.headers, context=context, setter=_kafka_setter) produce_hook.assert_called_once_with(span, self.args, self.kwargs) original_send_callback.assert_called_once_with(*self.args, **self.kwargs) self.assertEqual(retval, original_send_callback.return_value) @mock.patch("opentelemetry.propagate.extract") @mock.patch("opentelemetry.instrumentation.kafka.utils._create_consumer_span") @mock.patch("opentelemetry.instrumentation.kafka.utils.KafkaPropertiesExtractor.extract_bootstrap_servers") def test_wrap_next( self, extract_bootstrap_servers: mock.MagicMock, _create_consumer_span: mock.MagicMock, extract: mock.MagicMock, ) -> None: tracer = mock.MagicMock() consume_hook = mock.MagicMock() original_next_callback = mock.MagicMock() kafka_consumer = mock.MagicMock() wrapped_next = _wrap_next(tracer, consume_hook) record = wrapped_next(original_next_callback, kafka_consumer, self.args, self.kwargs) extract_bootstrap_servers.assert_called_once_with(kafka_consumer) bootstrap_servers = extract_bootstrap_servers.return_value original_next_callback.assert_called_once_with(*self.args, **self.kwargs) self.assertEqual(record, original_next_callback.return_value) extract.assert_called_once_with(record.headers, getter=_kafka_getter) context = extract.return_value _create_consumer_span.assert_called_once_with( tracer, consume_hook, record, context, bootstrap_servers, self.args, self.kwargs, ) @mock.patch("opentelemetry.trace.set_span_in_context") @mock.patch("opentelemetry.context.attach") @mock.patch("opentelemetry.instrumentation.kafka.utils._enrich_span") @mock.patch("opentelemetry.context.detach") def test_create_consumer_span( self, detach: mock.MagicMock, enrich_span: mock.MagicMock, attach: mock.MagicMock, set_span_in_context: mock.MagicMock, ) -> None: tracer = mock.MagicMock() consume_hook = mock.MagicMock() bootstrap_servers = mock.MagicMock() extracted_context = mock.MagicMock() record = mock.MagicMock() _create_consumer_span( tracer, consume_hook, record, extracted_context, bootstrap_servers, self.args, self.kwargs, ) expected_span_name = _get_span_name("receive", record.topic) tracer.start_as_current_span.assert_called_once_with( expected_span_name, context=extracted_context, kind=SpanKind.CONSUMER, ) span = tracer.start_as_current_span.return_value.__enter__() set_span_in_context.assert_called_once_with(span, extracted_context) attach.assert_called_once_with(set_span_in_context.return_value) enrich_span.assert_called_once_with(span, bootstrap_servers, record.topic, record.partition) consume_hook.assert_called_once_with(span, record, self.args, self.kwargs) detach.assert_called_once_with(attach.return_value) def test_extract_send_partition_reads_actual_partition_from_future(self): """The partition is read back from the future returned by ``send()``, which reflects where the message was actually routed. Regression test for https://github.com/open-telemetry/opentelemetry-python-contrib/issues/4625 """ future = mock.MagicMock() future._produce_future.topic_partition = ("test_topic", 7) partition = KafkaPropertiesExtractor.extract_send_partition(future) self.assertEqual(partition, 7) def test_extract_send_partition_zero(self): """Partition ``0`` is falsy but valid and must not be dropped.""" future = mock.MagicMock() future._produce_future.topic_partition = ("test_topic", 0) partition = KafkaPropertiesExtractor.extract_send_partition(future) self.assertEqual(partition, 0) def test_extract_send_partition_matches_real_kafka_future(self): """Guards against kafka-python changing the internal attribute the extractor relies on.""" produce_future = FutureProduceResult(("test_topic", 4)) future = FutureRecordMetadata(produce_future, 0, None, None, -1, -1, -1) partition = KafkaPropertiesExtractor.extract_send_partition(future) self.assertEqual(partition, 4) def test_extract_send_partition_returns_none_when_unavailable(self): """If the future does not expose the expected internals, the attribute is omitted rather than raising.""" class _NoInternals: pass partition = KafkaPropertiesExtractor.extract_send_partition(_NoInternals()) self.assertIsNone(partition) def test_enrich_span_records_partition_when_present(self): span = mock.MagicMock() span.is_recording.return_value = True _enrich_span(span, ["localhost:9092"], self.topic_name, 2) span.set_attribute.assert_any_call(SpanAttributes.MESSAGING_KAFKA_PARTITION, 2) def test_enrich_span_omits_partition_when_none(self): span = mock.MagicMock() span.is_recording.return_value = True _enrich_span(span, ["localhost:9092"], self.topic_name, None) recorded_attributes = {call.args[0] for call in span.set_attribute.call_args_list} self.assertNotIn(SpanAttributes.MESSAGING_KAFKA_PARTITION, recorded_attributes) @mock.patch( "opentelemetry.instrumentation.kafka.utils.KafkaPropertiesExtractor.extract_send_partition", return_value=0, ) class TestWrapSendSpanLifetime(TestBase): """The producer span must stay open while the wrapped ``send`` runs, so that errors raised by ``send()`` are recorded on the span.""" def setUp(self) -> None: super().setUp() self.tracer = self.tracer_provider.get_tracer(__name__) self.topic_name = "test_topic" self.args = [self.topic_name] self.kwargs = {"partition": 0, "headers": []} self.producer = mock.MagicMock() self.producer.config = {"bootstrap_servers": ["localhost:9092"]} def test_wrap_send_records_exception_raised_by_send(self, _extract_send_partition: mock.MagicMock) -> None: error = ConnectionError("broker unavailable") def failing_send(*args, **kwargs): raise error wrapped_send = _wrap_send(self.tracer, None) with self.assertRaises(ConnectionError) as raised: wrapped_send(failing_send, self.producer, self.args, self.kwargs) self.assertIs(raised.exception, error) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, _get_span_name("send", self.topic_name)) self.assertEqual(span.kind, SpanKind.PRODUCER) self.assertIs(span.status.status_code, StatusCode.ERROR) self.assertEqual(len(span.events), 1) self.assertEqual(span.events[0].name, "exception") self.assertEqual(span.events[0].attributes["exception.type"], "ConnectionError") def test_wrap_send_leaves_successful_send_unchanged(self, _extract_send_partition: mock.MagicMock) -> None: original_send = mock.MagicMock() wrapped_send = _wrap_send(self.tracer, None) retval = wrapped_send(original_send, self.producer, self.args, self.kwargs) original_send.assert_called_once_with(*self.args, **self.kwargs) self.assertEqual(retval, original_send.return_value) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, _get_span_name("send", self.topic_name)) self.assertEqual(span.kind, SpanKind.PRODUCER) self.assertIs(span.status.status_code, StatusCode.UNSET) self.assertEqual(len(span.events), 0) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/000077500000000000000000000000001527070072300325715ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/LICENSE000066400000000000000000000261351527070072300336050ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/README.rst000066400000000000000000000012241527070072300342570ustar00rootroot00000000000000OpenTelemetry logging integration ================================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-logging.svg :target: https://pypi.org/project/opentelemetry-instrumentation-logging/ Installation ------------ :: pip install opentelemetry-instrumentation-logging References ---------- * `OpenTelemetry logging integration `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ benchmark-requirements.txt000066400000000000000000000000301527070072300377170ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-loggingpytest-benchmark==4.0.0 benchmarks/000077500000000000000000000000001527070072300346275ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-loggingtest_benchmark_logging_handler.py000066400000000000000000000023271527070072300434010ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/benchmarks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import pytest from opentelemetry.instrumentation.logging.handler import LoggingHandler from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import ( InMemoryLogRecordExporter, SimpleLogRecordProcessor, ) def _set_up_logging_handler(level): logger_provider = LoggerProvider() exporter = InMemoryLogRecordExporter() processor = SimpleLogRecordProcessor(exporter=exporter) logger_provider.add_log_record_processor(processor) handler = LoggingHandler(level=level, logger_provider=logger_provider) return handler def _create_logger(handler, name): logger = logging.getLogger(name) logger.addHandler(handler) return logger @pytest.mark.parametrize("num_loggers", [1, 10, 100, 1000]) def test_simple_get_logger_different_names(benchmark, num_loggers): handler = _set_up_logging_handler(level=logging.DEBUG) loggers = [_create_logger(handler, str(f"logger_{i}")) for i in range(num_loggers)] def benchmark_get_logger(): for index in range(1000): loggers[index % num_loggers].warning("test message") benchmark(benchmark_get_logger) pyproject.toml000066400000000000000000000031041527070072300354240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-logging" dynamic = ["version"] description = "OpenTelemetry Logging instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", ] [project.optional-dependencies] instruments = [] [project.entry-points.opentelemetry_instrumentor] logging = "opentelemetry.instrumentation.logging:LoggingInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-logging" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/logging/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/src/000077500000000000000000000000001527070072300333605ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300361755ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/srcinstrumentation/000077500000000000000000000000001527070072300414405ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetrylogging/000077500000000000000000000000001527070072300430665ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation__init__.py000066400000000000000000000224231527070072300452020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation/logging# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=empty-docstring,no-value-for-parameter,no-member,no-name-in-module """ The OpenTelemetry `logging` instrumentation automatically instruments Python logging system with an handler to convert log messages into OpenTelemetry logs. You can disable this setting `OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION` to `false`. Trace context injection is opt-in. Pass ``inject_trace_context=True`` to add ``otelSpanID``, ``otelTraceID``, ``otelTraceSampled``, and ``otelServiceName`` to every log record without changing the logging format: .. code-block:: python import logging from opentelemetry.instrumentation.logging import LoggingInstrumentor LoggingInstrumentor().instrument(inject_trace_context=True) logging.warning("OTel test") Alternatively, set ``set_logging_format=True`` (or the environment variable ``OTEL_PYTHON_LOG_CORRELATION=true``) to inject those same attributes and call ``logging.basicConfig()`` with a format string that includes them: .. code-block:: python import logging from opentelemetry.instrumentation.logging import LoggingInstrumentor LoggingInstrumentor().instrument(set_logging_format=True) logging.warning("OTel test") When running the above example you will see the following output: :: 2025-03-05 09:40:04,398 WARNING [root] [example.py:7] [trace_id=0 span_id=0 resource.service.name= trace_sampled=False] - OTel test """ import logging # pylint: disable=import-self from collections.abc import Collection from os import environ from opentelemetry._logs import get_logger_provider from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.logging.constants import ( _MODULE_DOC, DEFAULT_LOGGING_FORMAT, ) from opentelemetry.instrumentation.logging.environment_variables import ( OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION, OTEL_PYTHON_LOG_CODE_ATTRIBUTES, OTEL_PYTHON_LOG_CORRELATION, OTEL_PYTHON_LOG_FORMAT, OTEL_PYTHON_LOG_HANDLER_LEVEL, OTEL_PYTHON_LOG_LEVEL, ) from opentelemetry.instrumentation.logging.handler import ( _setup_logging_handler, ) from opentelemetry.instrumentation.logging.package import _instruments from opentelemetry.trace import ( INVALID_SPAN, INVALID_SPAN_CONTEXT, get_current_span, get_tracer_provider, ) __doc__ = _MODULE_DOC LEVELS = { "debug": logging.DEBUG, "info": logging.INFO, "warning": logging.WARNING, "error": logging.ERROR, } _logger = logging.getLogger(__name__) def _get_log_level(level_name: str | None) -> int | None: if level_name is None: return None result = logging.getLevelName(level_name.upper().strip()) if not isinstance(result, int): _logger.warning( "Invalid log level %r for %s; defaulting to NOTSET", level_name, OTEL_PYTHON_LOG_HANDLER_LEVEL, ) return logging.NOTSET return result class LoggingInstrumentor(BaseInstrumentor): # pylint: disable=empty-docstring __doc__ = f"""An instrumentor for stdlib logging module. This instrumentor optionally injects tracing context into logging records and sets the global logging format to the following: .. code-block:: {DEFAULT_LOGGING_FORMAT} def log_hook(span: Span, record: LogRecord): if span and span.is_recording(): record.custom_user_attribute_from_log_hook = "some-value" span_ctx = span.get_span_context() record.from_sampled_span = span_ctx.trace_flags.sampled Args: tracer_provider: Tracer provider instance that can be used to fetch a tracer. set_logging_format: When set to True, injects trace context attributes into log records and calls logging.basicConfig() with a format string that includes those attributes. inject_trace_context: When set to True, injects trace context attributes into every log record without modifying the logging format. logging_format: Accepts a string and sets it as the logging format when set_logging_format is set to True. log_level: Accepts one of the following values and sets the logging level to it. logging.INFO logging.DEBUG logging.WARN logging.ERROR logging.FATAL log_hook: execute custom logic when record is created See `BaseInstrumentor` """ _old_factory = None _log_hook = None _logging_handler = None def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): provider = kwargs.get("tracer_provider", None) or get_tracer_provider() old_factory = logging.getLogRecordFactory() LoggingInstrumentor._old_factory = old_factory LoggingInstrumentor._log_hook = kwargs.get("log_hook", None) service_name = None set_logging_format = kwargs.get( "set_logging_format", environ.get(OTEL_PYTHON_LOG_CORRELATION, "false").lower() == "true", ) if set_logging_format: log_format = ( kwargs.get("logging_format", environ.get(OTEL_PYTHON_LOG_FORMAT, None)) or DEFAULT_LOGGING_FORMAT ) log_level = kwargs.get("log_level", LEVELS.get(environ.get(OTEL_PYTHON_LOG_LEVEL))) or logging.INFO logging.basicConfig(format=log_format, level=log_level) inject_context = set_logging_format or kwargs.get("inject_trace_context", False) def record_factory(*args, **kwargs): record = old_factory(*args, **kwargs) if not inject_context and not callable(LoggingInstrumentor._log_hook): return record if inject_context: record.otelSpanID = "0" record.otelTraceID = "0" record.otelTraceSampled = False nonlocal service_name if service_name is None: resource = getattr(provider, "resource", None) if resource: service_name = resource.attributes.get("service.name") or "" else: service_name = "" record.otelServiceName = service_name span = get_current_span() if span != INVALID_SPAN: ctx = span.get_span_context() if ctx != INVALID_SPAN_CONTEXT: if inject_context: record.otelSpanID = format(ctx.span_id, "016x") record.otelTraceID = format(ctx.trace_id, "032x") record.otelTraceSampled = ctx.trace_flags.sampled if callable(LoggingInstrumentor._log_hook): try: LoggingInstrumentor._log_hook( # pylint: disable=E1102 span, record ) except Exception: # pylint: disable=W0703 pass return record logging.setLogRecordFactory(record_factory) # Here we need to handle 3 scenarios: # - the sdk logging handler is enabled and we should do no nothing # - the sdk logging handler is not enabled and we should setup the handler by default # - the sdk logging handler is not enabled and the user do not want we setup the handler sdk_autoinstrumentation_env_var = ( environ.get("OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED", "notset").strip().lower() ) if sdk_autoinstrumentation_env_var == "true": _logger.warning( "Skipping installation of LoggingHandler from " "`opentelemetry-instrumentation-logging` to avoid duplicate logs. " "The SDK's deprecated LoggingHandler is already active " "(OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true). To migrate, unset " "this environment variable. The SDK's handler will be removed in a future release." ) elif kwargs.get( "enable_log_auto_instrumentation", environ.get(OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION, "true").strip().lower() == "true", ): log_code_attributes = kwargs.get( "log_code_attributes", environ.get(OTEL_PYTHON_LOG_CODE_ATTRIBUTES, "false").strip().lower() == "true", ) handler_level = kwargs.get( "log_handler_level", _get_log_level(environ.get(OTEL_PYTHON_LOG_HANDLER_LEVEL)), ) logger_provider = get_logger_provider() handler = _setup_logging_handler( logger_provider=logger_provider, log_code_attributes=log_code_attributes, level=handler_level, ) LoggingInstrumentor._logging_handler = handler def _uninstrument(self, **kwargs): if LoggingInstrumentor._old_factory: logging.setLogRecordFactory(LoggingInstrumentor._old_factory) LoggingInstrumentor._old_factory = None if LoggingInstrumentor._logging_handler: logging.getLogger().removeHandler(LoggingInstrumentor._logging_handler) LoggingInstrumentor._logging_handler = None constants.py000066400000000000000000000156211527070072300454610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation/logging# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 DEFAULT_LOGGING_FORMAT = "%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] [trace_id=%(otelTraceID)s span_id=%(otelSpanID)s resource.service.name=%(otelServiceName)s trace_sampled=%(otelTraceSampled)s] - %(message)s" _MODULE_DOC = f""" The OpenTelemetry ``logging`` instrumentation automatically instruments Python logging with a handler to convert Python log messages into OpenTelemetry logs and export them. You can disable this by setting ``OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION`` to ``false``. .. warning:: This package provides a logging handler to replace the deprecated one in ``opentelemetry-sdk``. Therefore if you have ``opentelemetry-instrumentation-logging`` installed, you don't need to set the ``OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED`` environment variable to ``true``. By default, this instrumentation does not add ``code`` namespace attributes as the SDK's logger does, but adding them can be enabled by using the ``OTEL_PYTHON_LOG_CODE_ATTRIBUTES`` environment variable. Enable trace context injection ------------------------------ The OpenTelemetry ``logging`` integration can also be configured to inject tracing context into log statements. The integration registers a custom log record factory with the the standard library logging module that automatically inject tracing context into log record objects. Optionally, the integration can also call ``logging.basicConfig()`` to set a logging format with placeholders for span ID, trace ID and service name. The following keys are injected into log record objects by the factory: - ``otelSpanID`` - ``otelTraceID`` - ``otelServiceName`` - ``otelTraceSampled`` The integration uses the following logging format by default: .. code-block:: {DEFAULT_LOGGING_FORMAT} Trace context injection is opt-in and can be enabled in two ways: - Pass ``inject_trace_context=True`` to inject trace context attributes into every log record without modifying the logging format. Use this when you manage the logging format yourself but still want ``otelSpanID``, ``otelTraceID``, ``otelTraceSampled``, and ``otelServiceName`` available on each record. - Set ``OTEL_PYTHON_LOG_CORRELATION`` to ``true`` (or pass ``set_logging_format=True``) to inject the same trace context attributes and call ``logging.basicConfig()`` with a format string that includes them. Environment variables --------------------- .. envvar:: OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION Set this env var to ``false`` to skip installing the logging handler provided by this package. The default value is ``true``. .. envvar:: OTEL_PYTHON_LOG_CODE_ATTRIBUTES Set this env var to ``true`` to add ``code`` attributes (``code.file.path``, ``code.function.name``, ``code.line.number``) to OpenTelemetry logs, referencing the Python source location that emitted each log message. The default value is ``false``. .. envvar:: OTEL_PYTHON_LOG_CORRELATION This env var must be set to ``true`` in order to enable trace context injection into logs by calling ``logging.basicConfig()`` and setting a logging format that makes use of the injected tracing variables. Alternatively, ``set_logging_format`` argument can be set to ``True`` when initializing the ``LoggingInstrumentor`` class to achieve the same effect. .. code-block:: LoggingInstrumentor(set_logging_format=True) The default value is ``false``. .. envvar:: OTEL_PYTHON_LOG_FORMAT This env var can be used to instruct the instrumentation to use a custom logging format. Alternatively, a custom logging format can be passed to the ``LoggingInstrumentor`` as the ``logging_format`` argument. For example: .. code-block:: LoggingInstrumentor(logging_format='%(msg)s [span_id=%(span_id)s]') The default value is: .. code-block:: {DEFAULT_LOGGING_FORMAT} .. envvar:: OTEL_PYTHON_LOG_HANDLER_LEVEL Set this env var to filter which log records are exported by OpenTelemetry``LoggingHandler`` instrumentation. Accepts case-insensitive level names: ``notset``, ``debug``, ``info``, ``warning``, ``error``. Only records at or above this level will be exported. For example, setting this to warning means DEBUG and INFO logs are still handled by your normal logging setup, but they are not exported as OTel logs. Unrecognized values fall back to ``notset``. Alternatively, the level can be set via the ``log_handler_level`` argument: .. code-block:: LoggingInstrumentor(log_handler_level=logging.WARNING) The default value is ``notset``. .. envvar:: OTEL_PYTHON_LOG_LEVEL This env var can be used to set a custom logging level. Alternatively, log level can be passed to the ``LoggingInstrumentor`` during initialization. For example: .. code-block:: LoggingInstrumentor(log_level=logging.DEBUG) The default value is ``info``. Options are: - ``info`` - ``error`` - ``debug`` - ``warning`` Manually calling logging.basicConfig ------------------------------------ ``logging.basicConfig()`` can be called to set a global logging level and format. Only the first ever call has any effect on the global logger. Any subsequent calls have no effect and do not override a previously configured global logger. This integration calls ``logging.basicConfig()`` for you when ``OTEL_PYTHON_LOG_CORRELATION`` is set to ``true``. It uses the format and level specified by ``OTEL_PYTHON_LOG_FORMAT`` and ``OTEL_PYTHON_LOG_LEVEL`` environment variables respectively. If you code or some other library/framework you are using calls logging.basicConfig before this integration is enabled, then this integration's logging format will not be used and log statements will not contain tracing context. For this reason, you'll need to make sure this integration is enabled as early as possible in the service lifecycle or your framework is configured to use a logging format with placeholders for tracing context. This can be achieved by adding the following placeholders to your logging format: .. code-block:: %(otelSpanID)s %(otelTraceID)s %(otelServiceName)s %(otelTraceSampled)s API ----- .. code-block:: python from opentelemetry.instrumentation.logging import LoggingInstrumentor # inject trace context attributes only (manage your own format) LoggingInstrumentor().instrument(inject_trace_context=True) .. code-block:: python from opentelemetry.instrumentation.logging import LoggingInstrumentor # inject trace context attributes and set the logging format LoggingInstrumentor().instrument(set_logging_format=True) Note ----- If you set a logging format with trace context placeholders (e.g. ``%(otelSpanID)s``) but do not enable trace context injection via ``inject_trace_context=True`` or ``set_logging_format=True``, the placeholders will not be populated. Any log statements emitted before injection is enabled will result in ``KeyError`` exceptions, which the logging module silently swallows. Enable this integration as early as possible to avoid these issues. """ environment_variables.py000066400000000000000000000006751527070072300500440ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation/logging# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION = "OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION" OTEL_PYTHON_LOG_CODE_ATTRIBUTES = "OTEL_PYTHON_LOG_CODE_ATTRIBUTES" OTEL_PYTHON_LOG_CORRELATION = "OTEL_PYTHON_LOG_CORRELATION" OTEL_PYTHON_LOG_FORMAT = "OTEL_PYTHON_LOG_FORMAT" OTEL_PYTHON_LOG_HANDLER_LEVEL = "OTEL_PYTHON_LOG_HANDLER_LEVEL" OTEL_PYTHON_LOG_LEVEL = "OTEL_PYTHON_LOG_LEVEL" handler.py000066400000000000000000000211501527070072300450540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation/logging# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging import logging.config import threading import traceback from collections.abc import Callable from contextvars import ContextVar from time import time_ns from opentelemetry._logs import ( LoggerProvider, LogRecord, NoOpLogger, get_logger, get_logger_provider, ) from opentelemetry.context import get_current from opentelemetry.instrumentation.log_utils import std_to_otel from opentelemetry.semconv._incubating.attributes import ( event_attributes, ) from opentelemetry.semconv.attributes import ( code_attributes, exception_attributes, otel_attributes, ) from opentelemetry.util.types import AnyValue _internal_logger = logging.getLogger(__name__ + ".internal") _internal_logger.propagate = False _internal_logger.addHandler(logging.StreamHandler()) _OTEL_PYTHON_LOG_HANDLER_LEVEL_BY_NAME = { "notset": logging.NOTSET, "debug": logging.DEBUG, "info": logging.INFO, "warn": logging.WARNING, "warning": logging.WARNING, "error": logging.ERROR, } def _setup_logging_handler( logger_provider: LoggerProvider, log_code_attributes: bool = False, level: int | None = None, ) -> LoggingHandler: handler = LoggingHandler( level=level or logging.NOTSET, logger_provider=logger_provider, log_code_attributes=log_code_attributes, ) logging.getLogger().addHandler(handler) _overwrite_logging_config_fns(handler) return handler def _overwrite_logging_config_fns(handler: LoggingHandler) -> None: root = logging.getLogger() def wrapper(config_fn: Callable) -> Callable: def overwritten_config_fn(*args, **kwargs): removed_handler = False # We don't want the OTLP handler to be modified or deleted by the logging config functions. # So we remove it and then add it back after the function call. if handler in root.handlers: removed_handler = True root.handlers.remove(handler) try: config_fn(*args, **kwargs) finally: # Ensure handler is added back if logging function throws exception. if removed_handler: root.addHandler(handler) return overwritten_config_fn logging.config.fileConfig = wrapper(logging.config.fileConfig) logging.config.dictConfig = wrapper(logging.config.dictConfig) logging.basicConfig = wrapper(logging.basicConfig) # skip natural LogRecord attributes # http://docs.python.org/library/logging.html#logrecord-attributes _RESERVED_ATTRS = frozenset( ( "asctime", "args", "created", "exc_info", "exc_text", "filename", "funcName", "getMessage", "message", "levelname", "levelno", "lineno", "module", "msecs", "msg", "name", "pathname", "process", "processName", "relativeCreated", "stack_info", "thread", "threadName", "taskName", ) ) class LoggingHandler(logging.Handler): """A handler class which writes logging records, in OTLP format, to a network destination or file. Supports signals from the `logging` module. https://docs.python.org/3/library/logging.html """ _is_emitting: ContextVar[bool] = ContextVar("_is_emitting", default=False) def __init__( self, level: int = logging.NOTSET, logger_provider: LoggerProvider | None = None, log_code_attributes: bool = False, ) -> None: super().__init__(level=level) self._logger_provider = logger_provider or get_logger_provider() self._log_code_attributes = log_code_attributes def _get_attributes(self, record: logging.LogRecord) -> tuple[dict[str, AnyValue], str | None]: attributes = {k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS} # Promote otel.event.name (stable) or event.name (deprecated) to the # first-class LogRecord.event_name field instead of leaving it as a # plain attribute. otel.event.name takes precedence; event.name is a # deprecated fallback per the OTel semantic conventions. # Both keys are always popped so neither leaks into attributes. event_name: str | None = attributes.pop(otel_attributes.OTEL_EVENT_NAME, None) # TODO: Remove the deprecated branch path before marking logs stable deprecated_event_name: str | None = attributes.pop(event_attributes.EVENT_NAME, None) if event_name is None: event_name = deprecated_event_name if self._log_code_attributes: # Add standard code attributes for logs. attributes[code_attributes.CODE_FILE_PATH] = record.pathname attributes[code_attributes.CODE_FUNCTION_NAME] = record.funcName attributes[code_attributes.CODE_LINE_NUMBER] = record.lineno if record.exc_info: exctype, value, tb = record.exc_info if exctype is not None: attributes[exception_attributes.EXCEPTION_TYPE] = exctype.__name__ if value is not None and value.args: attributes[exception_attributes.EXCEPTION_MESSAGE] = str(value.args[0]) if tb is not None: # https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-spans/#stacktrace-representation attributes[exception_attributes.EXCEPTION_STACKTRACE] = "".join( traceback.format_exception(*record.exc_info) ) return attributes, event_name def _translate(self, record: logging.LogRecord) -> LogRecord: timestamp = int(record.created * 1e9) observered_timestamp = time_ns() severity_number = std_to_otel(record.levelno) if self.formatter: body = self.format(record) else: body = record.getMessage() attributes, event_name = self._get_attributes(record) # Map Python log level names to OTel severity text as defined in # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#displaying-severity _python_to_otel_severity_text = { "WARNING": "WARN", "CRITICAL": "FATAL", } level_name = _python_to_otel_severity_text.get(record.levelname, record.levelname) return LogRecord( timestamp=timestamp, observed_timestamp=observered_timestamp, context=get_current() or None, severity_text=level_name, severity_number=severity_number, body=body, attributes=attributes, event_name=event_name, ) def emit(self, record: logging.LogRecord) -> None: """ Emit a record. Skip emitting if logger is NoOp. The record is translated to OTel format, and then sent across the pipeline. """ # Prevent recursive logging that can cause infinite recursion or deadlock. # During _translate(), internal OTel code (e.g., _clean_extended_attribute) # may call _logger.warning() for invalid attributes. If the OTel # LoggingHandler is in the logger chain, this warning re-enters emit(), # creating an infinite loop that prevents the handler lock from ever # being released, blocking all other threads. # See: https://github.com/open-telemetry/opentelemetry-python/issues/3858 if self._is_emitting.get(): _internal_logger.warning("LoggingHandler.emit detected recursive logging, skipping to prevent deadlock.") return token = self._is_emitting.set(True) try: logger = get_logger(record.name, logger_provider=self._logger_provider) if not isinstance(logger, NoOpLogger): logger.emit(self._translate(record)) finally: self._is_emitting.reset(token) def flush(self) -> None: """ Flushes the logging output. Skip flushing if logging_provider has no force_flush method. """ if hasattr(self._logger_provider, "force_flush") and callable( self._logger_provider.force_flush # type: ignore[reportAttributeAccessIssue] ): # This is done in a separate thread to avoid a potential deadlock, for # details see https://github.com/open-telemetry/opentelemetry-python/pull/4636. thread = threading.Thread(target=self._logger_provider.force_flush) # type: ignore[reportAttributeAccessIssue] thread.start() package.py000066400000000000000000000001451527070072300450330ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation/logging# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = tuple() version.py000066400000000000000000000001741527070072300451270ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation/logging# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" _instruments = tuple() test-requirements.txt000066400000000000000000000004151527070072300367530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-loggingasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-logging opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/tests/000077500000000000000000000000001527070072300337335ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300357530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/teststest_handler.py000066400000000000000000001007731527070072300367120ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import os import unittest from unittest.mock import Mock, patch from opentelemetry._logs import NoOpLoggerProvider, SeverityNumber from opentelemetry._logs import get_logger as APIGetLogger from opentelemetry.attributes import BoundedAttributes from opentelemetry.instrumentation.logging import _get_log_level from opentelemetry.instrumentation.logging.handler import ( LoggingHandler, _setup_logging_handler, ) from opentelemetry.sdk import trace from opentelemetry.sdk._logs import ( LoggerProvider, LogRecordProcessor, ReadableLogRecord, ) from opentelemetry.sdk.environment_variables import OTEL_ATTRIBUTE_COUNT_LIMIT from opentelemetry.semconv.attributes import ( code_attributes, exception_attributes, ) from opentelemetry.trace import ( INVALID_SPAN_CONTEXT, set_span_in_context, ) class MutatingFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: record.my_special_attr = "my-special-attr-value" return super().format(record) # pylint: disable=too-many-public-methods class TestLoggingHandler(unittest.TestCase): def test_handler_default_log_level(self): processor, logger, handler = set_up_test_logging(logging.NOTSET) # Make sure debug messages are ignored by default logger.debug("Debug message") assert processor.emit_count() == 0 # Assert emit gets called for warning message with self.assertLogs(level=logging.WARNING): logger.warning("Warning message") self.assertEqual(processor.emit_count(), 1) logger.removeHandler(handler) def test_handler_custom_log_level(self): processor, logger, handler = set_up_test_logging(logging.ERROR) with self.assertLogs(level=logging.WARNING): logger.warning("Warning message test custom log level") # Make sure any log with level < ERROR is ignored assert processor.emit_count() == 0 with self.assertLogs(level=logging.ERROR): logger.error("Mumbai, we have a major problem") with self.assertLogs(level=logging.CRITICAL): logger.critical("No Time For Caution") self.assertEqual(processor.emit_count(), 2) logger.removeHandler(handler) # pylint: disable=protected-access def test_log_record_emit_noop(self): noop_logger_provider = NoOpLoggerProvider() logger_mock = APIGetLogger(__name__, logger_provider=noop_logger_provider) logger = logging.getLogger(__name__) handler_mock = Mock(spec=LoggingHandler) handler_mock._logger = logger_mock handler_mock.level = logging.WARNING logger.addHandler(handler_mock) with self.assertLogs(level=logging.WARNING): logger.warning("Warning message") logger.removeHandler(handler_mock) def test_log_flush_noop(self): no_op_logger_provider = NoOpLoggerProvider() logger = logging.getLogger("foo") handler = LoggingHandler(level=logging.NOTSET, logger_provider=no_op_logger_provider) logger.addHandler(handler) with self.assertLogs(level=logging.WARNING): logger.warning("Warning message") # the LoggingHandler flush method will call the force_flush method of LoggerProvider in # a separate thread if present. NoOpLoggerProvider is not supposed to have that with patch("opentelemetry.instrumentation.logging.handler.threading") as threading_mock: logger.handlers[0].flush() threading_mock.Thread.assert_not_called() logger.removeHandler(handler) def test_log_record_no_span_context(self): processor, logger, handler = set_up_test_logging(logging.WARNING) # Assert emit gets called for warning message with self.assertLogs(level=logging.WARNING): logger.warning("Warning message") record = processor.get_log_record(0) self.assertIsNotNone(record) self.assertEqual(record.log_record.trace_id, INVALID_SPAN_CONTEXT.trace_id) self.assertEqual(record.log_record.span_id, INVALID_SPAN_CONTEXT.span_id) self.assertEqual( record.log_record.trace_flags, INVALID_SPAN_CONTEXT.trace_flags, ) logger.removeHandler(handler) def test_log_record_observed_timestamp(self): processor, logger, handler = set_up_test_logging(logging.WARNING) with self.assertLogs(level=logging.WARNING): logger.warning("Warning message") record = processor.get_log_record(0) self.assertIsNotNone(record.log_record.observed_timestamp) logger.removeHandler(handler) def test_log_record_user_attributes(self): """Attributes can be injected into logs by adding them to the ReadWriteLogRecord""" processor, logger, handler = set_up_test_logging(logging.WARNING) # Assert emit gets called for warning message with self.assertLogs(level=logging.WARNING): logger.warning("Warning message", extra={"http.status_code": 200}) record = processor.get_log_record(0) self.assertIsNotNone(record) self.assertEqual(len(record.log_record.attributes), 1) self.assertEqual(record.log_record.attributes["http.status_code"], 200) self.assertTrue(isinstance(record.log_record.attributes, BoundedAttributes)) logger.removeHandler(handler) def test_log_record_with_code_attributes(self): processor, logger, handler = set_up_test_logging(logging.WARNING, log_code_attributes=True) # Assert emit gets called for warning message with self.assertLogs(level=logging.WARNING): logger.warning("Warning message", extra={"http.status_code": 200}) record = processor.get_log_record(0) self.assertIsNotNone(record) self.assertEqual(len(record.log_record.attributes), 4) self.assertEqual(record.log_record.attributes["http.status_code"], 200) self.assertTrue(record.log_record.attributes[code_attributes.CODE_FILE_PATH].endswith("test_handler.py")) self.assertEqual( record.log_record.attributes[code_attributes.CODE_FUNCTION_NAME], "test_log_record_with_code_attributes", ) # The line of the log statement is not a constant (changing tests may change that), # so only check that the attribute is present. self.assertTrue(code_attributes.CODE_LINE_NUMBER in record.log_record.attributes) self.assertTrue(isinstance(record.log_record.attributes, BoundedAttributes)) logger.removeHandler(handler) def test_log_record_exception(self): """Exception information will be included in attributes""" processor, logger, handler = set_up_test_logging(logging.ERROR) try: raise ZeroDivisionError("division by zero") except ZeroDivisionError: with self.assertLogs(level=logging.ERROR): logger.exception("Zero Division Error") record = processor.get_log_record(0) self.assertIsNotNone(record) self.assertTrue(isinstance(record.log_record.body, str)) self.assertEqual(record.log_record.body, "Zero Division Error") self.assertEqual( record.log_record.attributes[exception_attributes.EXCEPTION_TYPE], ZeroDivisionError.__name__, ) self.assertEqual( record.log_record.attributes[exception_attributes.EXCEPTION_MESSAGE], "division by zero", ) stack_trace = record.log_record.attributes[exception_attributes.EXCEPTION_STACKTRACE] self.assertIsInstance(stack_trace, str) self.assertTrue("Traceback" in stack_trace) self.assertTrue("ZeroDivisionError" in stack_trace) self.assertTrue("division by zero" in stack_trace) self.assertTrue(__file__ in stack_trace) logger.removeHandler(handler) def test_log_record_recursive_exception(self): """Exception information will be included in attributes even though it is recursive""" processor, logger, handler = set_up_test_logging(logging.ERROR) try: raise ZeroDivisionError(ZeroDivisionError(ZeroDivisionError("division by zero"))) except ZeroDivisionError: with self.assertLogs(level=logging.ERROR): logger.exception("Zero Division Error") record = processor.get_log_record(0) self.assertIsNotNone(record) self.assertEqual(record.log_record.body, "Zero Division Error") self.assertEqual( record.log_record.attributes[exception_attributes.EXCEPTION_TYPE], ZeroDivisionError.__name__, ) self.assertEqual( record.log_record.attributes[exception_attributes.EXCEPTION_MESSAGE], "division by zero", ) stack_trace = record.log_record.attributes[exception_attributes.EXCEPTION_STACKTRACE] self.assertIsInstance(stack_trace, str) self.assertTrue("Traceback" in stack_trace) self.assertTrue("ZeroDivisionError" in stack_trace) self.assertTrue("division by zero" in stack_trace) self.assertTrue(__file__ in stack_trace) logger.removeHandler(handler) def test_log_exc_info_false(self): """Exception information will not be included in attributes""" processor, logger, handler = set_up_test_logging(logging.NOTSET) try: raise ZeroDivisionError("division by zero") except ZeroDivisionError: with self.assertLogs(level=logging.ERROR): logger.error("Zero Division Error", exc_info=False) record = processor.get_log_record(0) self.assertIsNotNone(record) self.assertEqual(record.log_record.body, "Zero Division Error") self.assertNotIn( exception_attributes.EXCEPTION_TYPE, record.log_record.attributes, ) self.assertNotIn( exception_attributes.EXCEPTION_MESSAGE, record.log_record.attributes, ) self.assertNotIn( exception_attributes.EXCEPTION_STACKTRACE, record.log_record.attributes, ) logger.removeHandler(handler) def test_log_record_exception_with_object_payload(self): processor, logger, handler = set_up_test_logging(logging.ERROR) class CustomException(Exception): def __str__(self): return "CustomException stringified" try: raise CustomException("CustomException message") except CustomException as exception: with self.assertLogs(level=logging.ERROR): logger.exception(exception) record = processor.get_log_record(0) self.assertIsNotNone(record) self.assertTrue(isinstance(record.log_record.body, str)) self.assertEqual(record.log_record.body, "CustomException stringified") self.assertEqual( record.log_record.attributes[exception_attributes.EXCEPTION_TYPE], CustomException.__name__, ) self.assertEqual( record.log_record.attributes[exception_attributes.EXCEPTION_MESSAGE], "CustomException message", ) stack_trace = record.log_record.attributes[exception_attributes.EXCEPTION_STACKTRACE] self.assertIsInstance(stack_trace, str) self.assertTrue("Traceback" in stack_trace) self.assertTrue("CustomException" in stack_trace) self.assertTrue(__file__ in stack_trace) logger.removeHandler(handler) def test_log_record_trace_correlation(self): processor, logger, handler = set_up_test_logging(logging.WARNING) tracer = trace.TracerProvider().get_tracer(__name__) with tracer.start_as_current_span("test") as span: mock_context = set_span_in_context(span) with patch( "opentelemetry.sdk._logs._internal.get_current", return_value=mock_context, ): with self.assertLogs(level=logging.CRITICAL): logger.critical("Critical message within span") record = processor.get_log_record(0) self.assertEqual( record.log_record.body, "Critical message within span", ) self.assertEqual(record.log_record.severity_text, "FATAL") self.assertEqual( record.log_record.severity_number, SeverityNumber.FATAL, ) self.assertEqual(record.log_record.context, mock_context) span_context = span.get_span_context() self.assertEqual(record.log_record.trace_id, span_context.trace_id) self.assertEqual(record.log_record.span_id, span_context.span_id) self.assertEqual( record.log_record.trace_flags, span_context.trace_flags, ) logger.removeHandler(handler) def test_log_record_trace_correlation_deprecated(self): processor, logger, handler = set_up_test_logging(logging.WARNING) tracer = trace.TracerProvider().get_tracer(__name__) with tracer.start_as_current_span("test") as span: with self.assertLogs(level=logging.CRITICAL): logger.critical("Critical message within span") record = processor.get_log_record(0) self.assertEqual(record.log_record.body, "Critical message within span") self.assertEqual(record.log_record.severity_text, "FATAL") self.assertEqual(record.log_record.severity_number, SeverityNumber.FATAL) span_context = span.get_span_context() self.assertEqual(record.log_record.trace_id, span_context.trace_id) self.assertEqual(record.log_record.span_id, span_context.span_id) self.assertEqual(record.log_record.trace_flags, span_context.trace_flags) logger.removeHandler(handler) def test_warning_without_formatter(self): processor, logger, handler = set_up_test_logging(logging.WARNING) logger.warning("Test message") record = processor.get_log_record(0) self.assertEqual(record.log_record.body, "Test message") logger.removeHandler(handler) def test_exception_without_formatter(self): processor, logger, handler = set_up_test_logging(logging.WARNING) logger.exception("Test exception") record = processor.get_log_record(0) self.assertEqual(record.log_record.body, "Test exception") logger.removeHandler(handler) def test_warning_with_formatter(self): processor, logger, handler = set_up_test_logging( logging.WARNING, formatter=logging.Formatter("%(name)s - %(levelname)s - %(message)s"), ) logger.warning("Test message") record = processor.get_log_record(0) self.assertEqual(record.log_record.body, "foo - WARNING - Test message") logger.removeHandler(handler) def test_formatter_added_attributes_are_exported(self): processor, logger, handler = set_up_test_logging( logging.WARNING, formatter=MutatingFormatter("%(message)s"), ) logger.warning("Test message") record = processor.get_log_record(0) logger.removeHandler(handler) self.assertEqual( record.log_record.attributes.get("my_special_attr"), "my-special-attr-value", ) def test_log_body_is_always_string_with_formatter(self): processor, logger, handler = set_up_test_logging( logging.WARNING, formatter=logging.Formatter("%(name)s - %(levelname)s - %(message)s"), ) logger.warning(["something", "of", "note"]) record = processor.get_log_record(0) self.assertIsInstance(record.log_record.body, str) logger.removeHandler(handler) def test_simple_log_record_processor_custom_single_obj(self): """ Tests that logging a single non-string object uses getMessage """ processor, logger, handler = set_up_test_logging(logging.WARNING) # NOTE: the behaviour of `record.getMessage` is detailed in the # `logging.Logger.debug` documentation: # > The msg is the message format string, and the args are the arguments # > which are merged into msg using the string formatting operator. [...] # > No % formatting operation is performed on msg when no args are supplied. # This test uses the presence of '%s' in the first arg to determine if # formatting was applied # string msg with no args - getMessage bypasses formatting and sets the string directly logger.warning("a string with a percent-s: %s") # pylint: disable=logging-too-few-args # string msg with args - getMessage formats args into the msg logger.warning("a string with a percent-s: %s", "and arg") # non-string msg with args - getMessage stringifies msg and formats args into it logger.warning(["a non-string with a percent-s", "%s"], "and arg") # non-string msg with no args - getMessage stringifies the object and bypasses formatting logger.warning(["a non-string with a percent-s", "%s"]) logger.removeHandler(handler) assert processor.emit_count() == 4 expected = [ ("a string with a percent-s: %s"), ("a string with a percent-s: and arg"), ("['a non-string with a percent-s', 'and arg']"), ("['a non-string with a percent-s', '%s']"), ] for index, msg in enumerate(expected): record = processor.get_log_record(index) self.assertEqual(record.log_record.body, msg) @patch.dict(os.environ, {"OTEL_SDK_DISABLED": "true"}) def test_handler_root_logger_with_disabled_sdk_does_not_go_into_recursion_error( self, ): processor, logger, handler = set_up_test_logging(logging.NOTSET, root_logger=True) logger.warning("hello") self.assertEqual(processor.emit_count(), 0) logger.removeHandler(handler) @patch.dict(os.environ, {OTEL_ATTRIBUTE_COUNT_LIMIT: "3"}) def test_otel_attribute_count_limit_respected_in_logging_handler(self): """Test that OTEL_ATTRIBUTE_COUNT_LIMIT is properly respected by LoggingHandler.""" # Create a new LoggerProvider within the patched environment # This will create LogRecordLimits() that reads from the environment variable logger_provider = LoggerProvider() processor = FakeProcessor() logger_provider.add_log_record_processor(processor) logger = logging.getLogger("env_test") handler = LoggingHandler(level=logging.WARNING, logger_provider=logger_provider) logger.addHandler(handler) # Create a log record with many extra attributes extra_attrs = {f"custom_attr_{i}": f"value_{i}" for i in range(10)} with self.assertLogs(level=logging.WARNING): logger.warning("Test message with many attributes", extra=extra_attrs) record = processor.get_log_record(0) # With OTEL_ATTRIBUTE_COUNT_LIMIT=3, should have exactly 3 attributes total_attrs = len(record.log_record.attributes) self.assertEqual( total_attrs, 3, f"Should have exactly 3 attributes due to limit, got {total_attrs}", ) # Should have 7 dropped attributes (10 custom - 3 kept = 7 dropped) self.assertEqual( record.dropped_attributes, 7, f"Should have 7 dropped attributes, got {record.dropped_attributes}", ) logger.removeHandler(handler) @patch.dict(os.environ, {OTEL_ATTRIBUTE_COUNT_LIMIT: "5"}) def test_otel_attribute_count_limit_includes_code_attributes(self): """Test that OTEL_ATTRIBUTE_COUNT_LIMIT applies to all attributes including code attributes.""" # Create a new LoggerProvider within the patched environment # This will create LogRecordLimits() that reads from the environment variable logger_provider = LoggerProvider() processor = FakeProcessor() logger_provider.add_log_record_processor(processor) logger = logging.getLogger("env_test_2") handler = LoggingHandler(level=logging.WARNING, logger_provider=logger_provider) logger.addHandler(handler) # Create a log record with some extra attributes extra_attrs = {f"user_attr_{i}": f"value_{i}" for i in range(8)} with self.assertLogs(level=logging.WARNING): logger.warning("Test message", extra=extra_attrs) record = processor.get_log_record(0) # With OTEL_ATTRIBUTE_COUNT_LIMIT=5, should have exactly 5 attributes total_attrs = len(record.log_record.attributes) self.assertEqual( total_attrs, 5, f"Should have exactly 5 attributes due to limit, got {total_attrs}", ) # Should have 3 dropped attributes (8 user - 5 kept = 3 dropped) self.assertEqual( record.dropped_attributes, 3, f"Should have 3 dropped attributes, got {record.dropped_attributes}", ) logger.removeHandler(handler) def test_logging_handler_without_env_var_uses_default_limit(self): """Test that without OTEL_ATTRIBUTE_COUNT_LIMIT, default limit (128) should apply.""" processor, logger, _ = set_up_test_logging(logging.WARNING) # Create a log record with many attributes (more than default limit of 128) extra_attrs = {f"attr_{i}": f"value_{i}" for i in range(150)} with self.assertLogs(level=logging.WARNING): logger.warning("Test message with many attributes", extra=extra_attrs) record = processor.get_log_record(0) # Should be limited to default limit (128) total attributes total_attrs = len(record.log_record.attributes) self.assertEqual( total_attrs, 128, f"Should have exactly 128 attributes (default limit), got {total_attrs}", ) # Should have 22 dropped attributes (150 user - 128 kept = 22 dropped) self.assertEqual( record.dropped_attributes, 22, f"Should have 22 dropped attributes, got {record.dropped_attributes}", ) # --- event_name promotion tests (issue #4743) --- def test_otel_event_name_promoted_to_event_name_field(self): """otel.event.name in extra is promoted to LogRecord.event_name, not left as an attribute.""" processor, logger, handler = set_up_test_logging(logging.WARNING) with self.assertLogs(level=logging.WARNING): logger.warning( "something happened", extra={"otel.event.name": "my.event"}, ) record = processor.get_log_record(0) self.assertEqual(record.log_record.event_name, "my.event") self.assertNotIn("otel.event.name", record.log_record.attributes) logger.removeHandler(handler) def test_deprecated_event_name_promoted_as_fallback(self): """event.name (deprecated) is promoted to LogRecord.event_name when otel.event.name is absent.""" processor, logger, handler = set_up_test_logging(logging.WARNING) with self.assertLogs(level=logging.WARNING): logger.warning( "something happened", extra={"event.name": "legacy.event"}, ) record = processor.get_log_record(0) self.assertEqual(record.log_record.event_name, "legacy.event") self.assertNotIn("event.name", record.log_record.attributes) logger.removeHandler(handler) def test_otel_event_name_takes_precedence_over_deprecated_event_name(self): """otel.event.name wins over event.name when both are present.""" processor, logger, handler = set_up_test_logging(logging.WARNING) with self.assertLogs(level=logging.WARNING): logger.warning( "something happened", extra={ "otel.event.name": "stable.event", "event.name": "legacy.event", }, ) record = processor.get_log_record(0) self.assertEqual(record.log_record.event_name, "stable.event") self.assertNotIn("otel.event.name", record.log_record.attributes) self.assertNotIn("event.name", record.log_record.attributes) logger.removeHandler(handler) def test_event_name_is_none_when_not_provided(self): """event_name is None when neither otel.event.name nor event.name is passed.""" processor, logger, handler = set_up_test_logging(logging.WARNING) with self.assertLogs(level=logging.WARNING): logger.warning("plain log, no event name") record = processor.get_log_record(0) self.assertIsNone(record.log_record.event_name) logger.removeHandler(handler) def test_other_extra_attributes_unaffected_by_event_name_promotion(self): """Unrelated extra attributes still land in the attributes dict normally.""" processor, logger, handler = set_up_test_logging(logging.WARNING) with self.assertLogs(level=logging.WARNING): logger.warning( "something happened", extra={ "otel.event.name": "my.event", "http.status_code": 200, }, ) record = processor.get_log_record(0) self.assertEqual(record.log_record.event_name, "my.event") self.assertEqual(record.log_record.attributes["http.status_code"], 200) self.assertNotIn("otel.event.name", record.log_record.attributes) logger.removeHandler(handler) # pylint: disable=invalid-name class SetupLoggingHandlerTestCase(unittest.TestCase): def test_basicConfig_works_with_otel_handler(self): logger_provider = LoggerProvider() with ResetGlobalLoggingState(): _setup_logging_handler(logger_provider=logger_provider) logging.basicConfig(level=logging.INFO) root_logger = logging.getLogger() stream_handlers = [h for h in root_logger.handlers if isinstance(h, logging.StreamHandler)] self.assertEqual( len(stream_handlers), 1, "basicConfig should add a StreamHandler even when OTel handler exists", ) def test_basicConfig_preserves_otel_handler(self): logger_provider = LoggerProvider() with ResetGlobalLoggingState(): _setup_logging_handler(logger_provider=logger_provider) root_logger = logging.getLogger() self.assertEqual( len(root_logger.handlers), 1, "Should be exactly one OpenTelemetry LoggingHandler", ) handler = root_logger.handlers[0] self.assertIsInstance(handler, LoggingHandler) logging.basicConfig() self.assertGreater(len(root_logger.handlers), 1) logging_handlers = [h for h in root_logger.handlers if isinstance(h, LoggingHandler)] self.assertEqual( len(logging_handlers), 1, "Should still have exactly one OpenTelemetry LoggingHandler", ) root_logger.removeHandler(logging_handlers[0]) def test_dictConfig_preserves_otel_handler(self): logger_provider = LoggerProvider() with ResetGlobalLoggingState(): _setup_logging_handler(logger_provider=logger_provider) root_logger = logging.getLogger() self.assertEqual( len(root_logger.handlers), 1, "Should be exactly one OpenTelemetry LoggingHandler", ) logging.config.dictConfig( { "version": 1, "disable_existing_loggers": False, # If this is True all loggers are disabled. Many unit tests assert loggers emit logs. "handlers": { "console": { "class": "logging.StreamHandler", "level": "DEBUG", "stream": "ext://sys.stdout", }, }, "loggers": { "": { # root logger "handlers": ["console"], }, }, } ) self.assertEqual(len(root_logger.handlers), 2) logging_handlers = [h for h in root_logger.handlers if isinstance(h, LoggingHandler)] self.assertEqual( len(logging_handlers), 1, "Should still have exactly one OpenTelemetry LoggingHandler", ) root_logger.removeHandler(logging_handlers[0]) def test_setup_logging_handler_with_level(self): logger_provider = LoggerProvider() with ResetGlobalLoggingState(): handler = _setup_logging_handler(logger_provider=logger_provider, level=logging.ERROR) self.assertEqual(handler.level, logging.ERROR) logging.getLogger().removeHandler(handler) def test_setup_logging_handler_default_level_is_notset(self): logger_provider = LoggerProvider() with ResetGlobalLoggingState(): handler = _setup_logging_handler(logger_provider=logger_provider) self.assertEqual(handler.level, logging.NOTSET) logging.getLogger().removeHandler(handler) class GetLogLevelTestCase(unittest.TestCase): def test_get_log_level_none_returns_none(self): self.assertIsNone(_get_log_level(None)) def test_get_log_level_notset(self): self.assertEqual(_get_log_level("notset"), logging.NOTSET) def test_get_log_level_notset_with_whitespace(self): self.assertEqual(_get_log_level(" NOTSET "), logging.NOTSET) def test_get_log_level_debug(self): self.assertEqual(_get_log_level(" DeBug "), logging.DEBUG) def test_get_log_level_info(self): self.assertEqual(_get_log_level(" info "), logging.INFO) def test_get_log_level_warning(self): self.assertEqual(_get_log_level(" warnING "), logging.WARNING) def test_get_log_level_error(self): self.assertEqual(_get_log_level(" eRroR"), logging.ERROR) def test_get_log_level_invalid_falls_back_to_notset(self): with self.assertLogs("opentelemetry.instrumentation.logging", level="WARNING") as cm: self.assertEqual(_get_log_level("foobar"), logging.NOTSET) self.assertTrue(any("foobar" in line for line in cm.output)) def set_up_test_logging(level, formatter=None, root_logger=False, log_code_attributes=False): logger_provider = LoggerProvider() processor = FakeProcessor() logger_provider.add_log_record_processor(processor) logger = logging.getLogger(None if root_logger else "foo") handler = LoggingHandler( level=level, logger_provider=logger_provider, log_code_attributes=log_code_attributes, ) if formatter: handler.setFormatter(formatter) logger.addHandler(handler) return processor, logger, handler class FakeProcessor(LogRecordProcessor): def __init__(self): self.log_data_emitted = [] def on_emit(self, log_record: ReadableLogRecord): self.log_data_emitted.append(log_record) def shutdown(self): pass def force_flush(self, timeout_millis: int = 30000): pass def emit_count(self): return len(self.log_data_emitted) def get_log_record(self, i): return self.log_data_emitted[i] # Any test that calls _init_logging with setup_logging_handler=True # should call _init_logging within this context manager, to # ensure the global logging state is reset after the test. class ResetGlobalLoggingState: def __init__(self): self.original_basic_config = logging.basicConfig self.original_dict_config = logging.config.dictConfig self.original_file_config = logging.config.fileConfig self.root_logger = logging.getLogger() self.original_handlers = None def __enter__(self): self.original_handlers = self.root_logger.handlers[:] self.root_logger.handlers = [] return self def __exit__(self, exc_type, exc_val, exc_tb): self.root_logger.handlers = [] for handler in self.original_handlers: self.root_logger.addHandler(handler) logging.basicConfig = self.original_basic_config logging.config.dictConfig = self.original_dict_config logging.config.fileConfig = self.original_file_config test_logging.py000066400000000000000000000447071527070072300367270ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging from unittest import mock import pytest from opentelemetry._logs import get_logger_provider from opentelemetry.instrumentation.logging import ( DEFAULT_LOGGING_FORMAT, LoggingInstrumentor, ) from opentelemetry.instrumentation.logging.handler import LoggingHandler from opentelemetry.test.test_base import TestBase from opentelemetry.trace import NoOpTracerProvider, ProxyTracer, get_tracer class FakeTracerProvider: def get_tracer( # pylint: disable=no-self-use self, instrumenting_module_name: str, instrumenting_library_version: str | None = None, schema_url: str | None = None, ) -> ProxyTracer: return ProxyTracer( instrumenting_module_name, instrumenting_library_version, schema_url, ) # pylint: disable=no-self-use,too-many-public-methods class TestLoggingInstrumentorProxyTracerProvider(TestBase): @pytest.fixture(autouse=True) def inject_fixtures(self, caplog): self.caplog = caplog def setUp(self): super().setUp() LoggingInstrumentor().instrument(tracer_provider=FakeTracerProvider(), set_logging_format=True) def tearDown(self): super().tearDown() LoggingInstrumentor().uninstrument() def test_trace_context_injection(self): with self.caplog.at_level(level=logging.INFO): logger = logging.getLogger("test logger") logger.info("hello") self.assertEqual(len(self.caplog.records), 1) record = self.caplog.records[0] self.assertEqual(record.otelSpanID, "0") self.assertEqual(record.otelTraceID, "0") self.assertEqual(record.otelServiceName, "") self.assertEqual(record.otelTraceSampled, False) def log_hook(span, record): record.custom_user_attribute_from_log_hook = "some-value" class TestLoggingInstrumentor(TestBase): @pytest.fixture(autouse=True) def inject_fixtures(self, caplog): self.caplog = caplog def setUp(self): super().setUp() LoggingInstrumentor().instrument() self.tracer = get_tracer(__name__) def tearDown(self): super().tearDown() LoggingInstrumentor().uninstrument() def assert_trace_context_injected(self, span_id, trace_id, trace_sampled, resource_attributes): with self.caplog.at_level(level=logging.INFO): logger = logging.getLogger("test logger") logger.info("hello") self.assertEqual(len(self.caplog.records), 1) record = self.caplog.records[0] self.assertEqual(record.otelSpanID, span_id) self.assertEqual(record.otelTraceID, trace_id) self.assertEqual(record.otelTraceSampled, trace_sampled) self.assertEqual(record.otelServiceName, resource_attributes["service.name"]) @mock.patch.dict("os.environ", {"OTEL_PYTHON_LOG_CORRELATION": "true"}) @mock.patch("logging.basicConfig") def test_trace_context_injection_with_log_correlation_from_env_var(self, basic_config_mock): LoggingInstrumentor().uninstrument() LoggingInstrumentor().instrument() basic_config_mock.assert_called_once_with(format=DEFAULT_LOGGING_FORMAT, level=logging.INFO) with self.tracer.start_as_current_span("s1") as span: span_ctx = span.get_span_context() span_id = format(span_ctx.span_id, "016x") trace_id = format(span_ctx.trace_id, "032x") trace_sampled = span_ctx.trace_flags.sampled self.assert_trace_context_injected(span_id, trace_id, trace_sampled, span.resource.attributes) @mock.patch("logging.basicConfig") def test_trace_context_injection_with_log_correlation_instrument_arg(self, basic_config_mock): LoggingInstrumentor().uninstrument() LoggingInstrumentor().instrument(set_logging_format=True) basic_config_mock.assert_called_once_with(format=DEFAULT_LOGGING_FORMAT, level=logging.INFO) with self.tracer.start_as_current_span("s1") as span: span_ctx = span.get_span_context() span_id = format(span_ctx.span_id, "016x") trace_id = format(span_ctx.trace_id, "032x") trace_sampled = span_ctx.trace_flags.sampled self.assert_trace_context_injected(span_id, trace_id, trace_sampled, span.resource.attributes) def test_no_trace_context_injection_by_default(self): with self.tracer.start_as_current_span("s1"): with self.caplog.at_level(level=logging.INFO): logger = logging.getLogger("test logger") logger.info("hello") self.assertEqual(len(self.caplog.records), 1) record = self.caplog.records[0] self.assertFalse(hasattr(record, "otelServiceName")) self.assertFalse(hasattr(record, "otelSpanID")) self.assertFalse(hasattr(record, "otelTraceID")) self.assertFalse(hasattr(record, "otelTraceSampled")) @mock.patch("logging.basicConfig") def test_inject_trace_context_arg(self, basic_config_mock): LoggingInstrumentor().uninstrument() LoggingInstrumentor().instrument(inject_trace_context=True) basic_config_mock.assert_not_called() with self.tracer.start_as_current_span("s1") as span: span_ctx = span.get_span_context() span_id = format(span_ctx.span_id, "016x") trace_id = format(span_ctx.trace_id, "032x") trace_sampled = span_ctx.trace_flags.sampled self.assert_trace_context_injected(span_id, trace_id, trace_sampled, span.resource.attributes) @mock.patch("logging.basicConfig") def test_inject_trace_context_arg_without_span(self, basic_config_mock): LoggingInstrumentor().uninstrument() LoggingInstrumentor().instrument(inject_trace_context=True) basic_config_mock.assert_not_called() self.assert_trace_context_injected("0", "0", False, self.tracer.resource.attributes) @mock.patch("logging.basicConfig") def test_trace_context_injection_without_span(self, basic_config_mock): LoggingInstrumentor().uninstrument() LoggingInstrumentor().instrument(set_logging_format=True) basic_config_mock.assert_called_once_with(format=DEFAULT_LOGGING_FORMAT, level=logging.INFO) self.assert_trace_context_injected("0", "0", False, self.tracer.resource.attributes) @mock.patch("logging.basicConfig") def test_basic_config_called(self, basic_config_mock): LoggingInstrumentor().uninstrument() LoggingInstrumentor().instrument() self.assertFalse(basic_config_mock.called) LoggingInstrumentor().uninstrument() with mock.patch.dict("os.environ", {"OTEL_PYTHON_LOG_CORRELATION": "true"}): LoggingInstrumentor().instrument() basic_config_mock.assert_called_with(format=DEFAULT_LOGGING_FORMAT, level=logging.INFO) @mock.patch("logging.basicConfig") def test_custom_format_and_level_env(self, basic_config_mock): LoggingInstrumentor().uninstrument() LoggingInstrumentor().instrument() self.assertFalse(basic_config_mock.called) LoggingInstrumentor().uninstrument() with mock.patch.dict( "os.environ", { "OTEL_PYTHON_LOG_CORRELATION": "true", "OTEL_PYTHON_LOG_FORMAT": "%(message)s %(otelSpanID)s", "OTEL_PYTHON_LOG_LEVEL": "error", }, ): LoggingInstrumentor().instrument() basic_config_mock.assert_called_with(format="%(message)s %(otelSpanID)s", level=logging.ERROR) @mock.patch("logging.basicConfig") def test_custom_format_and_level_api(self, basic_config_mock): # pylint: disable=no-self-use LoggingInstrumentor().uninstrument() LoggingInstrumentor().instrument( set_logging_format=True, logging_format="%(message)s span_id=%(otelSpanID)s", log_level=logging.WARNING, ) basic_config_mock.assert_called_with(format="%(message)s span_id=%(otelSpanID)s", level=logging.WARNING) def test_log_hook(self): LoggingInstrumentor().uninstrument() LoggingInstrumentor().instrument( log_hook=log_hook, ) with self.tracer.start_as_current_span("s1"): with self.caplog.at_level(level=logging.INFO): logger = logging.getLogger("test logger") logger.info("hello") self.assertEqual(len(self.caplog.records), 1) record = self.caplog.records[0] self.assertFalse(hasattr(record, "otelServiceName")) self.assertFalse(hasattr(record, "otelSpanID")) self.assertFalse(hasattr(record, "otelTraceID")) self.assertFalse(hasattr(record, "otelTraceSampled")) self.assertEqual(record.custom_user_attribute_from_log_hook, "some-value") @mock.patch("logging.basicConfig") def test_log_hook_with_set_logging_format(self, basic_config_mock): LoggingInstrumentor().uninstrument() LoggingInstrumentor().instrument( set_logging_format=True, log_hook=log_hook, ) basic_config_mock.assert_called_once_with(format=DEFAULT_LOGGING_FORMAT, level=logging.INFO) with self.tracer.start_as_current_span("s1") as span: span_ctx = span.get_span_context() span_id = format(span_ctx.span_id, "016x") trace_id = format(span_ctx.trace_id, "032x") trace_sampled = span_ctx.trace_flags.sampled with self.caplog.at_level(level=logging.INFO): logger = logging.getLogger("test logger") logger.info("hello") self.assertEqual(len(self.caplog.records), 1) record = self.caplog.records[0] self.assertEqual(record.otelSpanID, span_id) self.assertEqual(record.otelTraceID, trace_id) self.assertEqual(record.otelServiceName, span.resource.attributes["service.name"]) self.assertEqual(record.otelTraceSampled, trace_sampled) self.assertEqual(record.custom_user_attribute_from_log_hook, "some-value") def test_uninstrumented(self): LoggingInstrumentor().uninstrument() with self.tracer.start_as_current_span("s1"): with self.caplog.at_level(level=logging.INFO): logger = logging.getLogger("test logger") logger.info("hello") self.assertEqual(len(self.caplog.records), 1) record = self.caplog.records[0] self.assertFalse(hasattr(record, "otelServiceName")) self.assertFalse(hasattr(record, "otelSpanID")) self.assertFalse(hasattr(record, "otelTraceID")) self.assertFalse(hasattr(record, "otelTraceSampled")) root_logger = logging.getLogger() logging_handler_instances = [handler for handler in root_logger.handlers if isinstance(handler, LoggingHandler)] self.assertEqual(logging_handler_instances, []) @mock.patch("logging.basicConfig") def test_no_op_tracer_provider(self, basic_config_mock): LoggingInstrumentor().uninstrument() LoggingInstrumentor().instrument(tracer_provider=NoOpTracerProvider(), set_logging_format=True) basic_config_mock.assert_called_once_with(format=DEFAULT_LOGGING_FORMAT, level=logging.INFO) with self.caplog.at_level(level=logging.INFO): logger = logging.getLogger("test logger") logger.info("hello") self.assertEqual(len(self.caplog.records), 1) record = self.caplog.records[0] self.assertEqual(record.otelSpanID, "0") self.assertEqual(record.otelTraceID, "0") self.assertEqual(record.otelServiceName, "") self.assertEqual(record.otelTraceSampled, False) @mock.patch.dict( "os.environ", {"OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED": "true"}, ) def test_handler_setup_is_disabled_if_sdk_autoinstrumentation_env_var_is_set_to_true( self, ): LoggingInstrumentor().uninstrument() with self.caplog.at_level(level=logging.WARNING): LoggingInstrumentor().instrument() self.assertEqual(len(self.caplog.records), 1) record = self.caplog.records[0] self.assertEqual( record.message, "Skipping installation of LoggingHandler from `opentelemetry-instrumentation-logging` " "to avoid duplicate logs. The SDK's deprecated LoggingHandler is already " "active (OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true). To migrate, unset " "this environment variable. The SDK's handler will be removed in a future release.", ) root_logger = logging.getLogger() logging_handler_instances = [handler for handler in root_logger.handlers if isinstance(handler, LoggingHandler)] self.assertEqual(logging_handler_instances, []) @mock.patch.dict( "os.environ", {"OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED": "false"}, ) def test_handler_setup_is_enabled_if_sdk_autoinstrumentation_env_var_is_set_to_false( self, ): LoggingInstrumentor().uninstrument() with self.caplog.at_level(level=logging.WARNING): LoggingInstrumentor().instrument() self.assertEqual(len(self.caplog.records), 0) root_logger = logging.getLogger() logging_handler_instances = [handler for handler in root_logger.handlers if isinstance(handler, LoggingHandler)] self.assertEqual(len(logging_handler_instances), 1) @mock.patch.dict( "os.environ", {"OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION": "false"}, ) def test_handler_setup_is_enabled_if_autoinstrumentation_env_var_is_set_to_false( self, ): LoggingInstrumentor().uninstrument() with self.caplog.at_level(level=logging.WARNING): LoggingInstrumentor().instrument() self.assertEqual(len(self.caplog.records), 0) root_logger = logging.getLogger() logging_handler_instances = [handler for handler in root_logger.handlers if isinstance(handler, LoggingHandler)] self.assertEqual(logging_handler_instances, []) def test_handler_setup_is_called_if_autoinstrumentation_env_vars_are_not_set( self, ): LoggingInstrumentor().uninstrument() with self.caplog.at_level(level=logging.WARNING): LoggingInstrumentor().instrument() self.assertEqual(len(self.caplog.records), 0) root_logger = logging.getLogger() logging_handler_instances = [handler for handler in root_logger.handlers if isinstance(handler, LoggingHandler)] self.assertEqual(len(logging_handler_instances), 1) def test_handler_setup_is_called_without_code_attributes_by_default(self): LoggingInstrumentor().uninstrument() with mock.patch("opentelemetry.instrumentation.logging._setup_logging_handler") as setup_mock: LoggingInstrumentor().instrument() logger_provider = get_logger_provider() setup_mock.assert_called_once_with( logger_provider=logger_provider, log_code_attributes=False, level=None, ) @mock.patch.dict("os.environ", {"OTEL_PYTHON_LOG_CODE_ATTRIBUTES": "true"}) def test_handler_setup_is_called_with_code_attributes_from_env_var(self): LoggingInstrumentor().uninstrument() with mock.patch("opentelemetry.instrumentation.logging._setup_logging_handler") as setup_mock: LoggingInstrumentor().instrument() logger_provider = get_logger_provider() setup_mock.assert_called_once_with( logger_provider=logger_provider, log_code_attributes=True, level=None, ) def test_handler_setup_is_controlled_by_instrumentor_parameter( self, ): LoggingInstrumentor().uninstrument() with self.caplog.at_level(level=logging.WARNING): LoggingInstrumentor().instrument(enable_log_auto_instrumentation=False) self.assertEqual(len(self.caplog.records), 0) root_logger = logging.getLogger() logging_handler_instances = [handler for handler in root_logger.handlers if isinstance(handler, LoggingHandler)] self.assertEqual(logging_handler_instances, []) def test_handler_code_attributes_is_controlled_by_instrumentor_parameter( self, ): LoggingInstrumentor().uninstrument() with mock.patch("opentelemetry.instrumentation.logging._setup_logging_handler") as setup_mock: LoggingInstrumentor().instrument(log_code_attributes=True) logger_provider = get_logger_provider() setup_mock.assert_called_once_with( logger_provider=logger_provider, log_code_attributes=True, level=None, ) @mock.patch.dict("os.environ", {"OTEL_PYTHON_LOG_HANDLER_LEVEL": "error"}) def test_handler_level_is_set_from_env_var(self): LoggingInstrumentor().uninstrument() with self.caplog.at_level(level=logging.WARNING): LoggingInstrumentor().instrument() root_logger = logging.getLogger() logging_handlers = [h for h in root_logger.handlers if isinstance(h, LoggingHandler)] self.assertEqual(len(logging_handlers), 1) self.assertEqual(logging_handlers[0].level, logging.ERROR) @mock.patch.dict( "os.environ", {"OTEL_PYTHON_LOG_HANDLER_LEVEL": "warning"}, ) def test_handler_setup_called_with_level_from_env_var(self): LoggingInstrumentor().uninstrument() with mock.patch("opentelemetry.instrumentation.logging._setup_logging_handler") as setup_mock: LoggingInstrumentor().instrument() logger_provider = get_logger_provider() setup_mock.assert_called_once_with( logger_provider=logger_provider, log_code_attributes=False, level=logging.WARNING, ) def test_handler_level_is_controlled_by_instrumentor_parameter(self): LoggingInstrumentor().uninstrument() with mock.patch("opentelemetry.instrumentation.logging._setup_logging_handler") as setup_mock: LoggingInstrumentor().instrument(log_handler_level=logging.DEBUG) logger_provider = get_logger_provider() setup_mock.assert_called_once_with( logger_provider=logger_provider, log_code_attributes=False, level=logging.DEBUG, ) test_logging_handler_recursion.py000066400000000000000000000055331527070072300425070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-logging/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import threading import unittest from opentelemetry.instrumentation.logging.handler import LoggingHandler from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import ( InMemoryLogExporter, SimpleLogRecordProcessor, ) class TestLoggingHandlerRecursionGuard(unittest.TestCase): @staticmethod def _create_handler(): exporter = InMemoryLogExporter() provider = LoggerProvider() provider.add_log_record_processor(SimpleLogRecordProcessor(exporter)) handler = LoggingHandler(logger_provider=provider) return handler, exporter @staticmethod def _make_record(msg="test"): return logging.LogRecord( name="test", level=logging.INFO, pathname="", lineno=0, msg=msg, args=(), exc_info=None, ) def test_recursive_emit_is_skipped(self): """Recursive emit() should be skipped to prevent deadlock. Regression test for https://github.com/open-telemetry/opentelemetry-python/issues/3858 """ handler, exporter = self._create_handler() token = handler._is_emitting.set(True) handler.emit(self._make_record("should be skipped")) self.assertEqual(len(exporter.get_finished_logs()), 0) handler._is_emitting.reset(token) def test_normal_emit_works(self): """Non-recursive emit() should process logs normally.""" handler, exporter = self._create_handler() handler.emit(self._make_record("should be captured")) logs = exporter.get_finished_logs() self.assertEqual(len(logs), 1) self.assertEqual(logs[0].log_record.body, "should be captured") def test_is_emitting_resets_after_emit(self): """Context must reset after emit(), allowing subsequent logs.""" handler, exporter = self._create_handler() handler.emit(self._make_record("first")) self.assertFalse(handler._is_emitting.get()) handler.emit(self._make_record("second")) self.assertEqual(len(exporter.get_finished_logs()), 2) def test_is_emitting_is_isolated_across_threads(self): """Emit context on one thread must not block other threads.""" handler, exporter = self._create_handler() token = handler._is_emitting.set(True) result = [False] def log_from_other_thread(): handler.emit(self._make_record("from other thread")) result[0] = True thread = threading.Thread(target=log_from_other_thread) thread.start() thread.join(timeout=5) self.assertTrue(result[0], "Other thread should not be blocked") self.assertEqual(len(exporter.get_finished_logs()), 1) handler._is_emitting.reset(token) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysql/000077500000000000000000000000001527070072300323105ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysql/LICENSE000066400000000000000000000261351527070072300333240ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysql/README.rst000066400000000000000000000014141527070072300337770ustar00rootroot00000000000000OpenTelemetry MySQL Instrumentation =================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-mysql.svg :target: https://pypi.org/project/opentelemetry-instrumentation-mysql/ Instrumentation with MySQL that supports the mysql-connector library and is specified to trace_integration using 'MySQL'. Installation ------------ :: pip install opentelemetry-instrumentation-mysql References ---------- * `OpenTelemetry MySQL Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031331527070072300351450ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysql[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-mysql" dynamic = ["version"] description = "OpenTelemetry MySQL instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-instrumentation-dbapi == 0.66b2", ] [project.optional-dependencies] instruments = [ "mysql-connector-python >= 8.0", ] [project.entry-points.opentelemetry_instrumentor] mysql = "opentelemetry.instrumentation.mysql:MySQLInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-mysql" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/mysql/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysql/src/000077500000000000000000000000001527070072300330775ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300357145ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysql/srcinstrumentation/000077500000000000000000000000001527070072300411575ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetrymysql/000077500000000000000000000000001527070072300423245ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation__init__.py000066400000000000000000000252671527070072300444510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ MySQL instrumentation supporting `mysql-connector`_, it can be enabled by using ``MySQLInstrumentor``. .. _mysql-connector: https://pypi.org/project/mysql-connector/ Usage ----- .. code:: python import mysql.connector from opentelemetry.instrumentation.mysql import MySQLInstrumentor # Call instrument() to wrap all database connections MySQLInstrumentor().instrument() cnx = mysql.connector.connect(database="MySQL_Database") cursor = cnx.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)") cursor.execute("INSERT INTO test (testField) VALUES (123)") cursor.close() cnx.close() .. code:: python import mysql.connector from opentelemetry.instrumentation.mysql import MySQLInstrumentor # Alternatively, use instrument_connection for an individual connection cnx = mysql.connector.connect(database="MySQL_Database") instrumented_cnx = MySQLInstrumentor().instrument_connection(cnx) cursor = instrumented_cnx.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)") cursor.execute("INSERT INTO test (testField) VALUES (123)") cursor.close() instrumented_cnx.close() Configuration ------------- SQLCommenter ************ You can optionally enable sqlcommenter which enriches the query with contextual information. Queries made after setting up trace integration with sqlcommenter enabled will have configurable key-value pairs appended to them, e.g. ``"select * from auth_users; /*traceparent=00-01234567-abcd-01*/"``. This supports context propagation between database client and server when database log records are enabled. For more information, see: * `Semantic Conventions - Database Spans `_ * `sqlcommenter `_ .. code:: python import mysql.connector from opentelemetry.instrumentation.mysql import MySQLInstrumentor MySQLInstrumentor().instrument(enable_commenter=True) cnx = mysql.connector.connect(database="MySQL_Database") cursor = cnx.cursor() cursor.execute("INSERT INTO test (testField) VALUES (123)") cursor.close() cnx.close() Warning: sqlcommenter for mysql-connector instrumentation should NOT be used if your application initializes cursors with ``prepared=True``, which will natively prepare and execute MySQL statements. Adding sqlcommenting will introduce a severe performance penalty by repeating ``Prepare`` of statements by mysql-connector that are made unique by traceparent in sqlcomment. The penalty does not happen if cursor ``prepared=False`` (default) and instrumentor ``enable_commenter=True``. SQLCommenter with commenter_options *********************************** The key-value pairs appended to the query can be configured using ``commenter_options``. When sqlcommenter is enabled, all available KVs/tags are calculated by default. ``commenter_options`` supports *opting out* of specific KVs. .. code:: python import mysql.connector from opentelemetry.instrumentation.mysql import MySQLInstrumentor # Opts into sqlcomment for mysql-connector trace integration. # Opts out of tags for mysql_client_version, db_driver. MySQLInstrumentor().instrument( enable_commenter=True, commenter_options={ "mysql_client_version": False, "db_driver": False, } ) Available commenter_options ########################### The following sqlcomment key-values can be opted out of through ``commenter_options``: +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | Commenter Option | Description | Example | +===========================+===========================================================+===========================================================================+ | ``db_driver`` | Database driver name with version (URL encoded). | ``mysql.connector%%%%3A2.2.9`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``dbapi_threadsafety`` | DB-API threadsafety value: 0-3 or unknown. | ``dbapi_threadsafety=2`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``dbapi_level`` | DB-API API level: 1.0, 2.0, or unknown. | ``dbapi_level='2.0'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``driver_paramstyle`` | DB-API paramstyle for SQL statement parameter. | ``driver_paramstyle='pyformat'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``mysql_client_version`` | MySQL client version. | ``mysql_client_version='123'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``opentelemetry_values`` | OpenTelemetry context as traceparent at time of query. | ``traceparent='00-03afa25236b8cd948fa853d67038ac79-405ff022e8247c46-01'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ SQLComment in span attribute **************************** If sqlcommenter is enabled, you can opt into the inclusion of sqlcomment in the query span ``db.statement`` and/or ``db.query.text`` attribute for your needs. If ``commenter_options`` have been set, the span attribute comment will also be configured by this setting. .. code:: python from opentelemetry.instrumentation.mysql import MySQLInstrumentor # Opts into sqlcomment for mysql-connector trace integration. # Opts into sqlcomment for `db.statement` and/or `db.query.text` span attribute. MySQLInstrumentor().instrument( enable_commenter=True, enable_attribute_commenter=True, ) Warning: Capture of sqlcomment in ``db.statement``/``db.query.text`` may have high cardinality without platform normalization. See `Semantic Conventions for database spans `_ for more information. API --- """ from collections.abc import Collection import mysql.connector from opentelemetry.instrumentation import dbapi from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.mysql.package import _instruments from opentelemetry.instrumentation.mysql.version import __version__ class MySQLInstrumentor(BaseInstrumentor): _CONNECTION_ATTRIBUTES = { "database": "database", "port": "server_port", "host": "server_host", "user": "user", } _DATABASE_SYSTEM = "mysql" def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): """Integrate with MySQL Connector/Python library. https://dev.mysql.com/doc/connector-python/en/ """ tracer_provider = kwargs.get("tracer_provider") enable_sqlcommenter = kwargs.get("enable_commenter", False) commenter_options = kwargs.get("commenter_options", {}) enable_attribute_commenter = kwargs.get("enable_attribute_commenter", False) dbapi.wrap_connect( __name__, mysql.connector, "connect", self._DATABASE_SYSTEM, self._CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, enable_commenter=enable_sqlcommenter, commenter_options=commenter_options, enable_attribute_commenter=enable_attribute_commenter, ) def _uninstrument(self, **kwargs): """ "Disable MySQL instrumentation""" dbapi.unwrap_connect(mysql.connector, "connect") # pylint:disable=no-self-use def instrument_connection( self, connection, tracer_provider=None, enable_commenter=None, commenter_options=None, enable_attribute_commenter=None, ): """Enable instrumentation in a MySQL connection. Args: connection: The existing MySQL connection instance to instrument. This connection is typically obtained through `mysql.connector.connect()` and is instrumented to collect telemetry data about database interactions. tracer_provider: An optional `TracerProvider` instance to use for tracing. If not provided, the globally configured tracer provider will be automatically used. enable_commenter: Optional flag to enable/disable sqlcommenter (default False). commenter_options: Optional configurations for tags to be appended at the sql query. enable_attribute_commenter: Optional flag to enable/disable addition of sqlcomment to span attribute (default False). Requires enable_commenter=True. Returns: An instrumented MySQL connection with OpenTelemetry tracing enabled. """ return dbapi.instrument_connection( __name__, connection, self._DATABASE_SYSTEM, self._CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, enable_commenter=enable_commenter, commenter_options=commenter_options, connect_module=mysql.connector, enable_attribute_commenter=enable_attribute_commenter, ) def uninstrument_connection(self, connection): """Disable instrumentation in a MySQL connection. Args: connection: The connection to uninstrument. Returns: An uninstrumented connection. """ return dbapi.uninstrument_connection(connection) package.py000066400000000000000000000004761527070072300443000ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # mysql-connector-python switched to a MySQL Server aligned versioning scheme # after 9.7.0 (the next release was 26.7.0), so no upper bound is declared here. _instruments = ("mysql-connector-python >= 8.0",) _semconv_status = "migration" version.py000066400000000000000000000001441527070072300443620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements-0.txt000066400000000000000000000005401527070072300366260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysqlasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 mysql-connector-python==8.3.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-dbapi -e instrumentation/opentelemetry-instrumentation-mysql test-requirements-1.txt000066400000000000000000000005371527070072300366350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysqlasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 mysql-connector-python==9.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.9.0 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-dbapi -e instrumentation/opentelemetry-instrumentation-mysql test-requirements-2.txt000066400000000000000000000005411527070072300366310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysqlasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 mysql-connector-python==26.7.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-dbapi -e instrumentation/opentelemetry-instrumentation-mysql opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysql/tests/000077500000000000000000000000001527070072300334525ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300354720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysql/teststest_mysql_integration.py000066400000000000000000000500621527070072300405570ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysql/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import contextlib from unittest import mock import mysql.connector import opentelemetry.instrumentation.mysql from opentelemetry import trace as trace_api from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.mysql import MySQLInstrumentor from opentelemetry.sdk import resources from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_STATEMENT, DB_SYSTEM, DB_USER, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_NAMESPACE, DB_QUERY_TEXT, DB_SYSTEM_NAME, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.test.test_base import TestBase @contextlib.contextmanager def use_semconv_opt_in(sem_conv_mode): env_patch = mock.patch.dict( "os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode}, ) _OpenTelemetrySemanticConventionStability._initialized = False env_patch.start() try: yield finally: env_patch.stop() _OpenTelemetrySemanticConventionStability._initialized = False def connect_and_execute_query(): cnx = mysql.connector.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) return cnx, query def make_mysql_connection_mock(): cnx = mock.MagicMock() cnx.database = "test" cnx.server_host = "localhost" cnx.server_port = 3306 cnx.user = "testuser" cursor = mock.MagicMock() cursor._cnx = cnx cnx.cursor.return_value = cursor return cnx def make_mysql_commenter_mocks(client_version="foobaz"): """Create mock objects for MySQL connector with SQL commenter support. Args: client_version: The MySQL client version string to return from get_client_info() Returns: Tuple of (mock_connect_module, mock_connection, mock_cursor) """ mock_connect_module = mock.MagicMock( __name__="mysql.connector", __version__="foobar", threadsafety="123", apilevel="123", paramstyle="test", ) mock_connection = mock.MagicMock() mock_connection.database = "test" mock_connection.server_host = "localhost" mock_connection.server_port = 3306 mock_cursor = mock_connect_module.connect().cursor() mock_cursor._cnx = mock_connection mock_cursor._cnx._cmysql.get_client_info.return_value = client_version mock_connection.cursor.return_value = mock_cursor mock_connect_module.connect.return_value = mock_connection return mock_connect_module, mock_connection, mock_cursor class TestMysqlIntegration(TestBase): def tearDown(self): super().tearDown() with self.disable_logging(): MySQLInstrumentor().uninstrument() @mock.patch("mysql.connector.connect") # pylint: disable=unused-argument def test_instrumentor(self, mock_connect): mock_connect.return_value = make_mysql_connection_mock() MySQLInstrumentor().instrument() connect_and_execute_query() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.mysql) # check that no spans are generated after uninstrumen MySQLInstrumentor().uninstrument() connect_and_execute_query() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) @mock.patch("mysql.connector.connect") # pylint: disable=unused-argument def test_custom_tracer_provider(self, mock_connect): mock_connect.return_value = make_mysql_connection_mock() resource = resources.Resource.create({}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result MySQLInstrumentor().instrument(tracer_provider=tracer_provider) connect_and_execute_query() span_list = exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertIs(span.resource, resource) @mock.patch("mysql.connector.connect") # pylint: disable=unused-argument def test_instrument_connection(self, mock_connect): mock_connect.return_value = make_mysql_connection_mock() cnx, query = connect_and_execute_query() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) cnx = MySQLInstrumentor().instrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) @mock.patch("mysql.connector.connect") def test_instrument_connection_no_op_tracer_provider(self, mock_connect): mock_connect.return_value = make_mysql_connection_mock() tracer_provider = trace_api.NoOpTracerProvider() MySQLInstrumentor().instrument(tracer_provider=tracer_provider) connect_and_execute_query() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) @mock.patch("opentelemetry.instrumentation.dbapi.instrument_connection") @mock.patch("mysql.connector") # pylint: disable=unused-argument def test_instrument_connection_enable_commenter_dbapi_kwargs( self, mock_connect, mock_instrument_connection, ): cnx = mysql.connector.connect(database="test") cnx = MySQLInstrumentor().instrument_connection( cnx, enable_commenter=True, commenter_options={"foo": True}, enable_attribute_commenter=True, ) cursor = cnx.cursor() cursor.execute("SELECT * FROM test") kwargs = mock_instrument_connection.call_args[1] self.assertEqual(kwargs["enable_commenter"], True) self.assertEqual(kwargs["commenter_options"], {"foo": True}) self.assertEqual(kwargs["enable_attribute_commenter"], True) def test_instrument_connection_with_dbapi_sqlcomment_enabled(self): mock_connect_module, mock_connection, mock_cursor = make_mysql_commenter_mocks() with mock.patch( "opentelemetry.instrumentation.mysql.mysql.connector", mock_connect_module, ): cnx_proxy = MySQLInstrumentor().instrument_connection( mock_connection, enable_commenter=True, ) cnx_proxy.cursor().execute("Select 1;") spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = format(span.get_span_context().trace_flags, "02x") self.assertEqual( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_instrument_connection_with_dbapi_sqlcomment_enabled_stmt_enabled( self, ): mock_connect_module, mock_connection, mock_cursor = make_mysql_commenter_mocks() with mock.patch( "opentelemetry.instrumentation.mysql.mysql.connector", mock_connect_module, ): cnx_proxy = MySQLInstrumentor().instrument_connection( mock_connection, enable_commenter=True, enable_attribute_commenter=True, ) cnx_proxy.cursor().execute("Select 1;") spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = format(span.get_span_context().trace_flags, "02x") self.assertEqual( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) self.assertEqual( span.attributes[DB_STATEMENT], f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) def test_instrument_connection_with_dbapi_sqlcomment_enabled_with_options( self, ): mock_connect_module, mock_connection, mock_cursor = make_mysql_commenter_mocks() with mock.patch( "opentelemetry.instrumentation.mysql.mysql.connector", mock_connect_module, ): cnx_proxy = MySQLInstrumentor().instrument_connection( mock_connection, enable_commenter=True, commenter_options={ "dbapi_level": False, "dbapi_threadsafety": True, "driver_paramstyle": False, }, ) cnx_proxy.cursor().execute("Select 1;") spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = format(span.get_span_context().trace_flags, "02x") self.assertEqual( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_threadsafety='123',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_instrument_connection_with_dbapi_sqlcomment_not_enabled_default( self, ): mock_connect_module, mock_connection, mock_cursor = make_mysql_commenter_mocks() with mock.patch( "opentelemetry.instrumentation.mysql.mysql.connector", mock_connect_module, ): cnx_proxy = MySQLInstrumentor().instrument_connection( mock_connection, ) cnx_proxy.cursor().execute("Select 1;") self.assertEqual( mock_cursor.execute.call_args[0][0], "Select 1;", ) spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) @mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect") @mock.patch("mysql.connector") # pylint: disable=unused-argument def test_instrument_enable_commenter_dbapi_kwargs( self, mock_connect, mock_wrap_connect, ): MySQLInstrumentor()._instrument( enable_commenter=True, commenter_options={"foo": True}, enable_attribute_commenter=True, ) kwargs = mock_wrap_connect.call_args[1] self.assertEqual(kwargs["enable_commenter"], True) self.assertEqual(kwargs["commenter_options"], {"foo": True}) self.assertEqual(kwargs["enable_attribute_commenter"], True) def test_instrument_with_dbapi_sqlcomment_enabled( self, ): mock_connect_module, _, mock_cursor = make_mysql_commenter_mocks() with mock.patch( "opentelemetry.instrumentation.mysql.mysql.connector", mock_connect_module, ): MySQLInstrumentor()._instrument( enable_commenter=True, ) cnx = mock_connect_module.connect(database="test") cursor = cnx.cursor() cursor.execute("Select 1;") spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = format(span.get_span_context().trace_flags, "02x") self.assertEqual( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_instrument_with_dbapi_sqlcomment_enabled_stmt_enabled( self, ): mock_connect_module, _, mock_cursor = make_mysql_commenter_mocks() with mock.patch( "opentelemetry.instrumentation.mysql.mysql.connector", mock_connect_module, ): MySQLInstrumentor()._instrument( enable_commenter=True, enable_attribute_commenter=True, ) cnx = mock_connect_module.connect(database="test") cursor = cnx.cursor() cursor.execute("Select 1;") spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = format(span.get_span_context().trace_flags, "02x") self.assertEqual( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) self.assertEqual( span.attributes[DB_STATEMENT], f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) def test_instrument_with_dbapi_sqlcomment_enabled_with_options( self, ): mock_connect_module, _, mock_cursor = make_mysql_commenter_mocks() with mock.patch( "opentelemetry.instrumentation.mysql.mysql.connector", mock_connect_module, ): MySQLInstrumentor()._instrument( enable_commenter=True, commenter_options={ "dbapi_level": False, "dbapi_threadsafety": True, "driver_paramstyle": False, }, ) cnx = mock_connect_module.connect(database="test") cursor = cnx.cursor() cursor.execute("Select 1;") spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = format(span.get_span_context().trace_flags, "02x") self.assertEqual( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_threadsafety='123',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_instrument_with_dbapi_sqlcomment_not_enabled_default( self, ): mock_connect_module, _, mock_cursor = make_mysql_commenter_mocks() with mock.patch( "opentelemetry.instrumentation.mysql.mysql.connector", mock_connect_module, ): MySQLInstrumentor()._instrument() cnx = mock_connect_module.connect(database="test") cursor = cnx.cursor() cursor.execute("Select 1;") self.assertEqual( mock_cursor.execute.call_args[0][0], "Select 1;", ) spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) @mock.patch("mysql.connector.connect") # pylint: disable=unused-argument def test_uninstrument_connection(self, mock_connect): mock_connect.return_value = make_mysql_connection_mock() MySQLInstrumentor().instrument() cnx, query = connect_and_execute_query() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) cnx = MySQLInstrumentor().uninstrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) @mock.patch("mysql.connector.connect") def test_semconv_stable(self, mock_connect): """database,http opt-in emits only stable attributes.""" with use_semconv_opt_in("database,http"): mock_connect.return_value = make_mysql_connection_mock() MySQLInstrumentor().instrument() connect_and_execute_query() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_SYSTEM_NAME], "mysql") self.assertEqual(span.attributes[DB_NAMESPACE], "test") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT * FROM test") self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost") self.assertEqual(span.attributes[SERVER_PORT], 3306) self.assertNotIn(DB_SYSTEM, span.attributes) self.assertNotIn(DB_NAME, span.attributes) self.assertNotIn(DB_STATEMENT, span.attributes) self.assertNotIn(DB_USER, span.attributes) self.assertNotIn(NET_PEER_NAME, span.attributes) self.assertNotIn(NET_PEER_PORT, span.attributes) @mock.patch("mysql.connector.connect") def test_semconv_dup(self, mock_connect): """database/dup,http/dup opt-in emits both legacy and stable attributes.""" with use_semconv_opt_in("database/dup,http/dup"): mock_connect.return_value = make_mysql_connection_mock() MySQLInstrumentor().instrument() connect_and_execute_query() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_SYSTEM], "mysql") self.assertEqual(span.attributes[DB_SYSTEM_NAME], "mysql") self.assertEqual(span.attributes[DB_NAME], "test") self.assertEqual(span.attributes[DB_NAMESPACE], "test") self.assertEqual(span.attributes[DB_STATEMENT], "SELECT * FROM test") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT * FROM test") self.assertEqual(span.attributes[DB_USER], "testuser") self.assertEqual(span.attributes[NET_PEER_NAME], "localhost") self.assertEqual(span.attributes[NET_PEER_PORT], 3306) self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost") self.assertEqual(span.attributes[SERVER_PORT], 3306) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysqlclient/000077500000000000000000000000001527070072300335075ustar00rootroot00000000000000LICENSE000066400000000000000000000261351527070072300344440ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysqlclient Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000012771527070072300351260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysqlclientOpenTelemetry mysqlclient Instrumentation ========================================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-mysqlclient.svg :target: https://pypi.org/project/opentelemetry-instrumentation-mysqlclient/ Installation ------------ :: pip install opentelemetry-instrumentation-mysqlclient References ---------- * `OpenTelemetry mysqlclient Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031671527070072300363530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysqlclient[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-mysqlclient" dynamic = ["version"] description = "OpenTelemetry mysqlclient instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-instrumentation-dbapi == 0.66b2", ] [project.optional-dependencies] instruments = [ "mysqlclient < 3", ] [project.entry-points.opentelemetry_instrumentor] mysqlclient = "opentelemetry.instrumentation.mysqlclient:MySQLClientInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-mysqlclient" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/mysqlclient/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysqlclient/src/000077500000000000000000000000001527070072300342765ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300371135ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysqlclient/srcinstrumentation/000077500000000000000000000000001527070072300423565ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetrymysqlclient/000077500000000000000000000000001527070072300447225ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry/instrumentation__init__.py000066400000000000000000000242751527070072300470450ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry/instrumentation/mysqlclient# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The integration with MySQLClient supports the `MySQLClient`_ library and can be enabled by using ``MySQLClientInstrumentor``. .. _MySQLClient: https://pypi.org/project/MySQLClient/ Usage ----- .. code:: python import MySQLdb from opentelemetry.instrumentation.mysqlclient import MySQLClientInstrumentor MySQLClientInstrumentor().instrument() cnx = MySQLdb.connect(database="MySQL_Database") cursor = cnx.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)") cursor.execute("INSERT INTO test (testField) VALUES (123)") cnx.commit() cursor.close() cnx.close() Configuration ------------- SQLCommenter ************ You can optionally enable sqlcommenter which enriches the query with contextual information. Queries made after setting up trace integration with sqlcommenter enabled will have configurable key-value pairs appended to them, e.g. ``"select * from auth_users; /*traceparent=00-01234567-abcd-01*/"``. This supports context propagation between database client and server when database log records are enabled. For more information, see: * `Semantic Conventions - Database Spans `_ * `sqlcommenter `_ .. code:: python import MySQLdb from opentelemetry.instrumentation.mysqlclient import MySQLClientInstrumentor MySQLClientInstrumentor().instrument(enable_commenter=True) cnx = MySQLdb.connect(database="MySQL_Database") cursor = cnx.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)") cursor.execute("INSERT INTO test (testField) VALUES (123)") cnx.commit() cursor.close() cnx.close() SQLCommenter with commenter_options *********************************** The key-value pairs appended to the query can be configured using ``commenter_options``. When sqlcommenter is enabled, all available KVs/tags are calculated by default. ``commenter_options`` supports *opting out* of specific KVs. .. code:: python import MySQLdb from opentelemetry.instrumentation.mysqlclient import MySQLClientInstrumentor # Opts into sqlcomment for MySQLClient trace integration. # Opts out of tags for mysql_client_version, db_driver. MySQLClientInstrumentor().instrument( enable_commenter=True, commenter_options={ "mysql_client_version": False, "db_driver": False, } ) Available commenter_options ########################### The following sqlcomment key-values can be opted out of through ``commenter_options``: +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | Commenter Option | Description | Example | +===========================+===========================================================+===========================================================================+ | ``db_driver`` | Database driver name with version (URL encoded). | ``MySQLdb%%%%3A1.2.3`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``dbapi_threadsafety`` | DB-API threadsafety value: 0-3 or unknown. | ``dbapi_threadsafety=2`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``dbapi_level`` | DB-API API level: 1.0, 2.0, or unknown. | ``dbapi_level='2.0'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``driver_paramstyle`` | DB-API paramstyle for SQL statement parameter. | ``driver_paramstyle='pyformat'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``mysql_client_version`` | MySQL client version. | ``mysql_client_version='123'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``opentelemetry_values`` | OpenTelemetry context as traceparent at time of query. | ``traceparent='00-03afa25236b8cd948fa853d67038ac79-405ff022e8247c46-01'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ SQLComment in span attribute **************************** If sqlcommenter is enabled, you can opt into the inclusion of sqlcomment in the query span ``db.statement`` attribute for your needs. If ``commenter_options`` have been set, the span attribute comment will also be configured by this setting. .. code:: python from opentelemetry.instrumentation.mysqlclient import MySQLClientInstrumentor # Opts into sqlcomment for mysqlclient trace integration. # Opts into sqlcomment for `db.statement` and/or `db.query.text` span attribute. MySQLClientInstrumentor().instrument( enable_commenter=True, enable_attribute_commenter=True, ) Warning: Capture of sqlcomment in ``db.statement``/``db.query.text`` may have high cardinality without platform normalization. See `Semantic Conventions for database spans `_ for more information. API --- """ from collections.abc import Collection import MySQLdb from opentelemetry.instrumentation import dbapi from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.mysqlclient.package import _instruments from opentelemetry.instrumentation.mysqlclient.version import __version__ _CONNECTION_ATTRIBUTES = { "database": "db", "port": "port", "host": "host", "user": "user", } _DATABASE_SYSTEM = "mysql" class MySQLClientInstrumentor(BaseInstrumentor): def instrumentation_dependencies(self) -> Collection[str]: # pylint: disable=no-self-use return _instruments def _instrument(self, **kwargs): # pylint: disable=no-self-use """Integrate with the mysqlclient library. https://github.com/PyMySQL/mysqlclient/ """ tracer_provider = kwargs.get("tracer_provider") enable_sqlcommenter = kwargs.get("enable_commenter", False) commenter_options = kwargs.get("commenter_options", {}) enable_attribute_commenter = kwargs.get("enable_attribute_commenter", False) dbapi.wrap_connect( __name__, MySQLdb, "connect", _DATABASE_SYSTEM, _CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, enable_commenter=enable_sqlcommenter, commenter_options=commenter_options, enable_attribute_commenter=enable_attribute_commenter, ) def _uninstrument(self, **kwargs): # pylint: disable=no-self-use """ "Disable mysqlclient instrumentation""" dbapi.unwrap_connect(MySQLdb, "connect") @staticmethod def instrument_connection( connection, tracer_provider=None, enable_commenter=None, commenter_options=None, enable_attribute_commenter=None, ): """Enable instrumentation in a mysqlclient connection. Args: connection: The MySQL connection instance to instrument. This connection is typically created using `MySQLdb.connect()` and needs to be wrapped to collect telemetry. tracer_provider: A custom `TracerProvider` instance to be used for tracing. If not specified, the globally configured tracer provider will be used. enable_commenter: A flag to enable the OpenTelemetry SQLCommenter feature. If set to `True`, SQL queries will be enriched with contextual information (e.g., database client details). Default is `None`. commenter_options: A dictionary of configuration options for SQLCommenter. All options are enabled (True) by default. This allows you to customize metadata appended to queries. Possible options include: - `db_driver`: Adds the database driver name and version. - `dbapi_threadsafety`: Adds threadsafety information. - `dbapi_level`: Adds the DB-API version. - `mysql_client_version`: Adds the MySQL client version. - `driver_paramstyle`: Adds the parameter style. - `opentelemetry_values`: Includes traceparent values. Returns: An instrumented MySQL connection with OpenTelemetry support enabled. """ return dbapi.instrument_connection( __name__, connection, _DATABASE_SYSTEM, _CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, enable_commenter=enable_commenter, commenter_options=commenter_options, connect_module=MySQLdb, enable_attribute_commenter=enable_attribute_commenter, ) @staticmethod def uninstrument_connection(connection): """Disable instrumentation in a mysqlclient connection. Args: connection: The connection to uninstrument. Returns: An uninstrumented connection. """ return dbapi.uninstrument_connection(connection) package.py000066400000000000000000000002211527070072300466620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry/instrumentation/mysqlclient# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("mysqlclient < 3",) _semconv_status = "migration" version.py000066400000000000000000000001441527070072300467600ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry/instrumentation/mysqlclient# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000005331527070072300376720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysqlclientasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 mysqlclient==2.2.4 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-dbapi -e instrumentation/opentelemetry-instrumentation-mysqlclient opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysqlclient/tests/000077500000000000000000000000001527070072300346515ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300366710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysqlclient/teststest_mysqlclient_integration.py000066400000000000000000000554651527070072300431710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-mysqlclient/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import contextlib from unittest import mock import MySQLdb import opentelemetry.instrumentation.mysqlclient from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.mysqlclient import MySQLClientInstrumentor from opentelemetry.sdk import resources from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_STATEMENT, DB_SYSTEM, DB_USER, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_NAMESPACE, DB_QUERY_TEXT, DB_SYSTEM_NAME, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.test.test_base import TestBase @contextlib.contextmanager def use_semconv_opt_in(sem_conv_mode): env_patch = mock.patch.dict( "os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode}, ) _OpenTelemetrySemanticConventionStability._initialized = False env_patch.start() try: yield finally: env_patch.stop() _OpenTelemetrySemanticConventionStability._initialized = False def connect_and_execute_query(): cnx = MySQLdb.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) return cnx, query def make_mysqlclient_connection_mock(): cnx = mock.MagicMock() cnx.db = "test" cnx.host = "localhost" cnx.port = 3306 cnx.user = "testuser" return cnx class TestMySQLClientIntegration(TestBase): # pylint: disable=invalid-name def tearDown(self): super().tearDown() with self.disable_logging(): MySQLClientInstrumentor().uninstrument() @mock.patch("MySQLdb.connect") # pylint: disable=unused-argument def test_instrumentor(self, mock_connect): MySQLClientInstrumentor().instrument() cnx = MySQLdb.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.mysqlclient) # check that no spans are generated after uninstrument MySQLClientInstrumentor().uninstrument() cnx = MySQLdb.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) @mock.patch("MySQLdb.connect") # pylint: disable=unused-argument def test_custom_tracer_provider(self, mock_connect): resource = resources.Resource.create({}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result MySQLClientInstrumentor().instrument(tracer_provider=tracer_provider) cnx = MySQLdb.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertIs(span.resource, resource) @mock.patch("MySQLdb.connect") # pylint: disable=unused-argument def test_instrument_connection(self, mock_connect): cnx = MySQLdb.connect(database="test") query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) cnx = MySQLClientInstrumentor().instrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) @mock.patch("opentelemetry.instrumentation.dbapi.instrument_connection") @mock.patch("MySQLdb.connect") # pylint: disable=unused-argument def test_instrument_connection_enable_commenter_dbapi_kwargs( self, mock_connect, mock_instrument_connection, ): cnx = MySQLdb.connect(database="test") cnx = MySQLClientInstrumentor().instrument_connection( cnx, enable_commenter=True, commenter_options={"foo": True}, enable_attribute_commenter=True, ) cursor = cnx.cursor() cursor.execute("Select 1;") kwargs = mock_instrument_connection.call_args[1] self.assertEqual(kwargs["enable_commenter"], True) self.assertEqual(kwargs["commenter_options"], {"foo": True}) self.assertEqual(kwargs["enable_attribute_commenter"], True) def test_instrument_connection_with_dbapi_sqlcomment_enabled(self): mock_connect_module = mock.MagicMock( __name__="MySQLdb", threadsafety="123", apilevel="123", paramstyle="test", ) mock_connect_module._mysql.get_client_info.return_value = "foobaz" mock_cursor = mock_connect_module.connect().cursor() mock_connection = mock.MagicMock() mock_connection.cursor.return_value = mock_cursor with ( mock.patch( "opentelemetry.instrumentation.mysqlclient.MySQLdb", mock_connect_module, ), mock.patch( "opentelemetry.instrumentation.dbapi.util_version", return_value="foobar", ), ): cnx_proxy = MySQLClientInstrumentor().instrument_connection( mock_connection, enable_commenter=True, ) cnx_proxy.cursor().execute("Select 1;") spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = format(span.get_span_context().trace_flags, "02x") self.assertEqual( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='MySQLdb%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_instrument_connection_with_dbapi_sqlcomment_enabled_stmt_enabled( self, ): mock_connect_module = mock.MagicMock( __name__="MySQLdb", threadsafety="123", apilevel="123", paramstyle="test", ) mock_connect_module._mysql.get_client_info.return_value = "foobaz" mock_cursor = mock_connect_module.connect().cursor() mock_connection = mock.MagicMock() mock_connection.cursor.return_value = mock_cursor with ( mock.patch( "opentelemetry.instrumentation.mysqlclient.MySQLdb", mock_connect_module, ), mock.patch( "opentelemetry.instrumentation.dbapi.util_version", return_value="foobar", ), ): cnx_proxy = MySQLClientInstrumentor().instrument_connection( mock_connection, enable_commenter=True, enable_attribute_commenter=True, ) cnx_proxy.cursor().execute("Select 1;") spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = format(span.get_span_context().trace_flags, "02x") self.assertEqual( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='MySQLdb%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) self.assertEqual( span.attributes[DB_STATEMENT], f"Select 1 /*db_driver='MySQLdb%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) def test_instrument_connection_with_dbapi_sqlcomment_enabled_with_options( self, ): mock_connect_module = mock.MagicMock( __name__="MySQLdb", threadsafety="123", apilevel="123", paramstyle="test", ) mock_connect_module._mysql.get_client_info.return_value = "foobaz" mock_cursor = mock_connect_module.connect().cursor() mock_connection = mock.MagicMock() mock_connection.cursor.return_value = mock_cursor with ( mock.patch( "opentelemetry.instrumentation.mysqlclient.MySQLdb", mock_connect_module, ), mock.patch( "opentelemetry.instrumentation.dbapi.util_version", return_value="foobar", ), ): cnx_proxy = MySQLClientInstrumentor().instrument_connection( mock_connection, enable_commenter=True, commenter_options={ "dbapi_level": False, "dbapi_threadsafety": True, "driver_paramstyle": False, }, ) cnx_proxy.cursor().execute("Select 1;") spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = format(span.get_span_context().trace_flags, "02x") self.assertEqual( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='MySQLdb%%3Afoobar',dbapi_threadsafety='123',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_instrument_connection_with_dbapi_sqlcomment_not_enabled_default( self, ): mock_connect_module = mock.MagicMock( __name__="MySQLdb", threadsafety="123", apilevel="123", paramstyle="test", ) mock_connect_module._mysql.get_client_info.return_value = "foobaz" mock_cursor = mock_connect_module.connect().cursor() mock_connection = mock.MagicMock() mock_connection.cursor.return_value = mock_cursor with ( mock.patch( "opentelemetry.instrumentation.mysqlclient.MySQLdb", mock_connect_module, ), mock.patch( "opentelemetry.instrumentation.dbapi.util_version", return_value="foobar", ), ): cnx_proxy = MySQLClientInstrumentor().instrument_connection( mock_connection, ) cnx_proxy.cursor().execute("Select 1;") self.assertEqual( mock_cursor.execute.call_args[0][0], "Select 1;", ) spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) @mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect") @mock.patch("MySQLdb.connect") # pylint: disable=unused-argument def test_instrument_enable_commenter_dbapi_kwargs( self, mock_connect, mock_wrap_connect, ): MySQLClientInstrumentor()._instrument( enable_commenter=True, commenter_options={"foo": True}, enable_attribute_commenter=True, ) kwargs = mock_wrap_connect.call_args[1] self.assertEqual(kwargs["enable_commenter"], True) self.assertEqual(kwargs["commenter_options"], {"foo": True}) self.assertEqual(kwargs["enable_attribute_commenter"], True) def test_instrument_with_dbapi_sqlcomment_enabled( self, ): mock_connect_module = mock.MagicMock( __name__="MySQLdb", threadsafety="123", apilevel="123", paramstyle="test", ) mock_connect_module._mysql.get_client_info.return_value = "foobaz" mock_cursor = mock_connect_module.connect().cursor() mock_connection = mock.MagicMock() mock_connection.cursor.return_value = mock_cursor with ( mock.patch( "opentelemetry.instrumentation.mysqlclient.MySQLdb", mock_connect_module, ), mock.patch( "opentelemetry.instrumentation.dbapi.util_version", return_value="foobar", ), ): MySQLClientInstrumentor()._instrument( enable_commenter=True, ) cnx = mock_connect_module.connect(database="test") cursor = cnx.cursor() cursor.execute("Select 1;") spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = format(span.get_span_context().trace_flags, "02x") self.assertEqual( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='MySQLdb%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_instrument_with_dbapi_sqlcomment_enabled_stmt_enabled( self, ): mock_connect_module = mock.MagicMock( __name__="MySQLdb", threadsafety="123", apilevel="123", paramstyle="test", ) mock_connect_module._mysql.get_client_info.return_value = "foobaz" mock_cursor = mock_connect_module.connect().cursor() mock_connection = mock.MagicMock() mock_connection.cursor.return_value = mock_cursor with ( mock.patch( "opentelemetry.instrumentation.mysqlclient.MySQLdb", mock_connect_module, ), mock.patch( "opentelemetry.instrumentation.dbapi.util_version", return_value="foobar", ), ): MySQLClientInstrumentor()._instrument( enable_commenter=True, enable_attribute_commenter=True, ) cnx = mock_connect_module.connect(database="test") cursor = cnx.cursor() cursor.execute("Select 1;") spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = format(span.get_span_context().trace_flags, "02x") self.assertEqual( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='MySQLdb%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) self.assertEqual( span.attributes[DB_STATEMENT], f"Select 1 /*db_driver='MySQLdb%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) def test_instrument_with_dbapi_sqlcomment_enabled_with_options( self, ): mock_connect_module = mock.MagicMock( __name__="MySQLdb", threadsafety="123", apilevel="123", paramstyle="test", ) mock_connect_module._mysql.get_client_info.return_value = "foobaz" mock_cursor = mock_connect_module.connect().cursor() mock_connection = mock.MagicMock() mock_connection.cursor.return_value = mock_cursor with ( mock.patch( "opentelemetry.instrumentation.mysqlclient.MySQLdb", mock_connect_module, ), mock.patch( "opentelemetry.instrumentation.dbapi.util_version", return_value="foobar", ), ): MySQLClientInstrumentor()._instrument( enable_commenter=True, commenter_options={ "dbapi_level": False, "dbapi_threadsafety": True, "driver_paramstyle": False, }, ) cnx = mock_connect_module.connect(database="test") cursor = cnx.cursor() cursor.execute("Select 1;") spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = format(span.get_span_context().trace_flags, "02x") self.assertEqual( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='MySQLdb%%3Afoobar',dbapi_threadsafety='123',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_instrument_with_dbapi_sqlcomment_not_enabled_default( self, ): mock_connect_module = mock.MagicMock( __name__="MySQLdb", threadsafety="123", apilevel="123", paramstyle="test", ) mock_connect_module._mysql.get_client_info.return_value = "foobaz" mock_cursor = mock_connect_module.connect().cursor() mock_connection = mock.MagicMock() mock_connection.cursor.return_value = mock_cursor with ( mock.patch( "opentelemetry.instrumentation.mysqlclient.MySQLdb", mock_connect_module, ), mock.patch( "opentelemetry.instrumentation.dbapi.util_version", return_value="foobar", ), ): MySQLClientInstrumentor()._instrument() cnx = mock_connect_module.connect(database="test") cursor = cnx.cursor() cursor.execute("Select 1;") self.assertEqual( mock_cursor.execute.call_args[0][0], "Select 1;", ) spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) @mock.patch("MySQLdb.connect") # pylint: disable=unused-argument def test_uninstrument_connection(self, mock_connect): MySQLClientInstrumentor().instrument() cnx = MySQLdb.connect(database="test") query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) self.memory_exporter.clear() cnx = MySQLClientInstrumentor().uninstrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) @mock.patch("MySQLdb.connect") def test_semconv_stable(self, mock_connect): """database,http opt-in emits only stable attributes.""" with use_semconv_opt_in("database,http"): mock_connect.return_value = make_mysqlclient_connection_mock() MySQLClientInstrumentor().instrument() connect_and_execute_query() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_SYSTEM_NAME], "mysql") self.assertEqual(span.attributes[DB_NAMESPACE], "test") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT * FROM test") self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost") self.assertEqual(span.attributes[SERVER_PORT], 3306) self.assertNotIn(DB_SYSTEM, span.attributes) self.assertNotIn(DB_NAME, span.attributes) self.assertNotIn(DB_STATEMENT, span.attributes) self.assertNotIn(DB_USER, span.attributes) self.assertNotIn(NET_PEER_NAME, span.attributes) self.assertNotIn(NET_PEER_PORT, span.attributes) @mock.patch("MySQLdb.connect") def test_semconv_dup(self, mock_connect): """database/dup,http/dup opt-in emits both legacy and stable attributes.""" with use_semconv_opt_in("database/dup,http/dup"): mock_connect.return_value = make_mysqlclient_connection_mock() MySQLClientInstrumentor().instrument() connect_and_execute_query() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_SYSTEM], "mysql") self.assertEqual(span.attributes[DB_SYSTEM_NAME], "mysql") self.assertEqual(span.attributes[DB_NAME], "test") self.assertEqual(span.attributes[DB_NAMESPACE], "test") self.assertEqual(span.attributes[DB_STATEMENT], "SELECT * FROM test") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT * FROM test") self.assertEqual(span.attributes[DB_USER], "testuser") self.assertEqual(span.attributes[NET_PEER_NAME], "localhost") self.assertEqual(span.attributes[NET_PEER_PORT], 3306) self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost") self.assertEqual(span.attributes[SERVER_PORT], 3306) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika/000077500000000000000000000000001527070072300320675ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika/LICENSE000066400000000000000000000261351527070072300331030ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika/README.rst000066400000000000000000000011141527070072300335530ustar00rootroot00000000000000OpenTelemetry pika Instrumentation ================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-pika.svg :target: https://pypi.org/project/opentelemetry-instrumentation-pika/ This library allows tracing requests made by the pika library. Installation ------------ :: pip install opentelemetry-instrumentation-pika References ---------- * `OpenTelemetry pika/ Tracing `_ * `OpenTelemetry Project `_ pyproject.toml000066400000000000000000000031671527070072300347330ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-pika" dynamic = ["version"] description = "OpenTelemetry pika instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-api ~= 1.5", "packaging >= 20.0", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [ "pika >= 0.12.0", ] [project.entry-points.opentelemetry_instrumentor] pika = "opentelemetry.instrumentation.pika:PikaInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-pika" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/pika/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika/src/000077500000000000000000000000001527070072300326565ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300354735ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika/srcinstrumentation/000077500000000000000000000000001527070072300407365ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetrypika/000077500000000000000000000000001527070072300416625ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation__init__.py000066400000000000000000000073701527070072300440020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Instrument pika to trace RabbitMQ applications. Usage ----- * Start broker backend .. code-block:: python docker run -p 5672:5672 rabbitmq * Run instrumented task .. code-block:: python import pika from opentelemetry.instrumentation.pika import PikaInstrumentor PikaInstrumentor().instrument() connection = pika.BlockingConnection(pika.URLParameters("amqp://localhost")) channel = connection.channel() channel.queue_declare(queue="hello") channel.basic_publish(exchange="", routing_key="hello", body=b"Hello World!") * PikaInstrumentor also supports instrumentation of a single channel .. code-block:: python import pika from opentelemetry.instrumentation.pika import PikaInstrumentor connection = pika.BlockingConnection(pika.URLParameters("amqp://localhost")) channel = connection.channel() channel.queue_declare(queue="hello") pika_instrumentation = PikaInstrumentor() pika_instrumentation.instrument_channel(channel=channel) channel.basic_publish(exchange="", routing_key="hello", body=b"Hello World!") pika_instrumentation.uninstrument_channel(channel=channel) * PikaInstrumentor also supports instrumentation without creating an object, and receiving a tracer_provider .. code-block:: python import pika from opentelemetry.instrumentation.pika import PikaInstrumentor from opentelemetry.trace import get_tracer_provider connection = pika.BlockingConnection(pika.URLParameters("amqp://localhost")) channel = connection.channel() tracer_provider = get_tracer_provider() channel.queue_declare(queue="hello") PikaInstrumentor.instrument_channel(channel, tracer_provider=tracer_provider) channel.basic_publish(exchange="", routing_key="hello", body=b"Hello World!") PikaInstrumentor.uninstrument_channel(channel) * PikaInstrumentor also supports instrumenting with hooks that will be called when producing or consuming a message. The hooks should be of type "Callable[[Span, bytes, BasicProperties], None]" where the first parameter is the span, the second parameter is the message body and the third parameter is the message properties .. code-block:: python import pika from opentelemetry.instrumentation.pika import PikaInstrumentor from opentelemetry.trace import Span from pika import BasicProperties def publish_hook(span: Span, body: bytes, properties: BasicProperties): span.set_attribute("messaging.payload", body.decode()) def consume_hook(span: Span, body: bytes, properties: BasicProperties): span.set_attribute("messaging.id", properties.message_id) connection = pika.BlockingConnection(pika.URLParameters("amqp://localhost")) channel = connection.channel() channel.queue_declare(queue="hello") PikaInstrumentor.instrument_channel(channel, publish_hook=publish_hook, consume_hook=consume_hook) channel.basic_publish(exchange="", routing_key="hello", body=b"Hello World!") PikaInstrumentor.uninstrument_channel(channel) Consumer Instrumentation ------------------------ For consumer instrumentation, pika supports two consuming modes: * Consumers using the `basic_consume` method which accepts a callback. This is supported for global instrumentation (`PikaInstrumentor().instrument()`) as well channel specific instrumentation (`PikaInstrumentor().instrument_channel(channel)`) * Consumers using the `consume` method which returns a generator over messages. This is supported for global instrumentations only (`PikaInstrumentor().instrument()`) API --- """ # pylint: disable=import-error from .pika_instrumentor import PikaInstrumentor from .version import __version__ __all__ = ["PikaInstrumentor", "__version__"] package.py000066400000000000000000000002501527070072300436240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Collection _instruments: Collection[str] = ("pika >= 0.12.0",) pika_instrumentor.py000066400000000000000000000205501527070072300460130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=unnecessary-dunder-call from collections.abc import Collection from logging import getLogger from typing import Any import pika import wrapt from packaging import version from pika.adapters import BlockingConnection from pika.adapters.blocking_connection import ( BlockingChannel, _QueueConsumerGeneratorInfo, ) from pika.channel import Channel from pika.connection import Connection from opentelemetry import trace from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.pika import utils from opentelemetry.instrumentation.pika.package import _instruments from opentelemetry.instrumentation.pika.version import __version__ from opentelemetry.instrumentation.utils import unwrap from opentelemetry.trace import Tracer, TracerProvider _LOG = getLogger(__name__) _CTX_KEY = "__otel_task_span" _FUNCTIONS_TO_UNINSTRUMENT = ["basic_publish"] def _consumer_callback_attribute_name() -> str: pika_version = version.parse(pika.__version__) return "on_message_callback" if pika_version >= version.parse("1.0.0") else "consumer_cb" class PikaInstrumentor(BaseInstrumentor): # type: ignore CONSUMER_CALLBACK_ATTR = _consumer_callback_attribute_name() @staticmethod def _instrument_channel_consumers( channel: BlockingChannel | Channel, tracer: Tracer, consume_hook: utils.HookT = utils.dummy_callback, ) -> Any: consumer_infos = None if isinstance(channel, BlockingChannel): consumer_infos = channel._consumer_infos elif isinstance(channel, Channel): consumer_infos = channel._consumers for consumer_tag, consumer_info in consumer_infos.items(): callback_attr = PikaInstrumentor.CONSUMER_CALLBACK_ATTR consumer_callback = getattr(consumer_info, callback_attr, None) if consumer_callback is None: continue if hasattr(consumer_callback, "_original_callback"): continue decorated_callback = utils._decorate_callback( consumer_callback, tracer, consumer_tag, consume_hook, ) decorated_callback._original_callback = consumer_callback setattr(consumer_info, callback_attr, decorated_callback) @staticmethod def _instrument_basic_publish( channel: BlockingChannel | Channel, tracer: Tracer, publish_hook: utils.HookT = utils.dummy_callback, ) -> None: original_function = channel.basic_publish decorated_function = utils._decorate_basic_publish(original_function, channel, tracer, publish_hook) decorated_function._original_function = original_function channel.__setattr__("basic_publish", decorated_function) channel.basic_publish = decorated_function @staticmethod def _instrument_channel_functions( channel: BlockingChannel | Channel, tracer: Tracer, publish_hook: utils.HookT = utils.dummy_callback, ) -> None: if hasattr(channel, "basic_publish"): PikaInstrumentor._instrument_basic_publish(channel, tracer, publish_hook) @staticmethod def _uninstrument_channel_functions( channel: BlockingChannel | Channel, ) -> None: for function_name in _FUNCTIONS_TO_UNINSTRUMENT: if not hasattr(channel, function_name): continue function = getattr(channel, function_name) if hasattr(function, "_original_function"): channel.__setattr__(function_name, function._original_function) unwrap(channel, "basic_consume") @staticmethod # Make sure that the spans are created inside hash them set as parent and not as brothers def instrument_channel( channel: BlockingChannel | Channel, tracer_provider: TracerProvider | None = None, publish_hook: utils.HookT = utils.dummy_callback, consume_hook: utils.HookT = utils.dummy_callback, ) -> None: if not hasattr(channel, "_is_instrumented_by_opentelemetry"): channel._is_instrumented_by_opentelemetry = False if channel._is_instrumented_by_opentelemetry: _LOG.warning("Attempting to instrument Pika channel while already instrumented!") return tracer = trace.get_tracer( __name__, __version__, tracer_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) PikaInstrumentor._instrument_channel_consumers(channel, tracer, consume_hook) PikaInstrumentor._decorate_basic_consume(channel, tracer, consume_hook) PikaInstrumentor._instrument_channel_functions(channel, tracer, publish_hook) @staticmethod def uninstrument_channel(channel: BlockingChannel) -> None: if not hasattr(channel, "_is_instrumented_by_opentelemetry") or not channel._is_instrumented_by_opentelemetry: _LOG.error("Attempting to uninstrument Pika channel while already uninstrumented!") return for consumers_tag, client_info in channel._consumer_infos.items(): callback_attr = PikaInstrumentor.CONSUMER_CALLBACK_ATTR consumer_callback = getattr(client_info, callback_attr, None) if hasattr(consumer_callback, "_original_callback"): channel._consumer_infos[consumers_tag] = consumer_callback._original_callback PikaInstrumentor._uninstrument_channel_functions(channel) def _decorate_channel_function( self, tracer_provider: TracerProvider | None, publish_hook: utils.HookT = utils.dummy_callback, consume_hook: utils.HookT = utils.dummy_callback, ) -> None: def wrapper(wrapped, instance, args, kwargs): channel = wrapped(*args, **kwargs) self.instrument_channel( channel, tracer_provider=tracer_provider, publish_hook=publish_hook, consume_hook=consume_hook, ) return channel wrapt.wrap_function_wrapper(BlockingConnection, "channel", wrapper) wrapt.wrap_function_wrapper(Connection, "channel", wrapper) @staticmethod def _decorate_basic_consume( channel: BlockingChannel | Channel, tracer: Tracer | None, consume_hook: utils.HookT = utils.dummy_callback, ) -> None: def wrapper(wrapped, instance, args, kwargs): return_value = wrapped(*args, **kwargs) PikaInstrumentor._instrument_channel_consumers(channel, tracer, consume_hook) return return_value wrapt.wrap_function_wrapper(channel, "basic_consume", wrapper) @staticmethod def _decorate_queue_consumer_generator( tracer_provider: TracerProvider | None, consume_hook: utils.HookT = utils.dummy_callback, ) -> None: tracer = trace.get_tracer(__name__, __version__, tracer_provider) def wrapper(wrapped, instance, args, kwargs): res = wrapped(*args, **kwargs) instance.pending_events = utils.ReadyMessagesDequeProxy( instance.pending_events, instance, tracer, consume_hook ) return res wrapt.wrap_function_wrapper(_QueueConsumerGeneratorInfo, "__init__", wrapper) def _instrument(self, **kwargs: dict[str, Any]) -> None: tracer_provider: TracerProvider = kwargs.get("tracer_provider", None) publish_hook: utils.HookT = kwargs.get("publish_hook", utils.dummy_callback) consume_hook: utils.HookT = kwargs.get("consume_hook", utils.dummy_callback) self.__setattr__("__opentelemetry_tracer_provider", tracer_provider) self._decorate_channel_function( tracer_provider, publish_hook=publish_hook, consume_hook=consume_hook, ) self._decorate_queue_consumer_generator(tracer_provider, consume_hook=consume_hook) def _uninstrument(self, **kwargs: dict[str, Any]) -> None: if hasattr(self, "__opentelemetry_tracer_provider"): delattr(self, "__opentelemetry_tracer_provider") unwrap(BlockingConnection, "channel") unwrap(Connection, "channel") unwrap(_QueueConsumerGeneratorInfo, "__init__") def instrumentation_dependencies(self) -> Collection[str]: return _instruments utils.py000066400000000000000000000220261527070072300433760ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable from logging import getLogger from typing import Any from pika.adapters.blocking_connection import ( _ConsumerDeliveryEvt, _QueueConsumerGeneratorInfo, ) from pika.channel import Channel from pika.spec import Basic, BasicProperties from wrapt import ObjectProxy from opentelemetry import context, propagate, trace from opentelemetry.instrumentation.utils import is_instrumentation_enabled from opentelemetry.propagators.textmap import CarrierT, Getter from opentelemetry.semconv._incubating.attributes import messaging_attributes from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv.trace import ( MessagingOperationValues, SpanAttributes, ) from opentelemetry.trace import SpanKind, Tracer from opentelemetry.trace.span import Span _LOG = getLogger(__name__) class _PikaGetter(Getter[CarrierT]): # type: ignore def get(self, carrier: CarrierT, key: str) -> list[str] | None: value = carrier.get(key, None) if value is None: return None return [value] def keys(self, carrier: CarrierT) -> list[str]: return [] _pika_getter = _PikaGetter() HookT = Callable[[Span, bytes, BasicProperties], None] def dummy_callback(span: Span, body: bytes, properties: BasicProperties): ... def _decorate_callback( callback: Callable[[Channel, Basic.Deliver, BasicProperties, bytes], Any], tracer: Tracer, task_name: str, consume_hook: HookT = dummy_callback, ): def decorated_callback( channel: Channel, method: Basic.Deliver, properties: BasicProperties, body: bytes, ) -> Any: if not properties: properties = BasicProperties(headers={}) if properties.headers is None: properties.headers = {} ctx = propagate.extract(properties.headers, getter=_pika_getter) if not ctx: ctx = context.get_current() token = context.attach(ctx) span = _get_span( tracer, channel, properties, destination=(method.exchange if method.exchange else method.routing_key), span_kind=SpanKind.CONSUMER, task_name=task_name, operation=MessagingOperationValues.RECEIVE, ) try: with trace.use_span(span, end_on_exit=True): try: consume_hook(span, body, properties) except Exception as hook_exception: # pylint: disable=W0703 _LOG.exception(hook_exception) retval = callback(channel, method, properties, body) finally: if token: context.detach(token) return retval return decorated_callback def _decorate_basic_publish( original_function: Callable[[str, str, bytes, BasicProperties, bool], Any], channel: Channel, tracer: Tracer, publish_hook: HookT = dummy_callback, ): def decorated_function( exchange: str, routing_key: str, body: bytes, properties: BasicProperties = None, mandatory: bool = False, ) -> Any: if not properties: properties = BasicProperties(headers={}) if properties.headers is None: properties.headers = {} span = _get_span( tracer, channel, properties, destination=exchange if exchange else routing_key, span_kind=SpanKind.PRODUCER, task_name="(temporary)", operation=None, ) if not span: return original_function(exchange, routing_key, body, properties, mandatory) with trace.use_span(span, end_on_exit=True): propagate.inject(properties.headers) try: publish_hook(span, body, properties) except Exception as hook_exception: # pylint: disable=W0703 _LOG.exception(hook_exception) retval = original_function(exchange, routing_key, body, properties, mandatory) return retval return decorated_function def _get_span( tracer: Tracer, channel: Channel | None, properties: BasicProperties, task_name: str, destination: str, span_kind: SpanKind, operation: MessagingOperationValues | None = None, ) -> Span | None: if not is_instrumentation_enabled(): return None task_name = properties.type if properties.type else task_name span = tracer.start_span( name=_generate_span_name(destination, operation), kind=span_kind, ) if span.is_recording(): _enrich_span(span, channel, properties, destination, operation) return span def _generate_span_name(task_name: str, operation: MessagingOperationValues | None) -> str: if not operation: return f"{task_name} send" return f"{task_name} {operation.value}" def _enrich_span( span: Span, channel: Channel | None, properties: BasicProperties, task_destination: str, operation: MessagingOperationValues | None = None, ) -> None: span.set_attribute(messaging_attributes.MESSAGING_SYSTEM, "rabbitmq") if operation: span.set_attribute(SpanAttributes.MESSAGING_OPERATION, operation.value) else: span.set_attribute(SpanAttributes.MESSAGING_TEMP_DESTINATION, True) span.set_attribute(SpanAttributes.MESSAGING_DESTINATION, task_destination) if properties.message_id: span.set_attribute( messaging_attributes.MESSAGING_MESSAGE_ID, properties.message_id, ) if properties.correlation_id: span.set_attribute(SpanAttributes.MESSAGING_CONVERSATION_ID, properties.correlation_id) if not channel: return if not hasattr(channel.connection, "params"): span.set_attribute(NET_PEER_NAME, channel.connection._impl.params.host) span.set_attribute(NET_PEER_PORT, channel.connection._impl.params.port) else: span.set_attribute(NET_PEER_NAME, channel.connection.params.host) span.set_attribute(NET_PEER_PORT, channel.connection.params.port) # pylint:disable=abstract-method class ReadyMessagesDequeProxy(ObjectProxy): def __init__( self, wrapped, queue_consumer_generator: _QueueConsumerGeneratorInfo, tracer: Tracer | None, consume_hook: HookT = dummy_callback, ): super().__init__(wrapped) self._self_active_token = None self._self_tracer = tracer self._self_consume_hook = consume_hook self._self_queue_consumer_generator = queue_consumer_generator def popleft(self, *args, **kwargs): try: # end active context if exists if self._self_active_token: context.detach(self._self_active_token) except Exception as inst_exception: # pylint: disable=W0703 _LOG.exception(inst_exception) evt = self.__wrapped__.popleft(*args, **kwargs) # pylint:disable=no-member try: # If a new message was received, create a span and set as active context if isinstance(evt, _ConsumerDeliveryEvt): method = evt.method properties = evt.properties if not properties: properties = BasicProperties(headers={}) if properties.headers is None: properties.headers = {} ctx = propagate.extract(properties.headers, getter=_pika_getter) if not ctx: ctx = context.get_current() message_ctx_token = context.attach(ctx) span = _get_span( self._self_tracer, None, properties, destination=(method.exchange if method.exchange else method.routing_key), span_kind=SpanKind.CONSUMER, task_name=self._self_queue_consumer_generator.consumer_tag, operation=MessagingOperationValues.RECEIVE, ) try: if message_ctx_token: context.detach(message_ctx_token) self._self_active_token = context.attach(trace.set_span_in_context(span)) self._self_consume_hook(span, evt.body, properties) except Exception as hook_exception: # pylint: disable=W0703 _LOG.exception(hook_exception) finally: # We must end the span here, because the next place we can hook # is not the end of the user code, but only when the next message # arrives. we still set this span's context as the active context # so spans created by user code that handles this message will be # children of this one. span.end() except Exception as inst_exception: # pylint: disable=W0703 _LOG.exception(inst_exception) return evt version.py000066400000000000000000000001441527070072300437200ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements-0-wrapt1.txt000066400000000000000000000000511527070072300376160ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika-r test-requirements-0.txt wrapt==1.17.3 test-requirements-0-wrapt2.txt000066400000000000000000000000501527070072300376160ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika-r test-requirements-0.txt wrapt==2.1.2 test-requirements-0.txt000066400000000000000000000003661527070072300364130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pikaasgiref==3.8.1 iniconfig==2.0.0 packaging==24.0 pika==0.13.1 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-pika test-requirements-1-wrapt1.txt000066400000000000000000000000511527070072300376170ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika-r test-requirements-1.txt wrapt==1.17.3 test-requirements-1-wrapt2.txt000066400000000000000000000000501527070072300376170ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika-r test-requirements-1.txt wrapt==2.1.2 test-requirements-1.txt000066400000000000000000000003651527070072300364130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pikaasgiref==3.8.1 iniconfig==2.0.0 packaging==24.0 pika==1.3.2 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-pika opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika/tests/000077500000000000000000000000001527070072300332315ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300352510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika/teststest_getter.py000066400000000000000000000013031527070072300360520ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from opentelemetry.instrumentation.pika.utils import _PikaGetter class TestPikaGetter(TestCase): def setUp(self) -> None: self.getter = _PikaGetter() def test_get_none(self) -> None: carrier = {} value = self.getter.get(carrier, "test") self.assertIsNone(value) def test_get_value(self) -> None: key = "test" value = "value" carrier = {key: value} val = self.getter.get(carrier, key) self.assertEqual(val, [value]) def test_keys(self): keys = self.getter.keys({}) self.assertEqual(keys, []) test_pika_instrumentation.py000066400000000000000000000332421527070072300410360ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections import Counter from types import SimpleNamespace from unittest import TestCase, mock from pika.adapters import BaseConnection, BlockingConnection from pika.adapters.blocking_connection import ( BlockingChannel, _QueueConsumerGeneratorInfo, ) from pika.channel import Channel from pika.connection import Connection from pika.spec import BasicProperties from wrapt import BoundFunctionWrapper from opentelemetry.instrumentation.pika import PikaInstrumentor from opentelemetry.instrumentation.pika.pika_instrumentor import ( _consumer_callback_attribute_name, ) from opentelemetry.instrumentation.pika.utils import ( ReadyMessagesDequeProxy, dummy_callback, ) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) from opentelemetry.trace import SpanKind, Tracer class TestPika(TestCase): def setUp(self) -> None: self.blocking_channel = mock.MagicMock(spec=BlockingChannel) self.channel = mock.MagicMock(spec=Channel) consumer_info = mock.MagicMock() callback_attr = PikaInstrumentor.CONSUMER_CALLBACK_ATTR setattr(consumer_info, callback_attr, self._consumer_callback) self.blocking_channel._consumer_infos = {"consumer-tag": consumer_info} self.channel._consumers = {"consumer-tag": consumer_info} self.mock_callback = mock.MagicMock() @staticmethod def _consumer_callback(channel: object, method: object, properties: object, body: object) -> None: pass def test_instrument_api(self) -> None: instrumentation = PikaInstrumentor() instrumentation.instrument() self.assertTrue(isinstance(BlockingConnection.channel, BoundFunctionWrapper)) self.assertTrue(isinstance(Connection.channel, BoundFunctionWrapper)) self.assertTrue(isinstance(BaseConnection.channel, BoundFunctionWrapper)) self.assertTrue(isinstance(_QueueConsumerGeneratorInfo.__init__, BoundFunctionWrapper)) assert hasattr(instrumentation, "__opentelemetry_tracer_provider"), "Tracer not stored for the object!" instrumentation.uninstrument() self.assertFalse(isinstance(BlockingConnection.channel, BoundFunctionWrapper)) self.assertFalse(isinstance(Connection.channel, BoundFunctionWrapper)) self.assertFalse(isinstance(BaseConnection.channel, BoundFunctionWrapper)) self.assertFalse(isinstance(_QueueConsumerGeneratorInfo.__init__, BoundFunctionWrapper)) @mock.patch("opentelemetry.instrumentation.pika.PikaInstrumentor._instrument_channel_functions") @mock.patch("opentelemetry.instrumentation.pika.PikaInstrumentor._decorate_basic_consume") @mock.patch("opentelemetry.instrumentation.pika.PikaInstrumentor._instrument_channel_consumers") def test_instrument_blocking_channel( self, instrument_channel_consumers: mock.MagicMock, instrument_basic_consume: mock.MagicMock, instrument_channel_functions: mock.MagicMock, ): PikaInstrumentor.instrument_channel(channel=self.blocking_channel) assert hasattr(self.blocking_channel, "_is_instrumented_by_opentelemetry"), ( "channel is not marked as instrumented!" ) instrument_channel_consumers.assert_called_once() instrument_basic_consume.assert_called_once() instrument_channel_functions.assert_called_once() @mock.patch("opentelemetry.instrumentation.pika.PikaInstrumentor._instrument_channel_functions") @mock.patch("opentelemetry.instrumentation.pika.PikaInstrumentor._decorate_basic_consume") @mock.patch("opentelemetry.instrumentation.pika.PikaInstrumentor._instrument_channel_consumers") def test_instrument_channel( self, instrument_channel_consumers: mock.MagicMock, instrument_basic_consume: mock.MagicMock, instrument_channel_functions: mock.MagicMock, ): PikaInstrumentor.instrument_channel(channel=self.channel) assert hasattr(self.channel, "_is_instrumented_by_opentelemetry"), "channel is not marked as instrumented!" instrument_channel_consumers.assert_called_once() instrument_basic_consume.assert_called_once() instrument_channel_functions.assert_called_once() @mock.patch("opentelemetry.instrumentation.pika.utils._decorate_callback") def test_instrument_consumers_on_blocking_channel(self, decorate_callback: mock.MagicMock) -> None: tracer = mock.MagicMock(spec=Tracer) callback_attr = PikaInstrumentor.CONSUMER_CALLBACK_ATTR expected_decoration_calls = [ mock.call(getattr(value, callback_attr), tracer, key, dummy_callback) for key, value in self.blocking_channel._consumer_infos.items() ] PikaInstrumentor._instrument_channel_consumers(self.blocking_channel, tracer) decorate_callback.assert_has_calls(calls=expected_decoration_calls, any_order=True) assert all( hasattr( getattr(callback, callback_attr), "_original_callback", ) for callback in self.blocking_channel._consumer_infos.values() ) @mock.patch("opentelemetry.instrumentation.pika.utils._decorate_callback") def test_instrument_consumers_on_channel(self, decorate_callback: mock.MagicMock) -> None: tracer = mock.MagicMock(spec=Tracer) callback_attr = PikaInstrumentor.CONSUMER_CALLBACK_ATTR expected_decoration_calls = [ mock.call(getattr(value, callback_attr), tracer, key, dummy_callback) for key, value in self.channel._consumers.items() ] PikaInstrumentor._instrument_channel_consumers(self.channel, tracer) decorate_callback.assert_has_calls(calls=expected_decoration_calls, any_order=True) assert all( hasattr( getattr(callback, callback_attr), "_original_callback", ) for callback in self.channel._consumers.values() ) @mock.patch("opentelemetry.instrumentation.pika.utils._decorate_callback") def test_instrument_consumers_skips_already_decorated_callbacks(self, decorate_callback: mock.MagicMock) -> None: tracer = mock.MagicMock(spec=Tracer) callback_attr = PikaInstrumentor.CONSUMER_CALLBACK_ATTR for channel, consumer_infos in ( (self.blocking_channel, self.blocking_channel._consumer_infos), (self.channel, self.channel._consumers), ): with self.subTest(channel=channel): decorated_consumer_info = mock.MagicMock() def decorated_callback( channel: object, method: object, properties: object, body: object, ) -> None: pass decorated_callback._original_callback = self._consumer_callback setattr( decorated_consumer_info, callback_attr, decorated_callback, ) new_consumer_info = mock.MagicMock() setattr( new_consumer_info, callback_attr, self._consumer_callback, ) consumer_infos.clear() consumer_infos.update( { "decorated-consumer-tag": decorated_consumer_info, "new-consumer-tag": new_consumer_info, } ) PikaInstrumentor._instrument_channel_consumers(channel, tracer) decorate_callback.assert_called_once_with( self._consumer_callback, tracer, "new-consumer-tag", dummy_callback, ) self.assertIs( getattr(decorated_consumer_info, callback_attr), decorated_callback, ) self.assertIs( getattr(new_consumer_info, callback_attr), decorate_callback.return_value, ) decorate_callback.reset_mock() def test_sequential_basic_consume_registrations_do_not_duplicate_spans( self, ) -> None: callback_attr = PikaInstrumentor.CONSUMER_CALLBACK_ATTR memory_exporter = InMemorySpanExporter() tracer_provider = TracerProvider() tracer_provider.add_span_processor(SimpleSpanProcessor(memory_exporter)) tracer = tracer_provider.get_tracer(__name__) callback_calls = [] def on_msg( channel: object, method: object, properties: object, body: object, ) -> None: callback_calls.append((channel, method, properties, body)) channel.basic_ack(method.delivery_tag) class FakeBlockingChannel: def __init__(self) -> None: self._consumer_infos = {} self.connection = SimpleNamespace(params=SimpleNamespace(host="localhost", port=5672)) self.basic_ack = mock.MagicMock() @property def __class__(self): return BlockingChannel def basic_consume(self, queue: str, *args: object, **kwargs: object) -> str: consumer_callback = ( kwargs.get("on_message_callback") or kwargs.get("consumer_callback") or kwargs.get("consumer_cb") or args[0] ) self._consumer_infos[queue] = SimpleNamespace(**{callback_attr: consumer_callback}) return queue channel = FakeBlockingChannel() PikaInstrumentor._decorate_basic_consume(channel, tracer) for queue_name in ("q1", "q2", "q3"): channel.basic_consume( queue_name, on_message_callback=on_msg, ) method = SimpleNamespace( exchange="", routing_key="q1", consumer_tag="q1", delivery_tag=1, ) properties = BasicProperties(headers={}) q1_callback = getattr(channel._consumer_infos["q1"], callback_attr) q1_callback( channel, method, properties, b"hello", ) spans = memory_exporter.get_finished_spans() self.assertEqual( Counter(span.name for span in spans if span.kind == SpanKind.CONSUMER), Counter({"q1 receive": 1}), ) self.assertEqual(callback_calls, [(channel, method, properties, b"hello")]) channel.basic_ack.assert_called_once_with(1) @mock.patch("opentelemetry.instrumentation.pika.utils._decorate_basic_publish") def test_instrument_basic_publish_on_blocking_channel(self, decorate_basic_publish: mock.MagicMock) -> None: tracer = mock.MagicMock(spec=Tracer) original_function = self.blocking_channel.basic_publish PikaInstrumentor._instrument_basic_publish(self.blocking_channel, tracer) decorate_basic_publish.assert_called_once_with(original_function, self.blocking_channel, tracer, dummy_callback) self.assertEqual( self.blocking_channel.basic_publish, decorate_basic_publish.return_value, ) @mock.patch("opentelemetry.instrumentation.pika.utils._decorate_basic_publish") def test_instrument_basic_publish_on_channel(self, decorate_basic_publish: mock.MagicMock) -> None: tracer = mock.MagicMock(spec=Tracer) original_function = self.channel.basic_publish PikaInstrumentor._instrument_basic_publish(self.channel, tracer) decorate_basic_publish.assert_called_once_with(original_function, self.channel, tracer, dummy_callback) self.assertEqual(self.channel.basic_publish, decorate_basic_publish.return_value) def test_instrument_queue_consumer_generator(self) -> None: instrumentation = PikaInstrumentor() instrumentation.instrument() generator_info = _QueueConsumerGeneratorInfo(params=("queue", False, False), consumer_tag="tag") self.assertTrue(isinstance(generator_info.pending_events, ReadyMessagesDequeProxy)) instrumentation.uninstrument() generator_info = _QueueConsumerGeneratorInfo(params=("queue", False, False), consumer_tag="tag") self.assertFalse(isinstance(generator_info.pending_events, ReadyMessagesDequeProxy)) def test_uninstrument_blocking_channel_functions(self) -> None: original_function = self.blocking_channel.basic_publish self.blocking_channel.basic_publish = mock.MagicMock() self.blocking_channel.basic_publish._original_function = original_function PikaInstrumentor._uninstrument_channel_functions(self.blocking_channel) self.assertEqual(self.blocking_channel.basic_publish, original_function) def test_uninstrument_channel_functions(self) -> None: original_function = self.channel.basic_publish self.channel.basic_publish = mock.MagicMock() self.channel.basic_publish._original_function = original_function PikaInstrumentor._uninstrument_channel_functions(self.channel) self.assertEqual(self.channel.basic_publish, original_function) def test_consumer_callback_attribute_name(self) -> None: with mock.patch("pika.__version__", "1.0.0"): self.assertEqual(_consumer_callback_attribute_name(), "on_message_callback") with mock.patch("pika.__version__", "0.12.0"): self.assertEqual(_consumer_callback_attribute_name(), "consumer_cb") test_utils.py000066400000000000000000000474411527070072300357350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pika/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import collections from unittest import TestCase, mock from pika.adapters.blocking_connection import ( _ConsumerCancellationEvt, _ConsumerDeliveryEvt, _QueueConsumerGeneratorInfo, ) from pika.channel import Channel from pika.spec import Basic, BasicProperties from opentelemetry.instrumentation.pika import utils from opentelemetry.semconv._incubating.attributes import messaging_attributes from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv.trace import ( MessagingOperationValues, SpanAttributes, ) from opentelemetry.trace import Span, SpanKind, Tracer class TestUtils(TestCase): @staticmethod @mock.patch("opentelemetry.context.get_value") @mock.patch("opentelemetry.instrumentation.pika.utils._generate_span_name") @mock.patch("opentelemetry.instrumentation.pika.utils._enrich_span") def test_get_span( enrich_span: mock.MagicMock, generate_span_name: mock.MagicMock, get_value: mock.MagicMock, ) -> None: tracer = mock.MagicMock(spec=Tracer) channel = mock.MagicMock() properties = mock.MagicMock() task_name = "test.test" destination = "myqueue" span_kind = mock.MagicMock(spec=SpanKind) get_value.return_value = None _ = utils._get_span(tracer, channel, properties, task_name, destination, span_kind) generate_span_name.assert_called_once() tracer.start_span.assert_called_once_with(name=generate_span_name.return_value, kind=span_kind) enrich_span.assert_called_once_with( tracer.start_span.return_value, channel, properties, destination, None, ) @mock.patch("opentelemetry.context.get_value") @mock.patch("opentelemetry.instrumentation.pika.utils._generate_span_name") @mock.patch("opentelemetry.instrumentation.pika.utils._enrich_span") def test_get_span_suppressed( self, enrich_span: mock.MagicMock, generate_span_name: mock.MagicMock, get_value: mock.MagicMock, ) -> None: tracer = mock.MagicMock(spec=Tracer) channel = mock.MagicMock() properties = mock.MagicMock() task_name = "test.test" span_kind = mock.MagicMock(spec=SpanKind) get_value.return_value = True ctx = mock.MagicMock() span = utils._get_span(tracer, channel, properties, task_name, span_kind, ctx) self.assertEqual(span, None) generate_span_name.assert_not_called() enrich_span.assert_not_called() def test_generate_span_name_no_operation(self) -> None: task_name = "test.test" operation = None span_name = utils._generate_span_name(task_name, operation) self.assertEqual(span_name, f"{task_name} send") def test_generate_span_name_with_operation(self) -> None: task_name = "test.test" operation = mock.MagicMock() operation.value = "process" span_name = utils._generate_span_name(task_name, operation) self.assertEqual(span_name, f"{task_name} {operation.value}") @staticmethod def test_enrich_span_basic_values() -> None: channel = mock.MagicMock() properties = mock.MagicMock() task_destination = "test.test" span = mock.MagicMock(spec=Span) utils._enrich_span(span, channel, properties, task_destination) span.set_attribute.assert_has_calls( any_order=True, calls=[ mock.call(messaging_attributes.MESSAGING_SYSTEM, "rabbitmq"), mock.call(SpanAttributes.MESSAGING_TEMP_DESTINATION, True), mock.call(SpanAttributes.MESSAGING_DESTINATION, task_destination), mock.call( messaging_attributes.MESSAGING_MESSAGE_ID, properties.message_id, ), mock.call( SpanAttributes.MESSAGING_CONVERSATION_ID, properties.correlation_id, ), mock.call( NET_PEER_NAME, channel.connection.params.host, ), mock.call( NET_PEER_PORT, channel.connection.params.port, ), ], ) @staticmethod def test_enrich_span_with_operation() -> None: channel = mock.MagicMock() properties = mock.MagicMock() task_destination = "test.test" operation = mock.MagicMock() span = mock.MagicMock(spec=Span) utils._enrich_span(span, channel, properties, task_destination, operation) span.set_attribute.assert_has_calls( any_order=True, calls=[mock.call(SpanAttributes.MESSAGING_OPERATION, operation.value)], ) @staticmethod def test_enrich_span_without_operation() -> None: channel = mock.MagicMock() properties = mock.MagicMock() task_destination = "test.test" span = mock.MagicMock(spec=Span) utils._enrich_span(span, channel, properties, task_destination) span.set_attribute.assert_has_calls( any_order=True, calls=[mock.call(SpanAttributes.MESSAGING_TEMP_DESTINATION, True)], ) @staticmethod def test_enrich_span_unique_connection() -> None: channel = mock.MagicMock() properties = mock.MagicMock() task_destination = "test.test" span = mock.MagicMock(spec=Span) # We do this to create the behaviour of hasattr(channel.connection, "params") == False del channel.connection.params utils._enrich_span(span, channel, properties, task_destination) span.set_attribute.assert_has_calls( any_order=True, calls=[ mock.call( NET_PEER_NAME, channel.connection._impl.params.host, ), mock.call( NET_PEER_PORT, channel.connection._impl.params.port, ), ], ) @mock.patch("opentelemetry.instrumentation.pika.utils._get_span") @mock.patch("opentelemetry.propagate.extract") @mock.patch("opentelemetry.trace.use_span") def test_decorate_callback( self, use_span: mock.MagicMock, extract: mock.MagicMock, get_span: mock.MagicMock, ) -> None: callback = mock.MagicMock() mock_task_name = "mock_task_name" tracer = mock.MagicMock() channel = mock.MagicMock(spec=Channel) method = mock.MagicMock(spec=Basic.Deliver) method.exchange = "test_exchange" properties = mock.MagicMock() mock_body = b"mock_body" decorated_callback = utils._decorate_callback(callback, tracer, mock_task_name) retval = decorated_callback(channel, method, properties, mock_body) extract.assert_called_once_with(properties.headers, getter=utils._pika_getter) get_span.assert_called_once_with( tracer, channel, properties, destination=method.exchange, span_kind=SpanKind.CONSUMER, task_name=mock_task_name, operation=MessagingOperationValues.RECEIVE, ) use_span.assert_called_once_with(get_span.return_value, end_on_exit=True) callback.assert_called_once_with(channel, method, properties, mock_body) self.assertEqual(retval, callback.return_value) @mock.patch("opentelemetry.instrumentation.pika.utils._get_span") @mock.patch("opentelemetry.propagate.extract") @mock.patch("opentelemetry.trace.use_span") def test_decorate_callback_with_hook( self, use_span: mock.MagicMock, extract: mock.MagicMock, get_span: mock.MagicMock, ) -> None: callback = mock.MagicMock() mock_task_name = "mock_task_name" tracer = mock.MagicMock() channel = mock.MagicMock(spec=Channel) method = mock.MagicMock(spec=Basic.Deliver) method.exchange = "test_exchange" properties = mock.MagicMock() mock_body = b"mock_body" consume_hook = mock.MagicMock() decorated_callback = utils._decorate_callback(callback, tracer, mock_task_name, consume_hook) retval = decorated_callback(channel, method, properties, mock_body) extract.assert_called_once_with(properties.headers, getter=utils._pika_getter) get_span.assert_called_once_with( tracer, channel, properties, destination=method.exchange, span_kind=SpanKind.CONSUMER, task_name=mock_task_name, operation=MessagingOperationValues.RECEIVE, ) use_span.assert_called_once_with(get_span.return_value, end_on_exit=True) consume_hook.assert_called_once_with(get_span.return_value, mock_body, properties) callback.assert_called_once_with(channel, method, properties, mock_body) self.assertEqual(retval, callback.return_value) @mock.patch("opentelemetry.instrumentation.pika.utils._get_span") @mock.patch("opentelemetry.propagate.inject") @mock.patch("opentelemetry.trace.use_span") def test_decorate_basic_publish( self, use_span: mock.MagicMock, inject: mock.MagicMock, get_span: mock.MagicMock, ) -> None: callback = mock.MagicMock() tracer = mock.MagicMock() channel = mock.MagicMock(spec=Channel) exchange_name = "test-exchange" routing_key = "test-routing-key" properties = mock.MagicMock() mock_body = b"mock_body" decorated_basic_publish = utils._decorate_basic_publish(callback, channel, tracer) retval = decorated_basic_publish(exchange_name, routing_key, mock_body, properties) get_span.assert_called_once_with( tracer, channel, properties, destination=exchange_name, span_kind=SpanKind.PRODUCER, task_name="(temporary)", operation=None, ) use_span.assert_called_once_with(get_span.return_value, end_on_exit=True) inject.assert_called_once_with(properties.headers) callback.assert_called_once_with(exchange_name, routing_key, mock_body, properties, False) self.assertEqual(retval, callback.return_value) @mock.patch("opentelemetry.instrumentation.pika.utils._get_span") @mock.patch("opentelemetry.propagate.inject") @mock.patch("opentelemetry.trace.use_span") @mock.patch("pika.spec.BasicProperties.__new__") def test_decorate_basic_publish_no_properties( self, basic_properties: mock.MagicMock, use_span: mock.MagicMock, inject: mock.MagicMock, get_span: mock.MagicMock, ) -> None: callback = mock.MagicMock() tracer = mock.MagicMock() channel = mock.MagicMock(spec=Channel) method = mock.MagicMock(spec=Basic.Deliver) mock_body = b"mock_body" decorated_basic_publish = utils._decorate_basic_publish(callback, channel, tracer) retval = decorated_basic_publish(channel, method, body=mock_body) basic_properties.assert_called_once_with(BasicProperties, headers={}) use_span.assert_called_once_with(get_span.return_value, end_on_exit=True) inject.assert_called_once_with(basic_properties.return_value.headers) self.assertEqual(retval, callback.return_value) @staticmethod @mock.patch("opentelemetry.instrumentation.pika.utils._get_span") def test_decorate_basic_publish_published_message_to_queue( get_span: mock.MagicMock, ) -> None: callback = mock.MagicMock() tracer = mock.MagicMock() channel = mock.MagicMock(spec=Channel) exchange_name = "" routing_key = "test-routing-key" properties = mock.MagicMock() mock_body = b"mock_body" decorated_basic_publish = utils._decorate_basic_publish(callback, channel, tracer) decorated_basic_publish(exchange_name, routing_key, mock_body, properties) get_span.assert_called_once_with( tracer, channel, properties, destination=routing_key, span_kind=SpanKind.PRODUCER, task_name="(temporary)", operation=None, ) @mock.patch("opentelemetry.instrumentation.pika.utils._get_span") @mock.patch("opentelemetry.propagate.inject") @mock.patch("opentelemetry.trace.use_span") def test_decorate_basic_publish_with_hook( self, use_span: mock.MagicMock, inject: mock.MagicMock, get_span: mock.MagicMock, ) -> None: callback = mock.MagicMock() tracer = mock.MagicMock() channel = mock.MagicMock(spec=Channel) exchange_name = "test-exchange" routing_key = "test-routing-key" properties = mock.MagicMock() mock_body = b"mock_body" publish_hook = mock.MagicMock() decorated_basic_publish = utils._decorate_basic_publish(callback, channel, tracer, publish_hook) retval = decorated_basic_publish(exchange_name, routing_key, mock_body, properties) get_span.assert_called_once_with( tracer, channel, properties, destination=exchange_name, span_kind=SpanKind.PRODUCER, task_name="(temporary)", operation=None, ) use_span.assert_called_once_with(get_span.return_value, end_on_exit=True) inject.assert_called_once_with(properties.headers) publish_hook.assert_called_once_with(get_span.return_value, mock_body, properties) callback.assert_called_once_with(exchange_name, routing_key, mock_body, properties, False) self.assertEqual(retval, callback.return_value) @mock.patch("opentelemetry.instrumentation.pika.utils._get_span") @mock.patch("opentelemetry.propagate.inject") @mock.patch("opentelemetry.trace.use_span") def test_decorate_basic_publish_when_span_is_not_recording( self, use_span: mock.MagicMock, inject: mock.MagicMock, get_span: mock.MagicMock, ) -> None: callback = mock.MagicMock() tracer = mock.MagicMock() channel = mock.MagicMock(spec=Channel) exchange_name = "test-exchange" routing_key = "test-routing-key" properties = mock.MagicMock() mock_body = b"mock_body" publish_hook = mock.MagicMock() mocked_span = mock.MagicMock() mocked_span.is_recording.return_value = False get_span.return_value = mocked_span decorated_basic_publish = utils._decorate_basic_publish(callback, channel, tracer, publish_hook) retval = decorated_basic_publish(exchange_name, routing_key, mock_body, properties) get_span.assert_called_once_with( tracer, channel, properties, destination=exchange_name, span_kind=SpanKind.PRODUCER, task_name="(temporary)", operation=None, ) use_span.assert_called_once_with(get_span.return_value, end_on_exit=True) inject.assert_called_once_with(properties.headers) publish_hook.assert_called_once_with(get_span.return_value, mock_body, properties) callback.assert_called_once_with(exchange_name, routing_key, mock_body, properties, False) self.assertEqual(retval, callback.return_value) # pylint: disable=too-many-statements @mock.patch("opentelemetry.instrumentation.pika.utils._get_span") @mock.patch("opentelemetry.propagate.extract") @mock.patch("opentelemetry.context.detach") @mock.patch("opentelemetry.context.attach") @mock.patch("opentelemetry.context.get_current") def test_decorate_deque_proxy( self, context_get_current: mock.MagicMock, context_attach: mock.MagicMock, context_detach: mock.MagicMock, extract: mock.MagicMock, get_span: mock.MagicMock, ) -> None: returned_span = mock.MagicMock() get_span.return_value = returned_span consume_hook = mock.MagicMock() tracer = mock.MagicMock() generator_info = mock.MagicMock( spec=_QueueConsumerGeneratorInfo, pending_events=mock.MagicMock(spec=collections.deque), consumer_tag="mock_task_name", ) method = mock.MagicMock(spec=Basic.Deliver) method.exchange = "test_exchange" properties = mock.MagicMock() evt = _ConsumerDeliveryEvt(method, properties, b"mock_body") generator_info.pending_events.popleft.return_value = evt proxy = utils.ReadyMessagesDequeProxy(generator_info.pending_events, generator_info, tracer, consume_hook) # First call (no detach cleanup) res = proxy.popleft() self.assertEqual(res, evt) generator_info.pending_events.popleft.assert_called_once() extract.assert_called_once_with(properties.headers, getter=utils._pika_getter) context_get_current.assert_called_once() self.assertEqual(context_attach.call_count, 2) self.assertEqual(context_detach.call_count, 1) get_span.assert_called_once_with( tracer, None, properties, destination=method.exchange, span_kind=SpanKind.CONSUMER, task_name=generator_info.consumer_tag, operation=MessagingOperationValues.RECEIVE, ) consume_hook.assert_called_once() returned_span.end.assert_called_once() generator_info.pending_events.reset_mock() extract.reset_mock() context_get_current.reset_mock() get_span.reset_mock() context_attach.reset_mock() context_detach.reset_mock() returned_span.end.reset_mock() consume_hook.reset_mock() # Second call (has detach cleanup) res = proxy.popleft() self.assertEqual(res, evt) generator_info.pending_events.popleft.assert_called_once() extract.assert_called_once_with(properties.headers, getter=utils._pika_getter) context_get_current.assert_called_once() self.assertEqual(context_attach.call_count, 2) self.assertEqual(context_detach.call_count, 2) get_span.assert_called_once_with( tracer, None, properties, destination=method.exchange, span_kind=SpanKind.CONSUMER, task_name=generator_info.consumer_tag, operation=MessagingOperationValues.RECEIVE, ) consume_hook.assert_called_once() returned_span.end.assert_called_once() generator_info.pending_events.reset_mock() extract.reset_mock() context_get_current.reset_mock() get_span.reset_mock() context_attach.reset_mock() context_detach.reset_mock() returned_span.end.reset_mock() consume_hook.reset_mock() # Third call (cancellation event) evt = _ConsumerCancellationEvt("") generator_info.pending_events.popleft.return_value = evt res = proxy.popleft() self.assertEqual(res, evt) generator_info.pending_events.popleft.assert_called_once() extract.assert_not_called() context_get_current.assert_not_called() context_detach.assert_called_once() context_attach.assert_not_called() get_span.assert_not_called() consume_hook.assert_not_called() returned_span.end.assert_not_called() def test_deque_proxy_is_iterable(self) -> None: deque = collections.deque([1, 2, 3]) generator_info = mock.MagicMock( spec=_QueueConsumerGeneratorInfo, consumer_tag="mock_task_name", ) proxy = utils.ReadyMessagesDequeProxy(deque, generator_info, None) self.assertEqual(list(proxy), [1, 2, 3]) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg/000077500000000000000000000000001527070072300326275ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg/LICENSE000066400000000000000000000261351527070072300336430ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg/README.rst000066400000000000000000000012371527070072300343210ustar00rootroot00000000000000OpenTelemetry Psycopg Instrumentation ===================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-psycopg.svg :target: https://pypi.org/project/opentelemetry-instrumentation-psycopg/ Installation ------------ :: pip install opentelemetry-instrumentation-psycopg References ---------- * `OpenTelemetry Psycopg Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031341527070072300354650ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-psycopg" dynamic = ["version"] description = "OpenTelemetry psycopg instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-instrumentation-dbapi == 0.66b2", ] [project.optional-dependencies] instruments = [ "psycopg >= 3.1.0", ] [project.entry-points.opentelemetry_instrumentor] psycopg = "opentelemetry.instrumentation.psycopg:PsycopgInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-psycopg" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/psycopg/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg/src/000077500000000000000000000000001527070072300334165ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300362335ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg/srcinstrumentation/000077500000000000000000000000001527070072300414765ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetrypsycopg/000077500000000000000000000000001527070072300431625ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation__init__.py000066400000000000000000000444561527070072300453100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The integration with PostgreSQL supports the `Psycopg`_ library. It can be enabled by using ``PsycopgInstrumentor``. .. _Psycopg: https://www.psycopg.org/psycopg3/docs/ Usage ----- .. code-block:: python import psycopg from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor # Call instrument() to wrap all database connections PsycopgInstrumentor().instrument() cnx = psycopg.connect(database="Database") cursor = cnx.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)") cursor.execute("INSERT INTO test (testField) VALUES (123)") cursor.close() cnx.close() .. code-block:: python import psycopg from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor # Alternatively, use instrument_connection for an individual connection cnx = psycopg.connect(database="Database") instrumented_cnx = PsycopgInstrumentor().instrument_connection(cnx) cursor = instrumented_cnx.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)") cursor.execute("INSERT INTO test (testField) VALUES (123)") cursor.close() instrumented_cnx.close() Configuration ------------- SQLCommenter ************ You can optionally configure Psycopg instrumentation to enable sqlcommenter which enriches the query with contextual information. Queries made after setting up trace integration with sqlcommenter enabled will have configurable key-value pairs appended to them, e.g. ``"select * from auth_users; /*traceparent=00-01234567-abcd-01*/"``. This supports context propagation between database client and server when database log records are enabled. For more information, see: * `Semantic Conventions - Database Spans `_ * `sqlcommenter `_ .. code:: python import psycopg from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor PsycopgInstrumentor().instrument(enable_commenter=True) # OR with specific connection cnx = psycopg.connect(database='Database') instrumented_cnx = PsycopgInstrumentor().instrument_connection( cnx, enable_commenter=True, ) SQLCommenter with commenter_options *********************************** The key-value pairs appended to the query can be configured using ``commenter_options``. When sqlcommenter is enabled, all available KVs/tags are calculated by default. ``commenter_options`` supports *opting out* of specific KVs. .. code:: python from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor # Opts into sqlcomment for Psycopg trace integration. # Opts out of tags for libpq_version, db_driver. PsycopgInstrumentor().instrument( enable_commenter=True, commenter_options={ "libpq_version": False, "db_driver": False, } ) Available commenter_options ########################### The following sqlcomment key-values can be opted out of through ``commenter_options``: +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | Commenter Option | Description | Example | +===========================+===========================================================+===========================================================================+ | ``db_driver`` | Database driver name with version. | ``psycopg='3.1.9'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``dbapi_threadsafety`` | DB-API threadsafety value: 0-3 or unknown. | ``dbapi_threadsafety=2`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``dbapi_level`` | DB-API API level: 1.0, 2.0, or unknown. | ``dbapi_level='2.0'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``driver_paramstyle`` | DB-API paramstyle for SQL statement parameter. | ``driver_paramstyle='pyformat'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``libpq_version`` | PostgreSQL libpq version | ``libpq_version=140001`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``opentelemetry_values`` | OpenTelemetry context as traceparent at time of query. | ``traceparent='00-03afa25236b8cd948fa853d67038ac79-405ff022e8247c46-01'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ SQLComment in span attribute **************************** If sqlcommenter is enabled, you can opt into the inclusion of sqlcomment in the query span ``db.statement`` and/or ``db.query.text`` attribute for your needs. If ``commenter_options`` have been set, the span attribute comment will also be configured by this setting. .. code:: python from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor # Opts into sqlcomment for Psycopg trace integration. # Opts into sqlcomment for `db.statement` and/or `db.query.text` span attribute. PsycopgInstrumentor().instrument( enable_commenter=True, enable_attribute_commenter=True, ) Warning: Capture of sqlcomment in ``db.statement``/``db.query.text`` may have high cardinality without platform normalization. See `Semantic Conventions for database spans `_ for more information. API --- """ from __future__ import annotations import logging from collections.abc import Callable, Collection from typing import Any, TypeVar import psycopg # pylint: disable=import-self from psycopg.sql import Composable # pylint: disable=no-name-in-module from opentelemetry.instrumentation import dbapi from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.psycopg.package import _instruments from opentelemetry.instrumentation.psycopg.version import __version__ from opentelemetry.trace import TracerProvider _logger = logging.getLogger(__name__) _OTEL_CURSOR_FACTORY_KEY = "_otel_orig_cursor_factory" ConnectionT = TypeVar("ConnectionT", psycopg.Connection, psycopg.AsyncConnection) CursorT = TypeVar("CursorT", psycopg.Cursor, psycopg.AsyncCursor) class PsycopgInstrumentor(BaseInstrumentor): _CONNECTION_ATTRIBUTES = { "database": "info.dbname", "port": "info.port", "host": "info.host", "user": "info.user", } _DATABASE_SYSTEM = "postgresql" def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs: Any): """Integrate with PostgreSQL Psycopg library. Psycopg: http://initd.org/psycopg/ """ tracer_provider = kwargs.get("tracer_provider") enable_sqlcommenter = kwargs.get("enable_commenter", False) commenter_options = kwargs.get("commenter_options", {}) enable_attribute_commenter = kwargs.get("enable_attribute_commenter", False) capture_parameters = kwargs.get("capture_parameters", False) dbapi.wrap_connect( __name__, psycopg, "connect", self._DATABASE_SYSTEM, self._CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, db_api_integration_factory=DatabaseApiIntegration, enable_commenter=enable_sqlcommenter, commenter_options=commenter_options, enable_attribute_commenter=enable_attribute_commenter, capture_parameters=capture_parameters, ) dbapi.wrap_connect( __name__, psycopg.Connection, # pylint: disable=no-member "connect", self._DATABASE_SYSTEM, self._CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, db_api_integration_factory=DatabaseApiIntegration, enable_commenter=enable_sqlcommenter, commenter_options=commenter_options, enable_attribute_commenter=enable_attribute_commenter, capture_parameters=capture_parameters, ) dbapi.wrap_connect( __name__, psycopg.AsyncConnection, # pylint: disable=no-member "connect", self._DATABASE_SYSTEM, self._CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, db_api_integration_factory=DatabaseApiAsyncIntegration, enable_commenter=enable_sqlcommenter, commenter_options=commenter_options, enable_attribute_commenter=enable_attribute_commenter, capture_parameters=capture_parameters, ) def _uninstrument(self, **kwargs: Any): """ "Disable Psycopg instrumentation""" dbapi.unwrap_connect(psycopg, "connect") # pylint: disable=no-member dbapi.unwrap_connect( psycopg.Connection, "connect", # pylint: disable=no-member ) dbapi.unwrap_connect( psycopg.AsyncConnection, "connect", # pylint: disable=no-member ) # TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql @staticmethod def instrument_connection( connection: ConnectionT, tracer_provider: TracerProvider | None = None, enable_commenter: bool = False, commenter_options: dict[Any, Any] | None = None, enable_attribute_commenter: bool = False, ) -> ConnectionT: """Enable instrumentation in a psycopg connection. Args: connection: psycopg.Connection The psycopg connection object to be instrumented. tracer_provider: opentelemetry.trace.TracerProvider, optional The TracerProvider to use for instrumentation. If not provided, the global TracerProvider will be used. enable_commenter: bool, optional Optional flag to enable/disable sqlcommenter (default False). commenter_options: dict, optional Optional configurations for tags to be appended at the sql query. enable_attribute_commenter: Optional flag to enable/disable addition of sqlcomment to span attribute (default False). Requires enable_commenter=True. Returns: An instrumented psycopg connection object. """ if not hasattr(connection, "_is_instrumented_by_opentelemetry"): connection._is_instrumented_by_opentelemetry = False if not connection._is_instrumented_by_opentelemetry: setattr(connection, _OTEL_CURSOR_FACTORY_KEY, connection.cursor_factory) if isinstance(connection, psycopg.AsyncConnection): connection.cursor_factory = _new_cursor_async_factory( tracer_provider=tracer_provider, enable_commenter=enable_commenter, commenter_options=commenter_options, enable_attribute_commenter=enable_attribute_commenter, ) else: connection.cursor_factory = _new_cursor_factory( tracer_provider=tracer_provider, enable_commenter=enable_commenter, commenter_options=commenter_options, enable_attribute_commenter=enable_attribute_commenter, ) connection._is_instrumented_by_opentelemetry = True else: _logger.warning("Attempting to instrument Psycopg connection while already instrumented") return connection # TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql @staticmethod def uninstrument_connection(connection: ConnectionT) -> ConnectionT: connection.cursor_factory = getattr(connection, _OTEL_CURSOR_FACTORY_KEY, None) return connection # TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql class DatabaseApiIntegration(dbapi.DatabaseApiIntegration): def wrapped_connection( self, connect_method: Callable[..., Any], args: tuple[Any, Any], kwargs: dict[Any, Any], ): """Add object proxy to connection object.""" base_cursor_factory = kwargs.pop("cursor_factory", None) new_factory_kwargs = {"db_api": self} if base_cursor_factory: new_factory_kwargs["base_factory"] = base_cursor_factory kwargs["cursor_factory"] = _new_cursor_factory(**new_factory_kwargs) connection = connect_method(*args, **kwargs) self.get_connection_attributes(connection) return connection class DatabaseApiAsyncIntegration(dbapi.DatabaseApiIntegration): async def wrapped_connection( self, connect_method: Callable[..., Any], args: tuple[Any, Any], kwargs: dict[Any, Any], ): """Add object proxy to connection object.""" base_cursor_factory = kwargs.pop("cursor_factory", None) new_factory_kwargs = {"db_api": self} if base_cursor_factory: new_factory_kwargs["base_factory"] = base_cursor_factory kwargs["cursor_factory"] = _new_cursor_async_factory(**new_factory_kwargs) connection = await connect_method(*args, **kwargs) self.get_connection_attributes(connection) return connection class CursorTracer(dbapi.CursorTracer): def get_operation_name(self, cursor: CursorT, args: list[Any]) -> str: if not args: return "" statement = args[0] if isinstance(statement, Composable): statement = statement.as_string(cursor) # A statement that is truthy but has no tokens after leading-comment # or whitespace stripping (e.g. a comment-only Composed) must not # raise IndexError; return an empty operation name instead. tokens = self._leading_comment_remover.sub("", statement).split() return tokens[0] if tokens else "" return super().get_operation_name(cursor, args) def get_statement(self, cursor: CursorT, args: list[Any]) -> str: if not args: return "" statement = args[0] if isinstance(statement, Composable): return statement.as_string(cursor) return super().get_statement(cursor, args) def _new_cursor_factory( db_api: DatabaseApiIntegration | None = None, base_factory: type[psycopg.Cursor] | None = None, tracer_provider: TracerProvider | None = None, enable_commenter: bool = False, commenter_options: dict = None, enable_attribute_commenter: bool = False, ): if not db_api: db_api = DatabaseApiIntegration( __name__, PsycopgInstrumentor._DATABASE_SYSTEM, connection_attributes=PsycopgInstrumentor._CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, enable_commenter=enable_commenter, commenter_options=commenter_options, connect_module=psycopg, enable_attribute_commenter=enable_attribute_commenter, ) base_factory = base_factory or psycopg.Cursor _cursor_tracer = CursorTracer(db_api) class TracedCursorFactory(base_factory): def execute(self, *args: Any, **kwargs: Any): return _cursor_tracer.traced_execution(self, super().execute, *args, **kwargs) def executemany(self, *args: Any, **kwargs: Any): return _cursor_tracer.traced_execution(self, super().executemany, *args, **kwargs) def callproc(self, *args: Any, **kwargs: Any): return _cursor_tracer.traced_execution(self, super().callproc, *args, **kwargs) return TracedCursorFactory def _new_cursor_async_factory( db_api: DatabaseApiAsyncIntegration | None = None, base_factory: type[psycopg.AsyncCursor] | None = None, tracer_provider: TracerProvider | None = None, enable_commenter: bool = False, commenter_options: dict = None, enable_attribute_commenter: bool = False, ): if not db_api: db_api = DatabaseApiAsyncIntegration( __name__, PsycopgInstrumentor._DATABASE_SYSTEM, connection_attributes=PsycopgInstrumentor._CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, enable_commenter=enable_commenter, commenter_options=commenter_options, connect_module=psycopg, enable_attribute_commenter=enable_attribute_commenter, ) base_factory = base_factory or psycopg.AsyncCursor _cursor_tracer = CursorTracer(db_api) class TracedCursorAsyncFactory(base_factory): async def execute(self, *args: Any, **kwargs: Any): return await _cursor_tracer.traced_execution_async(self, super().execute, *args, **kwargs) async def executemany(self, *args: Any, **kwargs: Any): return await _cursor_tracer.traced_execution_async(self, super().executemany, *args, **kwargs) async def callproc(self, *args: Any, **kwargs: Any): return await _cursor_tracer.traced_execution_async(self, super().callproc, *args, **kwargs) return TracedCursorAsyncFactory package.py000066400000000000000000000003051527070072300451250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations _instruments: tuple[str, ...] = ("psycopg >= 3.1.0",) _semconv_status = "migration" py.typed000066400000000000000000000000001527070072300446470ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopgversion.py000066400000000000000000000001441527070072300452200ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements-0.txt000066400000000000000000000005561527070072300371540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopgasgiref==3.8.1 backports.zoneinfo==0.2.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 psycopg==3.1.18 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-dbapi -e instrumentation/opentelemetry-instrumentation-psycopg test-requirements-1.txt000066400000000000000000000005231527070072300371470ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopgasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 psycopg==3.3.4 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-dbapi -e instrumentation/opentelemetry-instrumentation-psycopg opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg/tests/000077500000000000000000000000001527070072300337715ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300360110ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg/teststest_psycopg_integration.py000066400000000000000000001170241527070072300414170ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=no-member,too-many-lines # psycopg stubs reference string.templatelib on 3.14+ import asyncio import contextlib import sys import types from unittest import IsolatedAsyncioTestCase, mock import psycopg import pytest from psycopg.sql import SQL, Composed import opentelemetry.instrumentation.psycopg from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor from opentelemetry.sdk import resources from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_STATEMENT, DB_SYSTEM, DB_USER, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_NAMESPACE, DB_QUERY_TEXT, DB_SYSTEM_NAME, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.test.test_base import TestBase @contextlib.contextmanager def use_semconv_opt_in(sem_conv_mode): env_patch = mock.patch.dict( "os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode}, ) _OpenTelemetrySemanticConventionStability._initialized = False env_patch.start() try: yield finally: env_patch.stop() _OpenTelemetrySemanticConventionStability._initialized = False class MockCursor: execute = mock.MagicMock(spec=types.MethodType) execute.__name__ = "execute" executemany = mock.MagicMock(spec=types.MethodType) executemany.__name__ = "executemany" callproc = mock.MagicMock(spec=types.MethodType) callproc.__name__ = "callproc" connection = None rowcount = "SomeRowCount" def __init__(self, *args, **kwargs): pass def __enter__(self): return self def __exit__(self, *args): return self class MockAsyncCursor: def __init__(self, *args, **kwargs): pass # pylint: disable=unused-argument, no-self-use async def execute(self, query, params=None, throw_exception=False, delay=0.0): if throw_exception: raise psycopg.Error("Test Exception") if delay: await asyncio.sleep(delay) # pylint: disable=unused-argument, no-self-use async def executemany(self, query, params=None, throw_exception=False): if throw_exception: raise psycopg.Error("Test Exception") # pylint: disable=unused-argument, no-self-use async def callproc(self, query, params=None, throw_exception=False): if throw_exception: raise psycopg.Error("Test Exception") async def __aenter__(self, *args, **kwargs): return self async def __aexit__(self, *args, **kwargs): pass def close(self): pass class MockConnection: commit = mock.MagicMock(spec=types.MethodType) commit.__name__ = "commit" rollback = mock.MagicMock(spec=types.MethodType) rollback.__name__ = "rollback" def __init__(self, *args, **kwargs): self.cursor_factory = kwargs.pop("cursor_factory", None) def cursor(self): if self.cursor_factory: return self.cursor_factory(self) return MockCursor() def get_dsn_parameters(self): # pylint: disable=no-self-use return {"dbname": "test"} class MockAsyncConnection(psycopg.AsyncConnection): commit = mock.MagicMock(spec=types.MethodType) commit.__name__ = "commit" rollback = mock.MagicMock(spec=types.MethodType) rollback.__name__ = "rollback" def __init__(self, *args, **kwargs): self.cursor_factory = kwargs.pop("cursor_factory", None) @staticmethod async def connect(*args, **kwargs): return MockAsyncConnection(**kwargs) def cursor(self, *args, **kwargs): if self.cursor_factory: cur = self.cursor_factory(self) return cur return MockAsyncCursor(*args, **kwargs) def execute(self, query, params=None, *, prepare=None, binary=False): cur = self.cursor() return cur.execute(query, params, prepare=prepare) def get_dsn_parameters(self): # pylint: disable=no-self-use return {"dbname": "test"} async def __aenter__(self): return self async def __aexit__(self, *args): return mock.MagicMock(spec=types.MethodType) class MockConnectionInfo: dbname = "test" host = "localhost" port = 5432 user = "testuser" class MockConnectionWithInfo(MockConnection): info = MockConnectionInfo() class MockAsyncConnectionWithInfo(MockAsyncConnection): info = MockConnectionInfo() @staticmethod async def connect(*args, **kwargs): return MockAsyncConnectionWithInfo(**kwargs) class PostgresqlIntegrationTestMixin: # pylint: disable=invalid-name def setUp(self): super().setUp() self.cursor_mock = mock.patch("opentelemetry.instrumentation.psycopg.psycopg.Cursor", MockCursor) self.cursor_async_mock = mock.patch( "opentelemetry.instrumentation.psycopg.psycopg.AsyncCursor", MockAsyncCursor, ) self.connection_mock = mock.patch("psycopg.connect", MockConnection) self.connection_sync_mock = mock.patch("psycopg.Connection.connect", MockConnection) self.connection_async_mock = mock.patch("psycopg.AsyncConnection.connect", MockAsyncConnection.connect) self.cursor_mock.start() self.cursor_async_mock.start() self.connection_mock.start() self.connection_sync_mock.start() self.connection_async_mock.start() # pylint: disable=invalid-name def tearDown(self): super().tearDown() self.memory_exporter.clear() self.cursor_mock.stop() self.cursor_async_mock.stop() self.connection_mock.stop() self.connection_sync_mock.stop() self.connection_async_mock.stop() with self.disable_logging(): PsycopgInstrumentor().uninstrument() # pylint: disable=too-many-public-methods class TestPostgresqlIntegration(PostgresqlIntegrationTestMixin, TestBase): # pylint: disable=unused-argument def test_instrumentor(self): PsycopgInstrumentor().instrument() cnx = psycopg.connect(database="test") self.assertTrue(issubclass(cnx.cursor_factory, MockCursor)) cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.psycopg) # check that no spans are generated after uninstrument PsycopgInstrumentor().uninstrument() cnx = psycopg.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) # pylint: disable=unused-argument def test_instrumentor_with_connection_class(self): PsycopgInstrumentor().instrument() cnx = psycopg.Connection.connect(database="test") self.assertTrue(issubclass(cnx.cursor_factory, MockCursor)) cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.psycopg) # check that no spans are generated after uninstrument PsycopgInstrumentor().uninstrument() cnx = psycopg.Connection.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) def test_span_name(self): PsycopgInstrumentor().instrument() cnx = psycopg.connect(database="test") self.assertTrue(issubclass(cnx.cursor_factory, MockCursor)) cursor = cnx.cursor() cursor.execute("Test query", ("param1Value", False)) cursor.execute( """multi line query""" ) cursor.execute("tab\tseparated query") cursor.execute("/* leading comment */ query") cursor.execute("/* leading comment */ query /* trailing comment */") cursor.execute("query /* trailing comment */") cursor.execute("") cursor.execute("--") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 8) self.assertEqual(spans_list[0].name, "Test") self.assertEqual(spans_list[1].name, "multi") self.assertEqual(spans_list[2].name, "tab") self.assertEqual(spans_list[3].name, "query") self.assertEqual(spans_list[4].name, "query") self.assertEqual(spans_list[5].name, "query") self.assertEqual(spans_list[6].name, "postgresql") self.assertEqual(spans_list[7].name, "--") def test_span_params_attribute(self): PsycopgInstrumentor().instrument(capture_parameters=True) cnx = psycopg.connect(database="test") self.assertTrue(issubclass(cnx.cursor_factory, MockCursor)) query = "SELECT * FROM mytable WHERE myparam1 = %s AND myparam2 = %s" params = ("test", 42) cursor = cnx.cursor() cursor.execute(query, params) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) self.assertEqual(spans_list[0].name, "SELECT") assert spans_list[0].attributes is not None self.assertEqual(spans_list[0].attributes["db.statement"], query) self.assertEqual(spans_list[0].attributes["db.statement.parameters"], str(params)) # pylint: disable=unused-argument def test_not_recording(self): mock_tracer = mock.Mock() mock_span = mock.Mock() mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span PsycopgInstrumentor().instrument() with mock.patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer cnx = psycopg.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) PsycopgInstrumentor().uninstrument() # pylint: disable=unused-argument def test_custom_tracer_provider(self): resource = resources.Resource.create({}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result PsycopgInstrumentor().instrument(tracer_provider=tracer_provider) cnx = psycopg.connect(database="test") self.assertTrue(issubclass(cnx.cursor_factory, MockCursor)) cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertIs(span.resource, resource) # pylint: disable=unused-argument def test_instrument_connection(self): cnx = psycopg.connect(database="test") query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) cnx = PsycopgInstrumentor().instrument_connection(cnx) self.assertTrue(issubclass(cnx.cursor_factory, MockCursor)) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) def test_instrument_connection_typed_sql_query(self): cnx = psycopg.connect(database="test") query = SQL("SELECT * FROM test") cnx = PsycopgInstrumentor().instrument_connection(cnx) self.assertTrue(issubclass(cnx.cursor_factory, MockCursor)) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) self.assertEqual(spans_list[0].name, "SELECT") self.assertEqual(spans_list[0].attributes["db.statement"], "SELECT * FROM test") def test_instrument_connection_composed_query(self): cnx = psycopg.connect(database="test") query: Composed = SQL("SELECT * FROM test").format() cnx = PsycopgInstrumentor().instrument_connection(cnx) self.assertTrue(issubclass(cnx.cursor_factory, MockCursor)) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) self.assertEqual(spans_list[0].name, "SELECT") self.assertEqual(spans_list[0].attributes["db.statement"], "SELECT * FROM test") def test_instrument_connection_comment_only_composed_query(self): # Regression: a Composed statement that is comment-only (empty after # leading-comment stripping) must not raise IndexError in # get_operation_name. cnx = psycopg.connect(database="test") query: Composed = SQL("/* comment only */").format() cnx = PsycopgInstrumentor().instrument_connection(cnx) cursor = cnx.cursor() # Must not raise IndexError. cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) # No tokens survive comment stripping, so operation_name is empty; # instrument_connection's mock fixture has no db name/vendor to fall # back to either, so the span name is empty too. self.assertEqual(spans_list[0].name, "") # pylint: disable=unused-argument def test_instrument_connection_with_instrument(self): cnx = psycopg.connect(database="test") query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) PsycopgInstrumentor().instrument() cnx = PsycopgInstrumentor().instrument_connection(cnx) self.assertTrue(issubclass(cnx.cursor_factory, MockCursor)) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) # pylint: disable=unused-argument def test_uninstrument_connection_with_instrument(self): PsycopgInstrumentor().instrument() cnx = psycopg.connect(database="test") query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) cnx = PsycopgInstrumentor().uninstrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) # pylint: disable=unused-argument def test_uninstrument_connection_with_instrument_connection(self): cnx = psycopg.connect(database="test") PsycopgInstrumentor().instrument_connection(cnx) query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) cnx = PsycopgInstrumentor().uninstrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) @mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect") def test_sqlcommenter_enabled(self, event_mocked): cnx = psycopg.connect(database="test") PsycopgInstrumentor().instrument(enable_commenter=True) query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) kwargs = event_mocked.call_args[1] self.assertEqual(kwargs["enable_commenter"], True) def test_sqlcommenter_enabled_instrument_connection_defaults(self): with ( mock.patch( "opentelemetry.instrumentation.psycopg.psycopg.__version__", "foobar", ), mock.patch( "opentelemetry.instrumentation.psycopg.psycopg.pq.__build_version__", "foobaz", ), mock.patch( "opentelemetry.instrumentation.psycopg.psycopg.pq.version", new=lambda: "foobaz", ), mock.patch( "opentelemetry.instrumentation.psycopg.psycopg.threadsafety", "123", ), mock.patch( "opentelemetry.instrumentation.psycopg.psycopg.apilevel", "123", ), mock.patch( "opentelemetry.instrumentation.psycopg.psycopg.paramstyle", "test", ), ): cnx = psycopg.connect(database="test") cnx = PsycopgInstrumentor().instrument_connection( cnx, enable_commenter=True, ) query = "Select 1" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = int(span.get_span_context().trace_flags) self.assertEqual( MockCursor.execute.call_args[0][0], f"Select 1 /*db_driver='psycopg%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',libpq_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags:02x}'*/", ) self.assertEqual( span.attributes[DB_STATEMENT], "Select 1", ) def test_sqlcommenter_enabled_instrument_connection_stmt_enabled(self): with ( mock.patch( "opentelemetry.instrumentation.psycopg.psycopg.__version__", "foobar", ), mock.patch( "opentelemetry.instrumentation.psycopg.psycopg.pq.__build_version__", "foobaz", ), mock.patch( "opentelemetry.instrumentation.psycopg.psycopg.pq.version", new=lambda: "foobaz", ), mock.patch( "opentelemetry.instrumentation.psycopg.psycopg.threadsafety", "123", ), mock.patch( "opentelemetry.instrumentation.psycopg.psycopg.apilevel", "123", ), mock.patch( "opentelemetry.instrumentation.psycopg.psycopg.paramstyle", "test", ), ): cnx = psycopg.connect(database="test") cnx = PsycopgInstrumentor().instrument_connection( cnx, enable_commenter=True, enable_attribute_commenter=True, ) query = "Select 1" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = int(span.get_span_context().trace_flags) self.assertEqual( MockCursor.execute.call_args[0][0], f"Select 1 /*db_driver='psycopg%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',libpq_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags:02x}'*/", ) self.assertEqual( span.attributes[DB_STATEMENT], f"Select 1 /*db_driver='psycopg%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',libpq_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags:02x}'*/", ) def test_sqlcommenter_enabled_instrument_connection_with_options(self): with ( mock.patch( "opentelemetry.instrumentation.psycopg.psycopg.__version__", "foobar", ), mock.patch( "opentelemetry.instrumentation.psycopg.psycopg.pq.__build_version__", "foobaz", ), mock.patch( "opentelemetry.instrumentation.psycopg.psycopg.pq.version", new=lambda: "foobaz", ), mock.patch( "opentelemetry.instrumentation.psycopg.psycopg.threadsafety", "123", ), ): cnx = psycopg.connect(database="test") cnx = PsycopgInstrumentor().instrument_connection( cnx, enable_commenter=True, commenter_options={ "dbapi_level": False, "dbapi_threadsafety": True, "driver_paramstyle": False, "foo": "ignored", }, ) query = "Select 1" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = int(span.get_span_context().trace_flags) self.assertEqual( MockCursor.execute.call_args[0][0], f"Select 1 /*db_driver='psycopg%%3Afoobar',dbapi_threadsafety='123',libpq_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags:02x}'*/", ) self.assertEqual( span.attributes[DB_STATEMENT], "Select 1", ) @mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect") def test_sqlcommenter_disabled(self, event_mocked): cnx = psycopg.connect(database="test") PsycopgInstrumentor().instrument() query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) kwargs = event_mocked.call_args[1] self.assertEqual(kwargs["enable_commenter"], False) def test_sqlcommenter_disabled_default_instrument_connection(self): cnx = psycopg.connect(database="test") cnx = PsycopgInstrumentor().instrument_connection( cnx, ) query = "Select 1" cursor = cnx.cursor() cursor.execute(query) self.assertEqual( MockCursor.execute.call_args[0][0], "Select 1", ) spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1", ) def test_sqlcommenter_disabled_explicit_instrument_connection(self): cnx = psycopg.connect(database="test") cnx = PsycopgInstrumentor().instrument_connection( cnx, enable_commenter=False, ) query = "Select 1" cursor = cnx.cursor() cursor.execute(query) self.assertEqual( MockCursor.execute.call_args[0][0], "Select 1", ) spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1", ) @pytest.mark.skipif( sys.version_info < (3, 14), reason="requires Python 3.14+ for t-strings", ) def test_t_string_span_attributes(self): # pylint: disable-next=import-outside-toplevel,no-name-in-module,no-member from string.templatelib import Interpolation, Template # noqa: PLC0415 PsycopgInstrumentor().instrument() cnx = psycopg.connect(database="test") cursor = cnx.cursor() value = 42 template = Template("SELECT ", Interpolation(value, "value"), " FROM foo") cursor.execute(template) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "SELECT") self.assertEqual(span.attributes["db.statement"], "SELECT {value} FROM foo") @pytest.mark.skipif( sys.version_info < (3, 14), reason="requires Python 3.14+ for t-strings", ) def test_t_string_commenter(self): # pylint: disable-next=import-outside-toplevel,no-name-in-module,no-member from string.templatelib import Interpolation, Template # noqa: PLC0415 PsycopgInstrumentor().instrument(enable_commenter=True) cnx = psycopg.connect(database="test") cursor = cnx.cursor() MockCursor.executemany.reset_mock() value = 42 template = Template("SELECT ", Interpolation(value, "value"), " FROM foo;") cursor.executemany(template) called_query = MockCursor.executemany.call_args[0][0] self.assertIsInstance(called_query, Template) self.assertRegex( called_query.strings[-1], r" FROM foo /\*.*traceparent=.*\*/;", ) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "SELECT") self.assertEqual(span.attributes["db.statement"], "SELECT {value} FROM foo;") def test_semconv_stable(self): """database,http opt-in emits only stable attributes.""" with ( use_semconv_opt_in("database,http"), mock.patch("psycopg.connect", MockConnectionWithInfo), ): PsycopgInstrumentor().instrument() cnx = psycopg.connect(database="test") cursor = cnx.cursor() cursor.execute("SELECT * FROM test") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_SYSTEM_NAME], "postgresql") self.assertEqual(span.attributes[DB_NAMESPACE], "test") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT * FROM test") self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost") self.assertEqual(span.attributes[SERVER_PORT], 5432) self.assertNotIn(DB_SYSTEM, span.attributes) self.assertNotIn(DB_NAME, span.attributes) self.assertNotIn(DB_STATEMENT, span.attributes) self.assertNotIn(DB_USER, span.attributes) self.assertNotIn(NET_PEER_NAME, span.attributes) self.assertNotIn(NET_PEER_PORT, span.attributes) def test_semconv_dup(self): """database/dup,http/dup opt-in emits both legacy and stable attributes.""" with ( use_semconv_opt_in("database/dup,http/dup"), mock.patch("psycopg.connect", MockConnectionWithInfo), ): PsycopgInstrumentor().instrument() cnx = psycopg.connect(database="test") cursor = cnx.cursor() cursor.execute("SELECT * FROM test") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_SYSTEM], "postgresql") self.assertEqual(span.attributes[DB_SYSTEM_NAME], "postgresql") self.assertEqual(span.attributes[DB_NAME], "test") self.assertEqual(span.attributes[DB_NAMESPACE], "test") self.assertEqual(span.attributes[DB_STATEMENT], "SELECT * FROM test") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT * FROM test") self.assertEqual(span.attributes[DB_USER], "testuser") self.assertEqual(span.attributes[NET_PEER_NAME], "localhost") self.assertEqual(span.attributes[NET_PEER_PORT], 5432) self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost") self.assertEqual(span.attributes[SERVER_PORT], 5432) class TestPostgresqlIntegrationAsync(PostgresqlIntegrationTestMixin, TestBase, IsolatedAsyncioTestCase): async def test_wrap_async_connection_class_with_cursor(self): PsycopgInstrumentor().instrument() async def test_async_connection(): acnx = await psycopg.AsyncConnection.connect("test") async with acnx as cnx: self.assertTrue(issubclass(cnx.cursor_factory, MockAsyncCursor)) async with cnx.cursor() as cursor: await cursor.execute("SELECT * FROM test") await test_async_connection() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.psycopg) # check that no spans are generated after uninstrument PsycopgInstrumentor().uninstrument() await test_async_connection() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) # pylint: disable=unused-argument async def test_instrumentor_with_async_connection_class(self): PsycopgInstrumentor().instrument() async def test_async_connection(): acnx = await psycopg.AsyncConnection.connect("test") async with acnx as cnx: self.assertTrue(issubclass(cnx.cursor_factory, MockAsyncCursor)) await cnx.execute("SELECT * FROM test") await test_async_connection() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.psycopg) # check that no spans are generated after uninstrument PsycopgInstrumentor().uninstrument() await test_async_connection() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) async def test_span_name_async(self): PsycopgInstrumentor().instrument() cnx = await psycopg.AsyncConnection.connect("test") self.assertTrue(issubclass(cnx.cursor_factory, MockAsyncCursor)) async with cnx.cursor() as cursor: await cursor.execute("Test query", ("param1Value", False)) await cursor.execute( """multi line query""" ) await cursor.execute("tab\tseparated query") await cursor.execute("/* leading comment */ query") await cursor.execute("/* leading comment */ query /* trailing comment */") await cursor.execute("query /* trailing comment */") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 6) self.assertEqual(spans_list[0].name, "Test") self.assertEqual(spans_list[1].name, "multi") self.assertEqual(spans_list[2].name, "tab") self.assertEqual(spans_list[3].name, "query") self.assertEqual(spans_list[4].name, "query") self.assertEqual(spans_list[5].name, "query") async def test_span_params_attribute(self): PsycopgInstrumentor().instrument(capture_parameters=True) cnx = await psycopg.AsyncConnection.connect("test") self.assertTrue(issubclass(cnx.cursor_factory, MockAsyncCursor)) query = "SELECT * FROM mytable WHERE myparam1 = %s AND myparam2 = %s" params = ("test", 42) async with cnx.cursor() as cursor: await cursor.execute(query, params) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) self.assertEqual(spans_list[0].name, "SELECT") assert spans_list[0].attributes is not None self.assertEqual(spans_list[0].attributes["db.statement"], query) self.assertEqual(spans_list[0].attributes["db.statement.parameters"], str(params)) # pylint: disable=unused-argument async def test_not_recording_async(self): mock_tracer = mock.Mock() mock_span = mock.Mock() mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span PsycopgInstrumentor().instrument() with mock.patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer cnx = await psycopg.AsyncConnection.connect("test") async with cnx.cursor() as cursor: query = "SELECT * FROM test" await cursor.execute(query) self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) PsycopgInstrumentor().uninstrument() async def test_tracing_is_async(self): PsycopgInstrumentor().instrument() # before this async fix cursor.execute would take 14000 ns, delaying for # 100,000ns delay = 0.0001 async def test_async_connection(): acnx = await psycopg.AsyncConnection.connect("test") self.assertTrue(issubclass(acnx.cursor_factory, MockAsyncCursor)) async with acnx as cnx: async with cnx.cursor() as cursor: await cursor.execute("SELECT * FROM test", delay=delay) await test_async_connection() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] # duration is nanoseconds duration = span.end_time - span.start_time self.assertGreater(duration, delay * 1e9) PsycopgInstrumentor().uninstrument() async def test_instrument_connection_uses_async_cursor_factory(self): query = b"SELECT * FROM test" acnx = await psycopg.AsyncConnection.connect("test") async with acnx: await acnx.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) acnx = PsycopgInstrumentor().instrument_connection(acnx) self.assertTrue(acnx._is_instrumented_by_opentelemetry) # The new cursor_factory should be a subclass of MockAsyncCursor, # the async traced cursor factory returned by _new_cursor_async_factory self.assertTrue(issubclass(acnx.cursor_factory, MockAsyncCursor)) cursor = acnx.cursor() await cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.psycopg) @pytest.mark.skipif( sys.version_info < (3, 14), reason="requires Python 3.14+ for t-strings", ) async def test_t_string_span_attributes_async(self): # pylint: disable-next=import-outside-toplevel,no-name-in-module,no-member from string.templatelib import Interpolation, Template # noqa: PLC0415 PsycopgInstrumentor().instrument() cnx = await psycopg.AsyncConnection.connect("test") self.assertTrue(issubclass(cnx.cursor_factory, MockAsyncCursor)) async with cnx.cursor() as cursor: value = 42 template = Template("SELECT ", Interpolation(value, "value"), " FROM foo") await cursor.execute(template) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "SELECT") self.assertEqual(span.attributes["db.statement"], "SELECT {value} FROM foo") async def test_semconv_stable_async(self): """database,http opt-in emits only stable attributes.""" with ( use_semconv_opt_in("database,http"), mock.patch( "psycopg.AsyncConnection.connect", MockAsyncConnectionWithInfo.connect, ), ): PsycopgInstrumentor().instrument() acnx = await psycopg.AsyncConnection.connect("test") async with acnx as cnx: async with cnx.cursor() as cursor: await cursor.execute("SELECT * FROM test") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_SYSTEM_NAME], "postgresql") self.assertEqual(span.attributes[DB_NAMESPACE], "test") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT * FROM test") self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost") self.assertEqual(span.attributes[SERVER_PORT], 5432) self.assertNotIn(DB_SYSTEM, span.attributes) self.assertNotIn(DB_NAME, span.attributes) self.assertNotIn(DB_STATEMENT, span.attributes) self.assertNotIn(DB_USER, span.attributes) self.assertNotIn(NET_PEER_NAME, span.attributes) self.assertNotIn(NET_PEER_PORT, span.attributes) async def test_semconv_dup_async(self): """database/dup,http/dup opt-in emits both legacy and stable attributes.""" with ( use_semconv_opt_in("database/dup,http/dup"), mock.patch( "psycopg.AsyncConnection.connect", MockAsyncConnectionWithInfo.connect, ), ): PsycopgInstrumentor().instrument() acnx = await psycopg.AsyncConnection.connect("test") async with acnx as cnx: async with cnx.cursor() as cursor: await cursor.execute("SELECT * FROM test") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_SYSTEM], "postgresql") self.assertEqual(span.attributes[DB_SYSTEM_NAME], "postgresql") self.assertEqual(span.attributes[DB_NAME], "test") self.assertEqual(span.attributes[DB_NAMESPACE], "test") self.assertEqual(span.attributes[DB_STATEMENT], "SELECT * FROM test") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT * FROM test") self.assertEqual(span.attributes[DB_USER], "testuser") self.assertEqual(span.attributes[NET_PEER_NAME], "localhost") self.assertEqual(span.attributes[NET_PEER_PORT], 5432) self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost") self.assertEqual(span.attributes[SERVER_PORT], 5432) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg2/000077500000000000000000000000001527070072300327115ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg2/LICENSE000066400000000000000000000261351527070072300337250ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000012471527070072300343250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg2OpenTelemetry Psycopg2 Instrumentation ====================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-psycopg2.svg :target: https://pypi.org/project/opentelemetry-instrumentation-psycopg2/ Installation ------------ :: pip install opentelemetry-instrumentation-psycopg2 References ---------- * `OpenTelemetry Psycopg2 Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000032331527070072300355470ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg2[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-psycopg2" dynamic = ["version"] description = "OpenTelemetry psycopg2 instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-instrumentation-dbapi == 0.66b2", ] [project.optional-dependencies] instruments = [] instruments-any = [ "psycopg2 >= 2.7.3.1", "psycopg2-binary >= 2.7.3.1", ] [project.entry-points.opentelemetry_instrumentor] psycopg2 = "opentelemetry.instrumentation.psycopg2:Psycopg2Instrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-psycopg2" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/psycopg2/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg2/src/000077500000000000000000000000001527070072300335005ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300363155ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg2/srcinstrumentation/000077500000000000000000000000001527070072300415605ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetrypsycopg2/000077500000000000000000000000001527070072300433265ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation__init__.py000066400000000000000000000376161527070072300454540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The integration with PostgreSQL supports the `psycopg2`_ library. It can be enabled by using ``Psycopg2Instrumentor``. .. _Psycopg2: https://www.psycopg.org/docs/ Usage ----- .. code-block:: python import psycopg2 from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor # Call instrument() to wrap all database connections Psycopg2Instrumentor().instrument() cnx = psycopg2.connect(database="Database") cursor = cnx.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)") cursor.execute("INSERT INTO test (testField) VALUES (123)") cursor.close() cnx.close() .. code-block:: python import psycopg2 from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor # Alternatively, use instrument_connection for an individual connection cnx = psycopg2.connect(database="Database") instrumented_cnx = Psycopg2Instrumentor().instrument_connection(cnx) cursor = instrumented_cnx.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)") cursor.execute("INSERT INTO test (testField) VALUES (123)") cursor.close() instrumented_cnx.close() Configuration ------------- SQLCommenter ************ You can optionally configure Psycopg2 instrumentation to enable sqlcommenter which enriches the query with contextual information. Queries made after setting up trace integration with sqlcommenter enabled will have configurable key-value pairs appended to them, e.g. ``"select * from auth_users; /*traceparent=00-01234567-abcd-01*/"``. This supports context propagation between database client and server when database log records are enabled. For more information, see: * `Semantic Conventions - Database Spans `_ * `sqlcommenter `_ .. code:: python import psycopg2 from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor Psycopg2Instrumentor().instrument(enable_commenter=True) # OR with specific connection cnx = psycopg2.connect(database='Database') instrumented_cnx = Psycopg2Instrumentor().instrument_connection( cnx, enable_commenter=True, ) SQLCommenter with commenter_options *********************************** The key-value pairs appended to the query can be configured using ``commenter_options``. When sqlcommenter is enabled, all available KVs/tags are calculated by default. ``commenter_options`` supports *opting out* of specific KVs. .. code:: python from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor # Opts into sqlcomment for Psycopg2 trace integration. # Opts out of tags for libpq_version, db_driver. Psycopg2Instrumentor().instrument( enable_commenter=True, commenter_options={ "libpq_version": False, "db_driver": False, } ) Available commenter_options ########################### The following sqlcomment key-values can be opted out of through ``commenter_options``: +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | Commenter Option | Description | Example | +===========================+===========================================================+===========================================================================+ | ``db_driver`` | Database driver name with version. | ``psycopg2='2.9.3'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``dbapi_threadsafety`` | DB-API threadsafety value: 0-3 or unknown. | ``dbapi_threadsafety=2`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``dbapi_level`` | DB-API API level: 1.0, 2.0, or unknown. | ``dbapi_level='2.0'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``driver_paramstyle`` | DB-API paramstyle for SQL statement parameter. | ``driver_paramstyle='pyformat'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``libpq_version`` | PostgreSQL libpq version | ``libpq_version=140001`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``opentelemetry_values`` | OpenTelemetry context as traceparent at time of query. | ``traceparent='00-03afa25236b8cd948fa853d67038ac79-405ff022e8247c46-01'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ SQLComment in span attribute **************************** If sqlcommenter is enabled, you can opt into the inclusion of sqlcomment in the query span ``db.statement`` and/or ``db.query.text`` attribute for your needs. If ``commenter_options`` have been set, the span attribute comment will also be configured by this setting. .. code:: python from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor # Opts into sqlcomment for Psycopg2 trace integration. # Opts into sqlcomment for `db.statement` and/or `db.query.text` span attribute. Psycopg2Instrumentor().instrument( enable_commenter=True, enable_attribute_commenter=True, ) Warning: Capture of sqlcomment in ``db.statement``/``db.query.text`` may have high cardinality without platform normalization. See `Semantic Conventions for database spans `_ for more information. Capture parameters ****************** By default, only statements are captured, without the associated query parameters. To capture query parameters in the span attribute `db.statement.parameters`, enable `capture_parameters`. .. code:: python from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor Psycopg2Instrumentor().instrument( capture_parameters=True, ) API --- """ from __future__ import annotations import logging import threading import typing import weakref from collections.abc import Collection from importlib.metadata import PackageNotFoundError, distribution from typing import Any import psycopg2 from psycopg2.extensions import ( cursor as pg_cursor, # pylint: disable=no-name-in-module ) from psycopg2.sql import Composed # pylint: disable=no-name-in-module from opentelemetry import trace as trace_api from opentelemetry.instrumentation import dbapi from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.psycopg2.package import ( _instruments_any, _instruments_psycopg2, _instruments_psycopg2_binary, ) from opentelemetry.instrumentation.psycopg2.version import __version__ _logger = logging.getLogger(__name__) if typing.TYPE_CHECKING: from psycopg2.extensions import ( # pylint: disable=no-name-in-module connection as PgConnection, ) class Psycopg2Instrumentor(BaseInstrumentor): _CONNECTION_ATTRIBUTES = { "database": "info.dbname", "port": "info.port", "host": "info.host", "user": "info.user", } _DATABASE_SYSTEM = "postgresql" _INSTRUMENTED_CONNECTIONS = weakref.WeakKeyDictionary() _INSTRUMENTED_CONNECTIONS_LOCK = threading.Lock() def instrumentation_dependencies(self) -> Collection[str]: # Determine which package of psycopg2 is installed # Right now there are two packages, psycopg2 and psycopg2-binary # The latter is a binary wheel package that does not require a compiler try: distribution("psycopg2") return (_instruments_psycopg2,) except PackageNotFoundError: pass try: distribution("psycopg2-binary") return (_instruments_psycopg2_binary,) except PackageNotFoundError: pass return _instruments_any def _instrument(self, **kwargs): """Integrate with PostgreSQL Psycopg library. Psycopg: http://initd.org/psycopg/ """ tracer_provider = kwargs.get("tracer_provider") enable_sqlcommenter = kwargs.get("enable_commenter", False) commenter_options = kwargs.get("commenter_options", {}) enable_attribute_commenter = kwargs.get("enable_attribute_commenter", False) capture_parameters = kwargs.get("capture_parameters", False) dbapi.wrap_connect( __name__, psycopg2, "connect", self._DATABASE_SYSTEM, self._CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, db_api_integration_factory=DatabaseApiIntegration, enable_commenter=enable_sqlcommenter, commenter_options=commenter_options, enable_attribute_commenter=enable_attribute_commenter, capture_parameters=capture_parameters, ) def _uninstrument(self, **kwargs): """ "Disable Psycopg2 instrumentation""" dbapi.unwrap_connect(psycopg2, "connect") # TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql @staticmethod def instrument_connection( connection: PgConnection, tracer_provider: trace_api.TracerProvider | None = None, enable_commenter: bool = False, commenter_options: dict[Any, Any] | None = None, enable_attribute_commenter: bool = False, ) -> PgConnection: """Enable instrumentation in a psycopg2 connection. Uses `_INSTRUMENTED_CONNECTIONS` to store the original `cursor_factory` per connection. Args: connection: The psycopg2 connection object to be instrumented. tracer_provider: opentelemetry.trace.TracerProvider, optional The TracerProvider to use for instrumentation. If not specified, the global TracerProvider will be used. enable_commenter: bool, optional Optional flag to enable/disable sqlcommenter (default False). commenter_options: dict, optional Optional configurations for tags to be appended at the sql query. enable_attribute_commenter: Optional flag to enable/disable addition of sqlcomment to span attribute (default False). Requires enable_commenter=True. Returns: An instrumented psycopg2 connection object. """ with Psycopg2Instrumentor._INSTRUMENTED_CONNECTIONS_LOCK: if connection in Psycopg2Instrumentor._INSTRUMENTED_CONNECTIONS: _logger.warning("Attempting to instrument Psycopg connection while already instrumented") return connection original_cursor_factory = connection.cursor_factory connection.cursor_factory = _new_cursor_factory( base_factory=original_cursor_factory, tracer_provider=tracer_provider, enable_commenter=enable_commenter, commenter_options=commenter_options, enable_attribute_commenter=enable_attribute_commenter, ) Psycopg2Instrumentor._INSTRUMENTED_CONNECTIONS[connection] = original_cursor_factory return connection # TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql @staticmethod def uninstrument_connection(connection: PgConnection) -> PgConnection: """Disable instrumentation for a psycopg2 connection. Restores the original `cursor_factory` from `_INSTRUMENTED_CONNECTIONS`. """ with Psycopg2Instrumentor._INSTRUMENTED_CONNECTIONS_LOCK: original_cursor_factory = Psycopg2Instrumentor._INSTRUMENTED_CONNECTIONS.pop(connection, None) connection.cursor_factory = original_cursor_factory return connection # TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql class DatabaseApiIntegration(dbapi.DatabaseApiIntegration): def wrapped_connection( self, connect_method: typing.Callable[..., typing.Any], args: tuple[typing.Any, typing.Any], kwargs: dict[typing.Any, typing.Any], ): """Add object proxy to connection object.""" base_cursor_factory = kwargs.pop("cursor_factory", None) new_factory_kwargs = {"db_api": self} if base_cursor_factory: new_factory_kwargs["base_factory"] = base_cursor_factory kwargs["cursor_factory"] = _new_cursor_factory(**new_factory_kwargs) connection = connect_method(*args, **kwargs) self.get_connection_attributes(connection) return connection class CursorTracer(dbapi.CursorTracer): def get_operation_name(self, cursor, args): if not args: return "" statement = args[0] if isinstance(statement, Composed): statement = statement.as_string(cursor) if isinstance(statement, str): # Strip leading comments so we get the operation name. A statement that # is truthy but has no tokens left (comment-only or whitespace-only) # must not raise IndexError; return an empty operation name instead. tokens = self._leading_comment_remover.sub("", statement).split() return tokens[0] if tokens else "" return "" def get_statement(self, cursor, args): if not args: return "" statement = args[0] if isinstance(statement, Composed): statement = statement.as_string(cursor) return statement def _new_cursor_factory( db_api: dbapi.DatabaseApiIntegration = None, base_factory: type[pg_cursor] | None = None, tracer_provider: trace_api.TracerProvider | None = None, enable_commenter: bool = False, commenter_options: dict[Any, Any] | None = None, enable_attribute_commenter: bool = False, ): if not db_api: db_api = DatabaseApiIntegration( __name__, Psycopg2Instrumentor._DATABASE_SYSTEM, connection_attributes=Psycopg2Instrumentor._CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, enable_commenter=enable_commenter, commenter_options=commenter_options, connect_module=psycopg2, enable_attribute_commenter=enable_attribute_commenter, ) base_factory = base_factory or pg_cursor _cursor_tracer = CursorTracer(db_api) class TracedCursorFactory(base_factory): def execute(self, *args, **kwargs): return _cursor_tracer.traced_execution(self, super().execute, *args, **kwargs) def executemany(self, *args, **kwargs): return _cursor_tracer.traced_execution(self, super().executemany, *args, **kwargs) def callproc(self, *args, **kwargs): return _cursor_tracer.traced_execution(self, super().callproc, *args, **kwargs) return TracedCursorFactory package.py000066400000000000000000000004761527070072300453020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments_psycopg2 = "psycopg2 >= 2.7.3.1" _instruments_psycopg2_binary = "psycopg2-binary >= 2.7.3.1" _instruments = () _instruments_any = ( _instruments_psycopg2, _instruments_psycopg2_binary, ) _semconv_status = "migration" version.py000066400000000000000000000001441527070072300453640ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements-binary.txt000066400000000000000000000005351527070072300403600ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg2asgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 psycopg2-binary==2.9.10 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-dbapi -e instrumentation/opentelemetry-instrumentation-psycopg2 test-requirements.txt000066400000000000000000000005261527070072300370760ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg2asgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 psycopg2==2.9.10 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-dbapi -e instrumentation/opentelemetry-instrumentation-psycopg2 opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg2/tests/000077500000000000000000000000001527070072300340535ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300360730ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg2/teststest_psycopg2_instrumentation.py000066400000000000000000000154631527070072300425070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from importlib.metadata import PackageNotFoundError from unittest import TestCase from unittest.mock import Mock, call, patch import psycopg2 from opentelemetry.instrumentation.auto_instrumentation._load import ( _load_instrumentors, ) from opentelemetry.instrumentation.psycopg2 import ( DatabaseApiIntegration, Psycopg2Instrumentor, ) from opentelemetry.instrumentation.psycopg2.package import ( _instruments_psycopg2, _instruments_psycopg2_binary, ) from opentelemetry.instrumentation.psycopg2.version import __version__ from opentelemetry.test.test_base import TestBase class TestPsycopg2InstrumentationDependencies(TestCase): """ The psycopg2 instrumentation is special in that it can instrument multiple packages. (psycopg2 and psycopg2-binary) The tests below are to verify that the correct package is instrumented depending on which package is installed. Including the cases where both are installed and neither is installed. These tests were added after we found a bug where the auto instrumentation path attempted to use a different dependency check than the manual instrumentation path. This caused the auto instrumentation to fail. """ @patch("opentelemetry.instrumentation.psycopg2.distribution") def test_instrumentation_dependencies_psycopg2_installed(self, mock_distribution) -> None: instrumentation = Psycopg2Instrumentor() def _distribution(name): if name == "psycopg2": return raise PackageNotFoundError mock_distribution.side_effect = _distribution package_to_instrument = instrumentation.instrumentation_dependencies() self.assertEqual(mock_distribution.call_count, 1) self.assertEqual( mock_distribution.mock_calls, [ call("psycopg2"), ], ) self.assertEqual(package_to_instrument, (_instruments_psycopg2,)) @patch("opentelemetry.instrumentation.psycopg2.distribution") def test_instrumentation_dependencies_psycopg2_binary_installed(self, mock_distribution) -> None: instrumentation = Psycopg2Instrumentor() def _distribution(name): if name == "psycopg2-binary": return raise PackageNotFoundError mock_distribution.side_effect = _distribution package_to_instrument = instrumentation.instrumentation_dependencies() self.assertEqual(mock_distribution.call_count, 2) self.assertEqual( mock_distribution.mock_calls, [ call("psycopg2"), call("psycopg2-binary"), ], ) self.assertEqual(package_to_instrument, (_instruments_psycopg2_binary,)) @patch("opentelemetry.instrumentation.psycopg2.distribution") def test_instrumentation_dependencies_both_installed(self, mock_distribution) -> None: instrumentation = Psycopg2Instrumentor() def _distribution(name): # The function returns None here for all names # to simulate both packages being installed return None mock_distribution.side_effect = _distribution package_to_instrument = instrumentation.instrumentation_dependencies() self.assertEqual(mock_distribution.call_count, 1) self.assertEqual(mock_distribution.mock_calls, [call("psycopg2")]) self.assertEqual(package_to_instrument, (_instruments_psycopg2,)) @patch("opentelemetry.instrumentation.psycopg2.distribution") def test_instrumentation_dependencies_none_installed(self, mock_distribution) -> None: instrumentation = Psycopg2Instrumentor() def _distribution(name): # Function raises PackageNotFoundError # if name is not in the list. We will # raise it for both names to simulate # neither being installed raise PackageNotFoundError mock_distribution.side_effect = _distribution package_to_instrument = instrumentation.instrumentation_dependencies() self.assertEqual(mock_distribution.call_count, 2) self.assertEqual( mock_distribution.mock_calls, [ call("psycopg2"), call("psycopg2-binary"), ], ) self.assertEqual( package_to_instrument, ( _instruments_psycopg2, _instruments_psycopg2_binary, ), ) # This test is to verify that the auto instrumentation path # will auto instrument psycopg2 or psycopg2-binary is installed. # Note there is only one test here but it is run twice in tox # once with the psycopg2 package installed and once with # psycopg2-binary installed. @patch("opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts") @patch("opentelemetry.instrumentation.auto_instrumentation._load._logger") def test_instruments_with_psycopg2_installed(self, mock_logger, mock_dep): def _instrumentation_loaded_successfully_call(): return call("Instrumented %s", "psycopg2") mock_distro = Mock() mock_dep.return_value = None mock_distro.load_instrumentor.return_value = None _load_instrumentors(mock_distro) self.assertEqual(len(mock_distro.load_instrumentor.call_args_list), 1) (ep,) = mock_distro.load_instrumentor.call_args.args self.assertEqual(ep.name, "psycopg2") mock_logger.debug.assert_has_calls([_instrumentation_loaded_successfully_call()]) @patch("opentelemetry.instrumentation.psycopg2.dbapi") class TestPsycopg2InstrumentorParameters(TestBase): def tearDown(self): super().tearDown() with self.disable_logging(): Psycopg2Instrumentor().uninstrument() def test_instrument_defaults(self, mock_dbapi): # pylint: disable=no-self-use Psycopg2Instrumentor().instrument() mock_dbapi.wrap_connect.assert_called_once_with( "opentelemetry.instrumentation.psycopg2", psycopg2, "connect", "postgresql", { "database": "info.dbname", "port": "info.port", "host": "info.host", "user": "info.user", }, version=__version__, tracer_provider=None, db_api_integration_factory=DatabaseApiIntegration, enable_commenter=False, commenter_options={}, enable_attribute_commenter=False, capture_parameters=False, ) def test_instrument_capture_parameters(self, mock_dbapi): Psycopg2Instrumentor().instrument(capture_parameters=True) self.assertTrue(mock_dbapi.wrap_connect.call_args.kwargs["capture_parameters"]) test_psycopg2_integration.py000066400000000000000000000512421527070072300415620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-psycopg2/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import types from unittest import mock import psycopg2 import opentelemetry.instrumentation.psycopg2 from opentelemetry import trace from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor from opentelemetry.sdk import resources from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_STATEMENT, ) from opentelemetry.test.test_base import TestBase class MockCursor: execute = mock.MagicMock(spec=types.MethodType) execute.__name__ = "execute" executemany = mock.MagicMock(spec=types.MethodType) executemany.__name__ = "executemany" callproc = mock.MagicMock(spec=types.MethodType) callproc.__name__ = "callproc" rowcount = "SomeRowCount" def __init__(self, *args, **kwargs): pass def __enter__(self): return self def __exit__(self, *args): return self class MockConnection: commit = mock.MagicMock(spec=types.MethodType) commit.__name__ = "commit" rollback = mock.MagicMock(spec=types.MethodType) rollback.__name__ = "rollback" def __init__(self, *args, **kwargs): self.cursor_factory = kwargs.pop("cursor_factory", None) def cursor(self): if self.cursor_factory: return self.cursor_factory(self) return MockCursor() def get_dsn_parameters(self): # pylint: disable=no-self-use return {"dbname": "test"} # pylint: disable=too-many-public-methods class TestPostgresqlIntegration(TestBase): def setUp(self): super().setUp() self.cursor_mock = mock.patch("opentelemetry.instrumentation.psycopg2.pg_cursor", MockCursor) self.connection_mock = mock.patch("psycopg2.connect", MockConnection) self.cursor_mock.start() self.connection_mock.start() def tearDown(self): super().tearDown() self.memory_exporter.clear() self.cursor_mock.stop() self.connection_mock.stop() with self.disable_logging(): Psycopg2Instrumentor().uninstrument() # pylint: disable=unused-argument def test_instrumentor(self): Psycopg2Instrumentor().instrument() cnx = psycopg2.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.psycopg2) # check that no spans are generated after uninstrument Psycopg2Instrumentor().uninstrument() cnx = psycopg2.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) def test_span_name(self): Psycopg2Instrumentor().instrument() cnx = psycopg2.connect(database="test") cursor = cnx.cursor() cursor.execute("Test query", ("param1Value", False)) cursor.execute( """multi line query""" ) cursor.execute("tab\tseparated query") cursor.execute("/* leading comment */ query") cursor.execute("/* leading comment */ query /* trailing comment */") cursor.execute("query /* trailing comment */") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 6) self.assertEqual(spans_list[0].name, "Test") self.assertEqual(spans_list[1].name, "multi") self.assertEqual(spans_list[2].name, "tab") self.assertEqual(spans_list[3].name, "query") self.assertEqual(spans_list[4].name, "query") self.assertEqual(spans_list[5].name, "query") def test_span_name_comment_or_whitespace_only(self): # Regression: a comment-only or whitespace-only statement is truthy but has # no tokens after leading-comment stripping; get_operation_name must not # raise IndexError. Psycopg2Instrumentor().instrument() cnx = psycopg2.connect(database="test") cursor = cnx.cursor() cursor.execute("/* comment only */") cursor.execute(" ") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 2) # No tokens survive comment/whitespace stripping, so operation_name is # empty and the span name falls back to the db vendor. self.assertEqual(spans_list[0].name, "postgresql") self.assertEqual(spans_list[1].name, "postgresql") # pylint: disable=unused-argument def test_not_recording(self): mock_tracer = mock.Mock() mock_span = mock.Mock() mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span Psycopg2Instrumentor().instrument() with mock.patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer cnx = psycopg2.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) Psycopg2Instrumentor().uninstrument() # pylint: disable=unused-argument def test_custom_tracer_provider(self): resource = resources.Resource.create({}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result Psycopg2Instrumentor().instrument(tracer_provider=tracer_provider) cnx = psycopg2.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertIs(span.resource, resource) # pylint: disable=unused-argument def test_instrument_connection(self): cnx = psycopg2.connect(database="test") query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) cnx = Psycopg2Instrumentor().instrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) # pylint: disable=unused-argument def test_instrument_connection_with_instrument(self): cnx = psycopg2.connect(database="test") query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) Psycopg2Instrumentor().instrument() cnx = Psycopg2Instrumentor().instrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) # pylint: disable=unused-argument def test_instrument_connection_is_idempotent(self): cnx = psycopg2.connect(database="test") query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) instrumentor = Psycopg2Instrumentor() cnx = instrumentor.instrument_connection(cnx) cnx = instrumentor.instrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) def test_instrument_connection_with_custom_cursor_factory_instrument_then_uninstrument( self, ): instrumentor = Psycopg2Instrumentor() cnx = psycopg2.connect(database="test", cursor_factory=MockCursor) query = "SELECT * FROM test" self.assertIs(cnx.cursor_factory, MockCursor) cnx = instrumentor.instrument_connection(cnx) self.assertIsNot(cnx.cursor_factory, MockCursor) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) cnx = instrumentor.uninstrument_connection(cnx) self.assertIs(cnx.cursor_factory, MockCursor) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) def test_uninstrument_connection_is_idempotent(self): instrumentor = Psycopg2Instrumentor() cnx = psycopg2.connect(database="test") query = "SELECT * FROM test" cnx = instrumentor.instrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) cnx = instrumentor.uninstrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) cnx = instrumentor.uninstrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) def test_instrument_connection_reinstrument_after_uninstrument(self): instrumentor = Psycopg2Instrumentor() cnx = psycopg2.connect(database="test") query = "SELECT * FROM test" cnx = instrumentor.instrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) cnx = instrumentor.uninstrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) cnx = instrumentor.instrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 2) # pylint: disable=unused-argument def test_uninstrument_connection_with_instrument(self): Psycopg2Instrumentor().instrument() cnx = psycopg2.connect(database="test") query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) cnx = Psycopg2Instrumentor().uninstrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) # pylint: disable=unused-argument def test_uninstrument_connection_with_instrument_connection(self): cnx = psycopg2.connect(database="test") Psycopg2Instrumentor().instrument_connection(cnx) query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) cnx = Psycopg2Instrumentor().uninstrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) @mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect") def test_sqlcommenter_enabled(self, event_mocked): cnx = psycopg2.connect(database="test") Psycopg2Instrumentor().instrument(enable_commenter=True) query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) kwargs = event_mocked.call_args[1] self.assertEqual(kwargs["enable_commenter"], True) def test_sqlcommenter_enabled_instrument_connection_defaults(self): with ( mock.patch( "opentelemetry.instrumentation.psycopg2.psycopg2.__version__", "foobar", ), mock.patch( "opentelemetry.instrumentation.psycopg2.psycopg2.__libpq_version__", "foobaz", ), mock.patch( "opentelemetry.instrumentation.psycopg2.psycopg2.threadsafety", "123", ), mock.patch( "opentelemetry.instrumentation.psycopg2.psycopg2.apilevel", "123", ), mock.patch( "opentelemetry.instrumentation.psycopg2.psycopg2.paramstyle", "test", ), ): cnx = psycopg2.connect(database="test") cnx = Psycopg2Instrumentor().instrument_connection( cnx, enable_commenter=True, ) query = "Select 1" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = int(span.get_span_context().trace_flags) self.assertEqual( MockCursor.execute.call_args[0][0], f"Select 1 /*db_driver='psycopg2%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',libpq_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags:02x}'*/", ) self.assertEqual( span.attributes[DB_STATEMENT], "Select 1", ) def test_sqlcommenter_enabled_instrument_connection_stmt_enabled(self): with ( mock.patch( "opentelemetry.instrumentation.psycopg2.psycopg2.__version__", "foobar", ), mock.patch( "opentelemetry.instrumentation.psycopg2.psycopg2.__libpq_version__", "foobaz", ), mock.patch( "opentelemetry.instrumentation.psycopg2.psycopg2.threadsafety", "123", ), mock.patch( "opentelemetry.instrumentation.psycopg2.psycopg2.apilevel", "123", ), mock.patch( "opentelemetry.instrumentation.psycopg2.psycopg2.paramstyle", "test", ), ): cnx = psycopg2.connect(database="test") cnx = Psycopg2Instrumentor().instrument_connection( cnx, enable_commenter=True, enable_attribute_commenter=True, ) query = "Select 1" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = int(span.get_span_context().trace_flags) self.assertEqual( MockCursor.execute.call_args[0][0], f"Select 1 /*db_driver='psycopg2%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',libpq_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags:02x}'*/", ) self.assertEqual( span.attributes[DB_STATEMENT], f"Select 1 /*db_driver='psycopg2%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',libpq_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags:02x}'*/", ) def test_sqlcommenter_enabled_instrument_connection_with_options(self): with ( mock.patch( "opentelemetry.instrumentation.psycopg2.psycopg2.__version__", "foobar", ), mock.patch( "opentelemetry.instrumentation.psycopg2.psycopg2.__libpq_version__", "foobaz", ), mock.patch( "opentelemetry.instrumentation.psycopg2.psycopg2.threadsafety", "123", ), ): cnx = psycopg2.connect(database="test") cnx = Psycopg2Instrumentor().instrument_connection( cnx, enable_commenter=True, commenter_options={ "dbapi_level": False, "dbapi_threadsafety": True, "driver_paramstyle": False, "foo": "ignored", }, ) query = "Select 1" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = int(span.get_span_context().trace_flags) self.assertEqual( MockCursor.execute.call_args[0][0], f"Select 1 /*db_driver='psycopg2%%3Afoobar',dbapi_threadsafety='123',libpq_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags:02x}'*/", ) self.assertEqual( span.attributes[DB_STATEMENT], "Select 1", ) @mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect") def test_sqlcommenter_disabled(self, event_mocked): cnx = psycopg2.connect(database="test") Psycopg2Instrumentor().instrument() query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) kwargs = event_mocked.call_args[1] self.assertEqual(kwargs["enable_commenter"], False) def test_sqlcommenter_disabled_default_instrument_connection(self): cnx = psycopg2.connect(database="test") cnx = Psycopg2Instrumentor().instrument_connection( cnx, ) query = "Select 1" cursor = cnx.cursor() cursor.execute(query) self.assertEqual( MockCursor.execute.call_args[0][0], "Select 1", ) spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1", ) def test_sqlcommenter_disabled_explicit_instrument_connection(self): cnx = psycopg2.connect(database="test") cnx = Psycopg2Instrumentor().instrument_connection( cnx, enable_commenter=False, ) query = "Select 1" cursor = cnx.cursor() cursor.execute(query) self.assertEqual( MockCursor.execute.call_args[0][0], "Select 1", ) spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1", ) def test_no_op_tracer_provider(self): Psycopg2Instrumentor().instrument(tracer_provider=trace.NoOpTracerProvider()) cnx = psycopg2.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) def test_span_capture_params_deactivated_by_default(self): Psycopg2Instrumentor().instrument() cnx = psycopg2.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test WHERE id = %s AND name = %s" cursor.execute(query, (42, "John Doe")) spans_list = self.memory_exporter.get_finished_spans() self.assertNotIn("db.statement.parameters", spans_list[0].attributes) def test_span_capture_params_activated(self): Psycopg2Instrumentor().instrument(capture_parameters=True) cnx = psycopg2.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test WHERE id = %s AND name = %s" cursor.execute(query, (42, "John Doe")) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual( spans_list[0].attributes["db.statement.parameters"], "(42, 'John Doe')", ) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcache/000077500000000000000000000000001527070072300332565ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcache/LICENSE000066400000000000000000000261351527070072300342720ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000012671527070072300346740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcacheOpenTelemetry pymemcache Instrumentation ======================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-pymemcache.svg :target: https://pypi.org/project/opentelemetry-instrumentation-pymemcache/ Installation ------------ :: pip install opentelemetry-instrumentation-pymemcache References ---------- * `OpenTelemetry Pymemcache Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000032251527070072300361150ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcache[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-pymemcache" dynamic = ["version"] description = "OpenTelemetry pymemcache instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [ "pymemcache >= 1.3.5, < 5", ] [project.entry-points.opentelemetry_instrumentor] pymemcache = "opentelemetry.instrumentation.pymemcache:PymemcacheInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-pymemcache" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/pymemcache/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcache/src/000077500000000000000000000000001527070072300340455ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300366625ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcache/srcinstrumentation/000077500000000000000000000000001527070072300421255ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetrypymemcache/000077500000000000000000000000001527070072300442405ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation__init__.py000066400000000000000000000145221527070072300463550ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Usage ----- The OpenTelemetry ``pymemcache`` integration traces pymemcache client operations Usage ----- .. code-block:: python from opentelemetry.instrumentation.pymemcache import PymemcacheInstrumentor PymemcacheInstrumentor().instrument() from pymemcache.client.base import Client client = Client(("localhost", 11211)) client.set("some_key", "some_value") API --- """ # pylint: disable=no-value-for-parameter import logging from collections.abc import Collection import pymemcache from wrapt import wrap_function_wrapper as _wrap from opentelemetry.instrumentation._semconv import ( _get_schema_url_for_signal_types, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _set_db_statement, _set_db_system, _set_http_net_peer_name_client, _set_http_peer_port_client, _set_net_transport, ) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.pymemcache.package import _instruments from opentelemetry.instrumentation.pymemcache.version import __version__ from opentelemetry.instrumentation.utils import is_instrumentation_enabled, unwrap from opentelemetry.semconv._incubating.attributes.net_attributes import ( NetTransportValues, ) from opentelemetry.semconv.attributes.network_attributes import ( NetworkTransportValues, ) from opentelemetry.trace import SpanKind, get_tracer logger = logging.getLogger(__name__) COMMANDS = [ "set", "set_many", "add", "replace", "append", "prepend", "cas", "get", "get_many", "gets", "gets_many", "delete", "delete_many", "incr", "decr", "touch", "stats", "version", "flush_all", "quit", "set_multi", "get_multi", ] def _with_tracer_wrapper(func): """Helper for providing tracer for wrapper functions.""" def _with_tracer(tracer, cmd, sem_conv_opt_in_mode): def wrapper(wrapped, instance, args, kwargs): # prevent double wrapping if hasattr(wrapped, "__wrapped__"): return wrapped(*args, **kwargs) return func( tracer, cmd, sem_conv_opt_in_mode, wrapped, instance, args, kwargs, ) return wrapper return _with_tracer @_with_tracer_wrapper def _wrap_cmd(tracer, cmd, sem_conv_opt_in_mode, wrapped, instance, args, kwargs): if not is_instrumentation_enabled(): return wrapped(*args, **kwargs) with tracer.start_as_current_span(cmd, kind=SpanKind.CLIENT, attributes={}) as span: try: if span.is_recording(): if not args: vals = "" else: vals = _get_query_string(args[0]) query = f"{cmd}{' ' if vals else ''}{vals}" attrs = _get_address_attributes(instance, sem_conv_opt_in_mode) _set_db_statement(attrs, query, sem_conv_opt_in_mode) span.set_attributes(attrs) except Exception as ex: # pylint: disable=broad-except logger.warning("Failed to set attributes for pymemcache span %s", str(ex)) return wrapped(*args, **kwargs) def _get_query_string(arg): """Return the query values given the first argument to a pymemcache command. If there are multiple query values, they are joined together space-separated. """ keys = "" if isinstance(arg, dict): arg = list(arg) if isinstance(arg, str): keys = arg elif isinstance(arg, bytes): keys = arg.decode() elif isinstance(arg, list) and len(arg) >= 1: if isinstance(arg[0], str): keys = " ".join(arg) elif isinstance(arg[0], bytes): keys = b" ".join(arg).decode() return keys def _get_address_attributes(instance, sem_conv_opt_in_mode): """Attempt to get host and port from Client instance.""" address_attributes = {} _set_db_system(address_attributes, "memcached", sem_conv_opt_in_mode) # client.base.Client contains server attribute which is either a host/port tuple, or unix socket path string # https://github.com/pinterest/pymemcache/blob/f02ddf73a28c09256589b8afbb3ee50f1171cac7/pymemcache/client/base.py#L228 if hasattr(instance, "server"): if isinstance(instance.server, tuple): host, port = instance.server _set_http_net_peer_name_client(address_attributes, host, sem_conv_opt_in_mode) _set_http_peer_port_client(address_attributes, port, sem_conv_opt_in_mode) _set_net_transport( address_attributes, NetTransportValues.IP_TCP.value, NetworkTransportValues.TCP.value, sem_conv_opt_in_mode, ) elif isinstance(instance.server, str): _set_http_net_peer_name_client(address_attributes, instance.server, sem_conv_opt_in_mode) _set_net_transport( address_attributes, NetTransportValues.OTHER.value, NetworkTransportValues.PIPE.value, sem_conv_opt_in_mode, ) return address_attributes class PymemcacheInstrumentor(BaseInstrumentor): """An instrumentor for pymemcache See `BaseInstrumentor`""" def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): _OpenTelemetrySemanticConventionStability._initialize() sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.DATABASE, ) tracer_provider = kwargs.get("tracer_provider") tracer = get_tracer( __name__, __version__, tracer_provider, schema_url=_get_schema_url_for_signal_types([_OpenTelemetryStabilitySignalType.DATABASE]), ) for cmd in COMMANDS: _wrap( "pymemcache.client.base", f"Client.{cmd}", _wrap_cmd(tracer, cmd, sem_conv_opt_in_mode), ) def _uninstrument(self, **kwargs): for command in COMMANDS: unwrap(pymemcache.client.base.Client, f"{command}") package.py000066400000000000000000000002311527070072300462010ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("pymemcache >= 1.3.5, < 5",) _semconv_status = "migration" version.py000066400000000000000000000001441527070072300462760ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements-0.txt000066400000000000000000000004561527070072300376020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcacheasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pymemcache==1.3.5 pytest==7.4.4 six==1.16.0 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-pymemcache test-requirements-1.txt000066400000000000000000000004561527070072300376030ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcacheasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pymemcache==2.2.2 pytest==7.4.4 six==1.16.0 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-pymemcache test-requirements-2.txt000066400000000000000000000004561527070072300376040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcacheasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pymemcache==3.4.1 pytest==7.4.4 six==1.16.0 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-pymemcache test-requirements-3.txt000066400000000000000000000004561527070072300376050ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcacheasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pymemcache==3.4.2 pytest==7.4.4 six==1.16.0 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-pymemcache test-requirements-4.txt000066400000000000000000000004421527070072300376010ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcacheasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pymemcache==4.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-pymemcache opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcache/tests/000077500000000000000000000000001527070072300344205ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300364400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcache/teststest_pymemcache.py000066400000000000000000000567031527070072300401000ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcache/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import mock import pymemcache from packaging import version as get_package_version from pymemcache import __version__ as pymemcache_package_version from pymemcache.exceptions import ( MemcacheClientError, MemcacheIllegalInputError, MemcacheServerError, MemcacheUnknownCommandError, MemcacheUnknownError, ) from opentelemetry import trace as trace_api from opentelemetry.instrumentation._semconv import ( _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.pymemcache import PymemcacheInstrumentor from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_STATEMENT, DB_SYSTEM, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_QUERY_TEXT, DB_SYSTEM_NAME, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import get_tracer from .utils import MockSocket, _str TEST_HOST = "localhost" TEST_PORT = 117711 pymemcache_version = get_package_version.parse(pymemcache_package_version) # In pymemcache versions greater than 2, set_multi, set_many and delete_multi # now use a batched call # https://github.com/pinterest/pymemcache/blob/master/ChangeLog.rst#new-in-version-200 pymemcache_version_lt_2 = pymemcache_version < get_package_version.parse("2.0.0") # In pymemcache versions greater than 3.4.1, the stats command no # longer sends trailing whitespace in the command # https://github.com/pinterest/pymemcache/blob/master/ChangeLog.rst#new-in-version-342 pymemcache_version_gt_341 = pymemcache_version > get_package_version.parse("3.4.1") class PymemcacheClientTestCase(TestBase): # pylint: disable=too-many-public-methods """Tests for a patched pymemcache.client.base.Client.""" def setUp(self): super().setUp() PymemcacheInstrumentor().instrument() # pylint: disable=protected-access self.tracer = get_tracer(__name__) def tearDown(self): super().tearDown() PymemcacheInstrumentor().uninstrument() def make_client(self, mock_socket_values, **kwargs): self.client = pymemcache.client.base.Client((TEST_HOST, TEST_PORT), **kwargs) self.client.sock = MockSocket(list(mock_socket_values)) return self.client def check_spans(self, spans, num_expected, queries_expected): """A helper for validating basic span information.""" self.assertEqual(num_expected, len(spans)) for span, query in zip(spans, queries_expected): command, *_ = query.split(" ") self.assertEqual(span.name, command) self.assertIs(span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(span.attributes[NET_PEER_NAME], TEST_HOST) self.assertEqual(span.attributes[NET_PEER_PORT], TEST_PORT) self.assertEqual(span.attributes[DB_SYSTEM], "memcached") self.assertEqual(span.attributes[DB_STATEMENT], query) def test_set_success(self): client = self.make_client([b"STORED\r\n"]) result = client.set(b"key", b"value", noreply=False) self.assertTrue(result) spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 1, ["set key"]) def test_suppressed_commands(self) -> None: client = self.make_client([b"STORED\r\n", b"VALUE key 0 5\r\nvalue\r\nEND\r\n", b"STORED\r\n"]) with suppress_instrumentation(): self.assertTrue(client.set(b"key", b"value", noreply=False)) self.assertEqual(client.get_many([b"key"]), {b"key": b"value"}) self.assertEqual(self.memory_exporter.get_finished_spans(), ()) self.assertTrue(client.set(b"key", b"value", noreply=False)) self.check_spans(self.memory_exporter.get_finished_spans(), 1, ["set key"]) def test_suppressed_command_error(self) -> None: error = MemcacheServerError(b"server unavailable") client = self.make_client([error]) with suppress_instrumentation(), self.assertRaises(MemcacheServerError) as raised: client.get(b"key") self.assertIs(raised.exception, error) self.assertEqual(self.memory_exporter.get_finished_spans(), ()) def test_set_not_recording(self): mock_tracer = mock.Mock() mock_span = mock.Mock() mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span with mock.patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer client = self.make_client([b"STORED\r\n"]) result = client.set(b"key", b"value", noreply=False) self.assertTrue(result) self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) def test_get_many_none_found(self): client = self.make_client([b"END\r\n"]) result = client.get_many([b"key1", b"key2"]) assert not result spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 1, ["get_many key1 key2"]) def test_get_multi_none_found(self): client = self.make_client([b"END\r\n"]) # alias for get_many result = client.get_multi([b"key1", b"key2"]) assert not result spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 1, ["get_multi key1 key2"]) def test_set_multi_success(self): client = self.make_client([b"STORED\r\n"]) # Alias for set_many, a convienance function that calls set for every key result = client.set_multi({b"key": b"value"}, noreply=False) spans = self.memory_exporter.get_finished_spans() # In pymemcache versions greater than 2, set_multi now uses a batched call # https://github.com/pinterest/pymemcache/blob/master/ChangeLog.rst#new-in-version-200 if pymemcache_version_lt_2: self.assertTrue(result) self.check_spans(spans, 2, ["set key", "set_multi key"]) else: assert len(result) == 0 self.check_spans(spans, 1, ["set_multi key"]) def test_delete_not_found(self): client = self.make_client([b"NOT_FOUND\r\n"]) result = client.delete(b"key", noreply=False) assert result is False spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 1, ["delete key"]) def test_incr_found(self): client = self.make_client([b"STORED\r\n", b"1\r\n"]) client.set(b"key", 0, noreply=False) result = client.incr(b"key", 1, noreply=False) assert result == 1 spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 2, ["set key", "incr key"]) def test_get_found(self): client = self.make_client([b"STORED\r\n", b"VALUE key 0 5\r\nvalue\r\nEND\r\n"]) result = client.set(b"key", b"value", noreply=False) result = client.get(b"key") assert result == b"value" spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 2, ["set key", "get key"]) def test_decr_found(self): client = self.make_client([b"STORED\r\n", b"1\r\n"]) client.set(b"key", 2, noreply=False) result = client.decr(b"key", 1, noreply=False) assert result == 1 spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 2, ["set key", "decr key"]) def test_add_stored(self): client = self.make_client([b"STORED\r", b"\n"]) result = client.add(b"key", b"value", noreply=False) self.assertTrue(result) spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 1, ["add key"]) def test_delete_many_found(self): client = self.make_client([b"STORED\r", b"\n", b"DELETED\r\n"]) result = client.add(b"key", b"value", noreply=False) # a convienance function that calls delete for every key result = client.delete_many([b"key"], noreply=False) self.assertTrue(result) spans = self.memory_exporter.get_finished_spans() # In pymemcache versions greater than 2, delete_many now uses a batched call # https://github.com/pinterest/pymemcache/blob/master/ChangeLog.rst#new-in-version-200 if pymemcache_version_lt_2: self.check_spans(spans, 3, ["add key", "delete key", "delete_many key"]) else: self.check_spans(spans, 2, ["add key", "delete_many key"]) def test_set_many_success(self): client = self.make_client([b"STORED\r\n"]) # a convienance function that calls set for every key result = client.set_many({b"key": b"value"}, noreply=False) spans = self.memory_exporter.get_finished_spans() # In pymemcache versions greater than 2, set_many now uses a batched call # https://github.com/pinterest/pymemcache/blob/master/ChangeLog.rst#new-in-version-200 if pymemcache_version_lt_2: self.assertTrue(result) self.check_spans(spans, 2, ["set key", "set_many key"]) else: assert len(result) == 0 self.check_spans(spans, 1, ["set_many key"]) def test_set_get(self): client = self.make_client([b"STORED\r\n", b"VALUE key 0 5\r\nvalue\r\nEND\r\n"]) client.set(b"key", b"value", noreply=False) result = client.get(b"key") assert _str(result) == "value" spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) self.assertEqual(spans[0].attributes[NET_PEER_NAME], TEST_HOST) self.assertEqual(spans[0].attributes[NET_PEER_PORT], TEST_PORT) def test_append_stored(self): client = self.make_client([b"STORED\r\n"]) result = client.append(b"key", b"value", noreply=False) self.assertTrue(result) spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 1, ["append key"]) def test_prepend_stored(self): client = self.make_client([b"STORED\r\n"]) result = client.prepend(b"key", b"value", noreply=False) self.assertTrue(result) spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 1, ["prepend key"]) def test_cas_stored(self): client = self.make_client([b"STORED\r\n"]) result = client.cas(b"key", b"value", b"1", noreply=False) self.assertTrue(result) spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 1, ["cas key"]) def test_cas_exists(self): client = self.make_client([b"EXISTS\r\n"]) result = client.cas(b"key", b"value", b"1", noreply=False) assert result is False spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 1, ["cas key"]) def test_cas_not_found(self): client = self.make_client([b"NOT_FOUND\r\n"]) result = client.cas(b"key", b"value", b"1", noreply=False) assert result is None spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 1, ["cas key"]) def test_delete_exception(self): client = self.make_client([Exception("fail")]) def _delete(): client.delete(b"key", noreply=False) with self.assertRaises(Exception): _delete() spans = self.memory_exporter.get_finished_spans() span = spans[0] self.assertFalse(span.status.is_ok) self.check_spans(spans, 1, ["delete key"]) def test_flush_all(self): client = self.make_client([b"OK\r\n"]) result = client.flush_all(noreply=False) self.assertTrue(result) spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 1, ["flush_all"]) def test_incr_exception(self): client = self.make_client([Exception("fail")]) def _incr(): client.incr(b"key", 1) with self.assertRaises(Exception): _incr() spans = self.memory_exporter.get_finished_spans() span = spans[0] self.assertFalse(span.status.is_ok) self.check_spans(spans, 1, ["incr key"]) def test_get_error(self): client = self.make_client([b"ERROR\r\n"]) def _get(): client.get(b"key") with self.assertRaises(MemcacheUnknownCommandError): _get() spans = self.memory_exporter.get_finished_spans() span = spans[0] self.assertFalse(span.status.is_ok) self.check_spans(spans, 1, ["get key"]) def test_get_unknown_error(self): client = self.make_client([b"foobarbaz\r\n"]) def _get(): client.get(b"key") with self.assertRaises(MemcacheUnknownError): _get() spans = self.memory_exporter.get_finished_spans() span = spans[0] self.assertFalse(span.status.is_ok) self.check_spans(spans, 1, ["get key"]) def test_gets_found(self): client = self.make_client([b"VALUE key 0 5 10\r\nvalue\r\nEND\r\n"]) result = client.gets(b"key") assert result == (b"value", b"10") spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 1, ["gets key"]) def test_touch_not_found(self): client = self.make_client([b"NOT_FOUND\r\n"]) result = client.touch(b"key", noreply=False) assert result is False spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 1, ["touch key"]) def test_set_client_error(self): client = self.make_client([b"CLIENT_ERROR some message\r\n"]) def _set(): client.set("key", "value", noreply=False) with self.assertRaises(MemcacheClientError): _set() spans = self.memory_exporter.get_finished_spans() span = spans[0] self.assertFalse(span.status.is_ok) self.check_spans(spans, 1, ["set key"]) def test_set_server_error(self): client = self.make_client([b"SERVER_ERROR some message\r\n"]) def _set(): client.set(b"key", b"value", noreply=False) with self.assertRaises(MemcacheServerError): _set() spans = self.memory_exporter.get_finished_spans() span = spans[0] self.assertFalse(span.status.is_ok) self.check_spans(spans, 1, ["set key"]) def test_set_key_with_space(self): client = self.make_client([b""]) def _set(): client.set(b"key has space", b"value", noreply=False) with self.assertRaises(MemcacheIllegalInputError): _set() spans = self.memory_exporter.get_finished_spans() span = spans[0] self.assertFalse(span.status.is_ok) self.check_spans(spans, 1, ["set key has space"]) def test_quit(self): client = self.make_client([]) assert client.quit() is None spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 1, ["quit"]) def test_replace_not_stored(self): client = self.make_client([b"NOT_STORED\r\n"]) result = client.replace(b"key", b"value", noreply=False) assert result is False spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 1, ["replace key"]) def test_version_success(self): client = self.make_client([b"VERSION 1.2.3\r\n"], default_noreply=False) result = client.version() assert result == b"1.2.3" spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 1, ["version"]) def test_stats(self): client = self.make_client([b"STAT fake_stats 1\r\n", b"END\r\n"]) result = client.stats() # In pymemcache versions greater than 3.4.1, the stats command no # longer sends trailing whitespace in the command # https://github.com/pinterest/pymemcache/blob/master/ChangeLog.rst#new-in-version-342 if pymemcache_version_gt_341: assert client.sock.send_bufs == [b"stats\r\n"] else: assert client.sock.send_bufs == [b"stats \r\n"] assert result == {b"fake_stats": 1} spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 1, ["stats"]) def test_new_semconv(self): PymemcacheInstrumentor().uninstrument() with mock.patch.dict( "os.environ", {"OTEL_SEMCONV_STABILITY_OPT_IN": "database"}, ): _OpenTelemetrySemanticConventionStability._initialized = False PymemcacheInstrumentor().instrument() client = self.make_client([b"STORED\r\n"]) client.set(b"key", b"value", noreply=False) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes[DB_SYSTEM_NAME], "memcached") self.assertEqual(span.attributes[DB_QUERY_TEXT], "set key") self.assertEqual(span.attributes[SERVER_ADDRESS], TEST_HOST) self.assertEqual(span.attributes[SERVER_PORT], TEST_PORT) self.assertNotIn(DB_SYSTEM, span.attributes) self.assertNotIn(DB_STATEMENT, span.attributes) self.assertNotIn(NET_PEER_NAME, span.attributes) self.assertNotIn(NET_PEER_PORT, span.attributes) self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.25.0", ) _OpenTelemetrySemanticConventionStability._initialized = False def test_dup_semconv(self): PymemcacheInstrumentor().uninstrument() with mock.patch.dict( "os.environ", {"OTEL_SEMCONV_STABILITY_OPT_IN": "database/dup"}, ): _OpenTelemetrySemanticConventionStability._initialized = False PymemcacheInstrumentor().instrument() client = self.make_client([b"STORED\r\n"]) client.set(b"key", b"value", noreply=False) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes[DB_SYSTEM_NAME], "memcached") self.assertEqual(span.attributes[DB_QUERY_TEXT], "set key") self.assertEqual(span.attributes[SERVER_ADDRESS], TEST_HOST) self.assertEqual(span.attributes[SERVER_PORT], TEST_PORT) self.assertEqual(span.attributes[DB_SYSTEM], "memcached") self.assertEqual(span.attributes[DB_STATEMENT], "set key") self.assertEqual(span.attributes[NET_PEER_NAME], TEST_HOST) self.assertEqual(span.attributes[NET_PEER_PORT], TEST_PORT) self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.25.0", ) _OpenTelemetrySemanticConventionStability._initialized = False def test_default_semconv(self): client = self.make_client([b"STORED\r\n"]) client.set(b"key", b"value", noreply=False) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes[DB_SYSTEM], "memcached") self.assertEqual(span.attributes[DB_STATEMENT], "set key") self.assertEqual(span.attributes[NET_PEER_NAME], TEST_HOST) self.assertEqual(span.attributes[NET_PEER_PORT], TEST_PORT) self.assertNotIn(DB_SYSTEM_NAME, span.attributes) self.assertNotIn(DB_QUERY_TEXT, span.attributes) self.assertNotIn(SERVER_ADDRESS, span.attributes) self.assertNotIn(SERVER_PORT, span.attributes) self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.11.0", ) def test_uninstrumented(self): PymemcacheInstrumentor().uninstrument() client = self.make_client([b"STORED\r\n", b"VALUE key 0 5\r\nvalue\r\nEND\r\n"]) client.set(b"key", b"value", noreply=False) result = client.get(b"key") assert _str(result) == "value" spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) PymemcacheInstrumentor().instrument() def test_no_op_tracer_provider(self): PymemcacheInstrumentor().uninstrument() tracer_provider = trace_api.NoOpTracerProvider() PymemcacheInstrumentor().instrument(tracer_provider=tracer_provider) client = self.make_client([b"STORED\r\n"]) result = client.set(b"key", b"value", noreply=False) self.assertTrue(result) spans = self.memory_exporter.get_finished_spans() assert spans is not None self.assertEqual(len(spans), 0) class PymemcacheHashClientTestCase(TestBase): """Tests for a patched pymemcache.client.hash.HashClient.""" def setUp(self): super().setUp() PymemcacheInstrumentor().instrument() # pylint: disable=protected-access self.tracer = get_tracer(__name__) def tearDown(self): super().tearDown() PymemcacheInstrumentor().uninstrument() def make_client_pool(self, hostname, mock_socket_values, serializer=None, **kwargs): # pylint: disable=no-self-use mock_client = pymemcache.client.base.Client(hostname, serializer=serializer, **kwargs) mock_client.sock = MockSocket(mock_socket_values) client = pymemcache.client.base.PooledClient(hostname, serializer=serializer) client.client_pool = pymemcache.pool.ObjectPool(lambda: mock_client) return mock_client def make_client(self, *mock_socket_values, **kwargs): current_port = TEST_PORT # pylint: disable=import-outside-toplevel from pymemcache.client.hash import HashClient # noqa: PLC0415 self.client = HashClient([], **kwargs) ip = TEST_HOST for vals in mock_socket_values: url_string = f"{ip}:{current_port}" clnt_pool = self.make_client_pool((ip, current_port), vals, **kwargs) self.client.clients[url_string] = clnt_pool self.client.hasher.add_node(url_string) current_port += 1 return self.client def check_spans(self, spans, num_expected, queries_expected): """A helper for validating basic span information.""" self.assertEqual(num_expected, len(spans)) for span, query in zip(spans, queries_expected): command, *_ = query.split(" ") self.assertEqual(span.name, command) self.assertIs(span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(span.attributes[NET_PEER_NAME], TEST_HOST) self.assertEqual(span.attributes[NET_PEER_PORT], TEST_PORT) self.assertEqual(span.attributes[DB_SYSTEM], "memcached") self.assertEqual(span.attributes[DB_STATEMENT], query) def test_delete_many_found(self): client = self.make_client([b"STORED\r", b"\n", b"DELETED\r\n"]) result = client.add(b"key", b"value", noreply=False) result = client.delete_many([b"key"], noreply=False) self.assertTrue(result) spans = self.memory_exporter.get_finished_spans() self.check_spans(spans, 2, ["add key", "delete key"]) utils.py000066400000000000000000000032721527070072300360570ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymemcache/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import collections import socket class MockSocket: def __init__(self, recv_bufs, connect_failure=None): self.recv_bufs = collections.deque(recv_bufs) self.send_bufs = [] self.closed = False self.timeouts = [] self.connect_failure = connect_failure self.connections = [] self.socket_options = [] def sendall(self, value): self.send_bufs.append(value) def close(self): self.closed = True def recv(self, size): # pylint: disable=unused-argument value = self.recv_bufs.popleft() if isinstance(value, Exception): raise value return value def settimeout(self, timeout): self.timeouts.append(timeout) def connect(self, server): if isinstance(self.connect_failure, Exception): raise self.connect_failure self.connections.append(server) def setsockopt(self, level, option, value): self.socket_options.append((level, option, value)) class MockSocketModule: def __init__(self, connect_failure=None): self.connect_failure = connect_failure self.sockets = [] def socket(self): mock_socket = MockSocket([], connect_failure=self.connect_failure) self.sockets.append(mock_socket) return mock_socket def __getattr__(self, name): return getattr(socket, name) # Compatibility to get a string back from a request def _str(string_input): if isinstance(string_input, str): return string_input if isinstance(string_input, bytes): return string_input.decode() return str(string_input) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymongo/000077500000000000000000000000001527070072300326335ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymongo/LICENSE000066400000000000000000000261351527070072300336470ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymongo/README.rst000066400000000000000000000012401527070072300343170ustar00rootroot00000000000000OpenTelemetry pymongo Instrumentation ===================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-pymongo.svg :target: https://pypi.org/project/opentelemetry-instrumentation-pymongo/ Installation ------------ :: pip install opentelemetry-instrumentation-pymongo References ---------- * `OpenTelemetry pymongo Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031401527070072300354660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymongo[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-pymongo" dynamic = ["version"] description = "OpenTelemetry pymongo instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", ] [project.optional-dependencies] instruments = [ "pymongo >= 3.1, < 5.0", ] [project.entry-points.opentelemetry_instrumentor] pymongo = "opentelemetry.instrumentation.pymongo:PymongoInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-pymongo" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/pymongo/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymongo/src/000077500000000000000000000000001527070072300334225ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300362375ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymongo/srcinstrumentation/000077500000000000000000000000001527070072300415025ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetrypymongo/000077500000000000000000000000001527070072300431725ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation__init__.py000066400000000000000000000300231527070072300453010ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The integration with MongoDB supports the `pymongo`_ library, it can be enabled using the ``PymongoInstrumentor``. .. _pymongo: https://pypi.org/project/pymongo Usage ----- .. code:: python from pymongo import MongoClient from opentelemetry.instrumentation.pymongo import PymongoInstrumentor PymongoInstrumentor().instrument() client = MongoClient() db = client["MongoDB_Database"] collection = db["MongoDB_Collection"] collection.find_one() API --- The `instrument` method accepts the following keyword args: * tracer_provider (``TracerProvider``) - an optional tracer provider * request_hook (``Callable[[Span, CommandStartedEvent], None]``) - a function with extra user-defined logic to be performed before querying mongodb * response_hook (``Callable[[Span, CommandSucceededEvent], None]``) - a function with extra user-defined logic to be performed after the query returns with a successful response * failed_hook (``Callable[[Span, CommandFailedEvent], None]``) - a function with extra user-defined logic to be performed after the query returns with a failed response * capture_statement (``bool``) - an optional value to enable capturing the database statement that is being executed for example: .. code:: python from opentelemetry.instrumentation.pymongo import PymongoInstrumentor from pymongo import MongoClient def request_hook(span, event): # request hook logic pass def response_hook(span, event): # response hook logic pass def failed_hook(span, event): # failed hook logic pass # Instrument pymongo with hooks PymongoInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook, failed_hook=failed_hook) # This will create a span with pymongo specific attributes, including custom attributes added from the hooks client = MongoClient() db = client["MongoDB_Database"] collection = db["MongoDB_Collection"] collection.find_one() """ from __future__ import annotations from collections.abc import Callable, Collection from logging import getLogger from typing import Any, TypeVar from pymongo import monitoring from opentelemetry.instrumentation._semconv import ( _get_schema_url_for_signal_types, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _report_new, _report_old, _set_db_name, _set_db_statement, _set_db_system, _set_http_net_peer_name_client, _set_http_peer_port_client, _StabilityMode, ) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.pymongo.package import _instruments from opentelemetry.instrumentation.pymongo.utils import ( COMMAND_TO_ATTRIBUTE_MAPPING, ) from opentelemetry.instrumentation.pymongo.version import __version__ from opentelemetry.instrumentation.utils import is_instrumentation_enabled from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_MONGODB_COLLECTION, DbSystemValues, ) from opentelemetry.semconv.attributes.db_attributes import DB_COLLECTION_NAME from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.trace import SpanKind, Tracer, get_tracer from opentelemetry.trace.span import Span from opentelemetry.trace.status import Status, StatusCode _LOG = getLogger(__name__) RequestHookT = Callable[[Span, monitoring.CommandStartedEvent], None] ResponseHookT = Callable[[Span, monitoring.CommandSucceededEvent], None] FailedHookT = Callable[[Span, monitoring.CommandFailedEvent], None] CommandEvent = TypeVar( "CommandEvent", monitoring.CommandStartedEvent, monitoring.CommandSucceededEvent, monitoring.CommandFailedEvent, ) def dummy_callback(span: Span, event: CommandEvent): ... class CommandTracer(monitoring.CommandListener): def __init__( self, tracer: Tracer, request_hook: RequestHookT = dummy_callback, response_hook: ResponseHookT = dummy_callback, failed_hook: FailedHookT = dummy_callback, capture_statement: bool = False, semconv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ): self._tracer = tracer self._span_dict = {} self.is_enabled = True self.start_hook = request_hook self.success_hook = response_hook self.failed_hook = failed_hook self.capture_statement = capture_statement self._semconv_opt_in_mode = semconv_opt_in_mode def started(self, event: monitoring.CommandStartedEvent): """Method to handle a pymongo CommandStartedEvent""" if not self.is_enabled or not is_instrumentation_enabled(): return command_name = event.command_name span_name = f"{event.database_name}.{command_name}" statement = self._get_statement_by_command_name(command_name, event) collection = _get_command_collection_name(event) try: span = self._tracer.start_span(span_name, kind=SpanKind.CLIENT) if span.is_recording(): span_attributes: dict = {} _set_db_system( span_attributes, DbSystemValues.MONGODB.value, self._semconv_opt_in_mode, ) _set_db_name( span_attributes, event.database_name, self._semconv_opt_in_mode, ) _set_db_statement( span_attributes, statement, self._semconv_opt_in_mode, ) if collection: if _report_old(self._semconv_opt_in_mode): span_attributes[DB_MONGODB_COLLECTION] = collection if _report_new(self._semconv_opt_in_mode): span_attributes[DB_COLLECTION_NAME] = collection if event.connection_id is not None: _set_http_net_peer_name_client( span_attributes, event.connection_id[0], self._semconv_opt_in_mode, ) _set_http_peer_port_client( span_attributes, event.connection_id[1], self._semconv_opt_in_mode, ) span.set_attributes(span_attributes) try: self.start_hook(span, event) except ( Exception # pylint: disable=broad-exception-caught ) as hook_exception: _LOG.exception(hook_exception) # Add Span to dictionary self._span_dict[_get_span_dict_key(event)] = span except Exception as ex: # pylint: disable=broad-exception-caught if span is not None and span.is_recording(): span.set_status(Status(StatusCode.ERROR, str(ex))) if _report_new(self._semconv_opt_in_mode): span.set_attribute(ERROR_TYPE, type(ex).__qualname__) span.end() self._pop_span(event) def succeeded(self, event: monitoring.CommandSucceededEvent): """Method to handle a pymongo CommandSucceededEvent""" if not self.is_enabled or not is_instrumentation_enabled(): return span = self._pop_span(event) if span is None: return if span.is_recording(): try: self.success_hook(span, event) except ( Exception # pylint: disable=broad-exception-caught ) as hook_exception: _LOG.exception(hook_exception) span.end() def failed(self, event: monitoring.CommandFailedEvent): """Method to handle a pymongo CommandFailedEvent""" if not (self.is_enabled and is_instrumentation_enabled()): return span = self._pop_span(event) if span is None: return if span.is_recording(): span.set_status( Status( StatusCode.ERROR, event.failure.get("errmsg", "Unknown error"), ) ) if _report_new(self._semconv_opt_in_mode): span.set_attribute(ERROR_TYPE, event.failure.get("codeName", "UnknownError")) try: self.failed_hook(span, event) except ( Exception # pylint: disable=broad-exception-caught ) as hook_exception: _LOG.exception(hook_exception) span.end() def _pop_span(self, event: CommandEvent) -> Span | None: return self._span_dict.pop(_get_span_dict_key(event), None) def _get_statement_by_command_name(self, command_name: str, event: CommandEvent) -> str: statement = command_name command_attribute = COMMAND_TO_ATTRIBUTE_MAPPING.get(command_name) command = event.command.get(command_attribute) if command and self.capture_statement: statement += " " + str(command) return statement def _get_command_collection_name(event: CommandEvent) -> str | None: collection_name = event.command.get(event.command_name) if not collection_name or not isinstance(collection_name, str): return None return collection_name def _get_span_dict_key( event: CommandEvent, ) -> int | tuple[int, tuple[str, int | None]]: if event.connection_id is not None: return event.request_id, event.connection_id return event.request_id class PymongoInstrumentor(BaseInstrumentor): _commandtracer_instance: CommandTracer | None = None # The instrumentation for PyMongo is based on the event listener interface # https://api.mongodb.com/python/current/api/pymongo/monitoring.html. # This interface only allows to register listeners and does not provide # an unregister API. In order to provide a mechanishm to disable # instrumentation an enabled flag is implemented in CommandTracer, # it's checked in the different listeners. def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs: Any): """Integrate with pymongo to trace it using event listener. https://api.mongodb.com/python/current/api/pymongo/monitoring.html Args: tracer_provider: The `TracerProvider` to use. If none is passed the current configured one is used. """ tracer_provider = kwargs.get("tracer_provider") request_hook = kwargs.get("request_hook", dummy_callback) response_hook = kwargs.get("response_hook", dummy_callback) failed_hook = kwargs.get("failed_hook", dummy_callback) capture_statement = kwargs.get("capture_statement") # Create and register a CommandTracer only the first time if self._commandtracer_instance is None: _OpenTelemetrySemanticConventionStability._initialize() semconv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.DATABASE, ) tracer = get_tracer( __name__, __version__, tracer_provider, schema_url=_get_schema_url_for_signal_types([_OpenTelemetryStabilitySignalType.DATABASE]), ) self._commandtracer_instance = CommandTracer( tracer, request_hook=request_hook, response_hook=response_hook, failed_hook=failed_hook, capture_statement=capture_statement, semconv_opt_in_mode=semconv_opt_in_mode, ) monitoring.register(self._commandtracer_instance) # If already created, just enable it self._commandtracer_instance.is_enabled = True def _uninstrument(self, **kwargs: Any): if self._commandtracer_instance is not None: self._commandtracer_instance.is_enabled = False package.py000066400000000000000000000002271527070072300451400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("pymongo >= 3.1, < 5.0",) _semconv_status = "migration" py.typed000066400000000000000000000000001527070072300446570ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongoutils.py000066400000000000000000000004151527070072300447040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 COMMAND_TO_ATTRIBUTE_MAPPING = { "insert": "documents", "delete": "deletes", "update": "updates", "find": "filter", "getMore": "collection", "aggregate": "pipeline", } version.py000066400000000000000000000001441527070072300452300ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000004551527070072300370210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymongoasgiref==3.8.1 Deprecated==1.2.14 dnspython==2.6.1 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pymongo==4.6.3 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-pymongo opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymongo/tests/000077500000000000000000000000001527070072300337755ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300360150ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymongo/teststest_pymongo.py000066400000000000000000000453601527070072300370270ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymongo/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import contextlib from unittest import mock from opentelemetry import trace as trace_api from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, _StabilityMode, ) from opentelemetry.instrumentation.pymongo import ( CommandTracer, PymongoInstrumentor, ) from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_MONGODB_COLLECTION, DB_NAME, DB_STATEMENT, DB_SYSTEM, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_COLLECTION_NAME, DB_NAMESPACE, DB_QUERY_TEXT, DB_SYSTEM_NAME, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.test.test_base import TestBase @contextlib.contextmanager def use_semconv_opt_in(sem_conv_mode): env_patch = mock.patch.dict( "os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode}, ) _OpenTelemetrySemanticConventionStability._initialized = False env_patch.start() try: yield finally: env_patch.stop() _OpenTelemetrySemanticConventionStability._initialized = False # pylint: disable=too-many-public-methods class TestPymongo(TestBase): def setUp(self): super().setUp() self.tracer = self.tracer_provider.get_tracer(__name__) self.start_callback = mock.MagicMock() self.success_callback = mock.MagicMock() self.failed_callback = mock.MagicMock() PymongoInstrumentor._instance = None PymongoInstrumentor._commandtracer_instance = None _OpenTelemetrySemanticConventionStability._initialized = False def tearDown(self): super().tearDown() PymongoInstrumentor._instance = None PymongoInstrumentor._commandtracer_instance = None _OpenTelemetrySemanticConventionStability._initialized = False def test_pymongo_instrumentor(self): mock_register = mock.Mock() patch = mock.patch("pymongo.monitoring.register", side_effect=mock_register) with patch: PymongoInstrumentor().instrument() self.assertTrue(mock_register.called) def test_started(self): command_attrs = { "command_name": "find", } command_tracer = CommandTracer(self.tracer, request_hook=self.start_callback) mock_event = MockEvent(command_attrs, ("test.com", "1234"), "test_request_id") command_tracer.started(event=mock_event) # the memory exporter can't be used here because the span isn't ended # yet # pylint: disable=protected-access span = command_tracer._pop_span(mock_event) self.assertIs(span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(span.name, "database_name.find") self.assertEqual(span.attributes[DB_SYSTEM], "mongodb") self.assertEqual(span.attributes[DB_NAME], "database_name") self.assertEqual(span.attributes[DB_STATEMENT], "find") self.assertEqual(span.attributes[NET_PEER_NAME], "test.com") self.assertEqual(span.attributes[NET_PEER_PORT], 1234) self.start_callback.assert_called_once_with(span, mock_event) def test_succeeded(self): mock_event = MockEvent({}) command_tracer = CommandTracer( self.tracer, request_hook=self.start_callback, response_hook=self.success_callback, ) command_tracer.started(event=mock_event) command_tracer.succeeded(event=mock_event) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertIs(span.status.status_code, trace_api.StatusCode.UNSET) self.assertIsNotNone(span.end_time) self.start_callback.assert_called_once() self.success_callback.assert_called_once() def test_not_recording(self): mock_tracer = mock.Mock() mock_span = mock.Mock() mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span mock_event = MockEvent({}) command_tracer = CommandTracer(mock_tracer) command_tracer.started(event=mock_event) command_tracer.succeeded(event=mock_event) self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) def test_suppression_key(self): mock_tracer = mock.Mock() mock_span = mock.Mock() mock_span.is_recording.return_value = True mock_tracer.start_span.return_value = mock_span mock_event = MockEvent({}) mock_event.command.get = mock.Mock() mock_event.command.get.return_value = "dummy" with suppress_instrumentation(): command_tracer = CommandTracer(mock_tracer) command_tracer.started(event=mock_event) command_tracer.succeeded(event=mock_event) # if suppression key is set, CommandTracer methods return immediately, so command.get is not invoked. self.assertFalse( mock_event.command.get.called # pylint: disable=no-member ) def test_failed(self): mock_event = MockEvent({}) command_tracer = CommandTracer( self.tracer, request_hook=self.start_callback, failed_hook=self.failed_callback, ) command_tracer.started(event=mock_event) mock_event.mark_as_failed() command_tracer.failed(event=mock_event) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertIs( span.status.status_code, trace_api.StatusCode.ERROR, ) self.assertEqual(span.status.description, "operation failed") self.assertIsNotNone(span.end_time) self.start_callback.assert_called_once() self.failed_callback.assert_called_once() def test_multiple_commands(self): first_mock_event = MockEvent({}, ("firstUrl", "123"), "first") second_mock_event = MockEvent({}, ("secondUrl", "456"), "second") command_tracer = CommandTracer(self.tracer) command_tracer.started(event=first_mock_event) command_tracer.started(event=second_mock_event) command_tracer.succeeded(event=first_mock_event) second_mock_event.mark_as_failed() command_tracer.failed(event=second_mock_event) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 2) first_span = spans_list[0] second_span = spans_list[1] self.assertIs( first_span.status.status_code, trace_api.StatusCode.UNSET, ) self.assertIs( second_span.status.status_code, trace_api.StatusCode.ERROR, ) def test_int_command(self): command_attrs = { "command_name": 123, } mock_event = MockEvent(command_attrs) command_tracer = CommandTracer(self.tracer) command_tracer.started(event=mock_event) command_tracer.succeeded(event=mock_event) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "database_name.123") def test_no_op_tracer(self): mock_event = MockEvent({}) tracer = trace_api.NoOpTracer() command_tracer = CommandTracer(tracer) command_tracer.started(event=mock_event) command_tracer.succeeded(event=mock_event) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) def test_capture_statement_getmore(self): command_attrs = { "command_name": "getMore", "collection": "test_collection", } mock_event = MockEvent(command_attrs) command_tracer = CommandTracer(self.tracer, capture_statement=True) command_tracer.started(event=mock_event) command_tracer.succeeded(event=mock_event) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "getMore test_collection", ) def test_capture_statement_aggregate(self): pipeline = [ {"$match": {"status": "active"}}, {"$group": {"_id": "$category", "count": {"$sum": 1}}}, ] command_attrs = { "command_name": "aggregate", "pipeline": pipeline, } command_tracer = CommandTracer(self.tracer, capture_statement=True) mock_event = MockEvent(command_attrs) command_tracer.started(event=mock_event) command_tracer.succeeded(event=mock_event) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] expected_statement = f"aggregate {pipeline}" self.assertEqual(span.attributes[DB_STATEMENT], expected_statement) def test_capture_statement_disabled_getmore(self): command_attrs = { "command_name": "getMore", "collection": "test_collection", } command_tracer = CommandTracer(self.tracer, capture_statement=False) mock_event = MockEvent(command_attrs) command_tracer.started(event=mock_event) command_tracer.succeeded(event=mock_event) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_STATEMENT], "getMore") def test_capture_statement_disabled_aggregate(self): pipeline = [{"$match": {"status": "active"}}] command_attrs = { "command_name": "aggregate", "pipeline": pipeline, } command_tracer = CommandTracer(self.tracer, capture_statement=False) mock_event = MockEvent(command_attrs) command_tracer.started(event=mock_event) command_tracer.succeeded(event=mock_event) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_STATEMENT], "aggregate") def test_collection_name_attribute(self): scenarios = [ ( { "command_name": "find", "find": "test_collection", }, "test_collection", ), ({"command_name": "find"}, None), ({"command_name": "find", "find": b"invalid"}, None), ] for command_attrs, expected in scenarios: with self.subTest(command_attrs=command_attrs, expected=expected): mock_event = MockEvent(command_attrs) command_tracer = CommandTracer(self.tracer, capture_statement=True) command_tracer.started(event=mock_event) command_tracer.succeeded(event=mock_event) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_STATEMENT], "find") self.assertEqual( span.attributes.get(DB_MONGODB_COLLECTION), expected, ) self.memory_exporter.clear() def test_schema_url_default(self): with mock.patch("pymongo.monitoring.register"): instrumentor = PymongoInstrumentor() instrumentor.instrument(tracer_provider=self.tracer_provider) self.assertEqual( instrumentor._commandtracer_instance._tracer._instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.11.0", ) def test_schema_url_new_semconv(self): with use_semconv_opt_in("database"): with mock.patch("pymongo.monitoring.register"): instrumentor = PymongoInstrumentor() instrumentor.instrument(tracer_provider=self.tracer_provider) self.assertEqual( instrumentor._commandtracer_instance._tracer._instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.25.0", ) def test_started_new_semconv(self): command_tracer = CommandTracer(self.tracer, semconv_opt_in_mode=_StabilityMode.DATABASE) mock_event = MockEvent({"command_name": "find"}, ("testhost", 1234)) command_tracer.started(event=mock_event) span = command_tracer._pop_span(mock_event) # pylint: disable=protected-access self.assertEqual(span.attributes[DB_SYSTEM_NAME], "mongodb") self.assertEqual(span.attributes[DB_NAMESPACE], "database_name") self.assertEqual(span.attributes[DB_QUERY_TEXT], "find") self.assertEqual(span.attributes[SERVER_ADDRESS], "testhost") self.assertEqual(span.attributes[SERVER_PORT], 1234) def test_started_both_semconv(self): command_tracer = CommandTracer(self.tracer, semconv_opt_in_mode=_StabilityMode.DATABASE_DUP) mock_event = MockEvent({"command_name": "find"}, ("testhost", 1234)) command_tracer.started(event=mock_event) span = command_tracer._pop_span(mock_event) # pylint: disable=protected-access self.assertEqual(span.attributes[DB_SYSTEM], "mongodb") self.assertEqual(span.attributes[DB_NAME], "database_name") self.assertEqual(span.attributes[DB_STATEMENT], "find") self.assertEqual(span.attributes[NET_PEER_NAME], "testhost") self.assertEqual(span.attributes[NET_PEER_PORT], 1234) self.assertEqual(span.attributes[DB_SYSTEM_NAME], "mongodb") self.assertEqual(span.attributes[DB_NAMESPACE], "database_name") self.assertEqual(span.attributes[DB_QUERY_TEXT], "find") self.assertEqual(span.attributes[SERVER_ADDRESS], "testhost") self.assertEqual(span.attributes[SERVER_PORT], 1234) def test_collection_name_default_semconv(self): command_tracer = CommandTracer(self.tracer) mock_event = MockEvent({"command_name": "find", "find": "test_coll"}) command_tracer.started(event=mock_event) command_tracer.succeeded(event=mock_event) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes[DB_MONGODB_COLLECTION], "test_coll") self.assertNotIn(DB_COLLECTION_NAME, span.attributes) def test_collection_name_new_semconv(self): command_tracer = CommandTracer(self.tracer, semconv_opt_in_mode=_StabilityMode.DATABASE) mock_event = MockEvent({"command_name": "find", "find": "test_coll"}) command_tracer.started(event=mock_event) command_tracer.succeeded(event=mock_event) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes[DB_COLLECTION_NAME], "test_coll") self.assertNotIn(DB_MONGODB_COLLECTION, span.attributes) def test_collection_name_both_semconv(self): command_tracer = CommandTracer(self.tracer, semconv_opt_in_mode=_StabilityMode.DATABASE_DUP) mock_event = MockEvent({"command_name": "find", "find": "test_coll"}) command_tracer.started(event=mock_event) command_tracer.succeeded(event=mock_event) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes[DB_MONGODB_COLLECTION], "test_coll") self.assertEqual(span.attributes[DB_COLLECTION_NAME], "test_coll") def test_failed_error_type_not_set_on_default(self): command_tracer = CommandTracer(self.tracer) mock_event = MockEvent({}) command_tracer.started(event=mock_event) mock_event.mark_as_failed() command_tracer.failed(event=mock_event) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertFalse(span.status.is_ok) self.assertNotIn(ERROR_TYPE, span.attributes) def test_failed_error_type_set_on_new_semconv(self): command_tracer = CommandTracer(self.tracer, semconv_opt_in_mode=_StabilityMode.DATABASE) mock_event = MockEvent({}) command_tracer.started(event=mock_event) mock_event.failure = { "errmsg": "operation failed", "codeName": "OperationFailed", } command_tracer.failed(event=mock_event) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertFalse(span.status.is_ok) self.assertEqual(span.attributes[ERROR_TYPE], "OperationFailed") def test_failed_error_type_set_on_both_semconv(self): command_tracer = CommandTracer(self.tracer, semconv_opt_in_mode=_StabilityMode.DATABASE_DUP) mock_event = MockEvent({}) command_tracer.started(event=mock_event) mock_event.failure = { "errmsg": "operation failed", "codeName": "OperationFailed", } command_tracer.failed(event=mock_event) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertFalse(span.status.is_ok) self.assertEqual(span.attributes[ERROR_TYPE], "OperationFailed") class MockCommand: def __init__(self, command_attrs): self.command_attrs = command_attrs def get(self, key, default=""): return self.command_attrs.get(key, default) class MockEvent: def __init__(self, command_attrs, connection_id=None, request_id=""): self.command = MockCommand(command_attrs) self.command_name = self.command.get("command_name") self.connection_id = connection_id self.request_id = request_id self.failure = None def mark_as_failed(self): # CommandFailedEvent.failure is type _DocumentOut, which pymongo defines as: # ``` # _DocumentOut = Union[MutableMapping[str, Any], "RawBSONDocument"] # ``` # we go with the former, but both provide a `.get(key, default)` method. # self.failure = {"errmsg": "operation failed"} def __getattr__(self, item): return item opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymssql/000077500000000000000000000000001527070072300326535ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymssql/LICENSE000066400000000000000000000261351527070072300336670ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymssql/README.rst000066400000000000000000000012371527070072300343450ustar00rootroot00000000000000OpenTelemetry pymssql Instrumentation ===================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-pymssql.svg :target: https://pypi.org/project/opentelemetry-instrumentation-pymssql/ Installation ------------ :: pip install opentelemetry-instrumentation-pymssql References ---------- * `OpenTelemetry pymssql Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031411527070072300355070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymssql[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-pymssql" dynamic = ["version"] description = "OpenTelemetry pymssql instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-instrumentation-dbapi == 0.66b2", ] [project.optional-dependencies] instruments = [ "pymssql >= 2.1.5, < 3", ] [project.entry-points.opentelemetry_instrumentor] pymssql = "opentelemetry.instrumentation.pymssql:PyMSSQLInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-pymssql" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/pymssql/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymssql/src/000077500000000000000000000000001527070072300334425ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300362575ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymssql/srcinstrumentation/000077500000000000000000000000001527070072300415225ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymssql/src/opentelemetrypymssql/000077500000000000000000000000001527070072300432325ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymssql/src/opentelemetry/instrumentation__init__.py000066400000000000000000000150571527070072300453530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymssql/src/opentelemetry/instrumentation/pymssql# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The integration with pymssql supports the `pymssql`_ library and can be enabled by using ``PyMSSQLInstrumentor``. .. _pymssql: https://pypi.org/project/pymssql/ Usage ----- .. code:: python import pymssql from opentelemetry.instrumentation.pymssql import PyMSSQLInstrumentor PyMSSQLInstrumentor().instrument() cnx = pymssql.connect(database="MSSQL_Database") cursor = cnx.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)") cursor.execute("INSERT INTO test (testField) VALUES (123)") cnx.commit() cursor.close() cnx.close() .. code:: python import pymssql from opentelemetry.instrumentation.pymssql import PyMSSQLInstrumentor # Alternatively, use instrument_connection for an individual connection cnx = pymssql.connect(database="MSSQL_Database") instrumented_cnx = PyMSSQLInstrumentor().instrument_connection(cnx) cursor = instrumented_cnx.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)") cursor.execute("INSERT INTO test (testField) VALUES (123)") instrumented_cnx.commit() cursor.close() instrumented_cnx.close() API --- The `instrument` method accepts the following keyword args: * tracer_provider (``TracerProvider``) - an optional tracer provider For example: .. code:: python import pymssql from opentelemetry.instrumentation.pymssql import PyMSSQLInstrumentor from opentelemetry.trace import NoOpTracerProvider PyMSSQLInstrumentor().instrument(tracer_provider=NoOpTracerProvider()) """ from __future__ import annotations from collections.abc import Callable, Collection from typing import Any, NamedTuple import pymssql from opentelemetry.instrumentation import dbapi from opentelemetry.instrumentation._semconv import ( _set_db_user, _set_http_net_peer_name_client, _set_http_peer_port_client, ) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.pymssql.package import _instruments from opentelemetry.instrumentation.pymssql.version import __version__ _DATABASE_SYSTEM = "mssql" class _PyMSSQLConnectMethodArgsTuple(NamedTuple): server: str | None = None user: str | None = None password: str | None = None database: str | None = None timeout: int | None = None login_timeout: int | None = None charset: str | None = None as_dict: bool | None = None host: str | None = None appname: str | None = None port: str | None = None conn_properties: str | None = None autocommit: bool | None = None tds_version: str | None = None class _PyMSSQLDatabaseApiIntegration(dbapi.DatabaseApiIntegration): def wrapped_connection( self, connect_method: Callable[..., Any], args: tuple[Any, Any], kwargs: dict[Any, Any], ): """Add object proxy to connection object.""" connection = connect_method(*args, **kwargs) connect_method_args = _PyMSSQLConnectMethodArgsTuple(*args) self.name = self.database_system self.database = kwargs.get("database") or connect_method_args.database user = kwargs.get("user") or connect_method_args.user if user is not None: _set_db_user(self.span_attributes, user, self._sem_conv_opt_in_mode_db) port = kwargs.get("port") or connect_method_args.port host = kwargs.get("server") or connect_method_args.server if host is None: host = kwargs.get("host") or connect_method_args.host if host is not None: # The host string can include the port, separated by either a coma or # a column for sep in (":", ","): if sep in host: tokens = host.rsplit(sep) host = tokens[0] if len(tokens) > 1: port = tokens[1] if host is not None: _set_http_net_peer_name_client(self.span_attributes, host, self._sem_conv_opt_in_mode_http) if port is not None: _set_http_peer_port_client(self.span_attributes, port, self._sem_conv_opt_in_mode_http) charset = kwargs.get("charset") or connect_method_args.charset if charset is not None: self.span_attributes["db.charset"] = charset tds_version = kwargs.get("tds_version") or connect_method_args.tds_version if tds_version is not None: self.span_attributes["db.protocol.tds.version"] = tds_version return dbapi.get_traced_connection_proxy(connection, self) class PyMSSQLInstrumentor(BaseInstrumentor): def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): """Integrate with the pymssql library. https://github.com/pymssql/pymssql/ """ tracer_provider = kwargs.get("tracer_provider") dbapi.wrap_connect( __name__, pymssql, "connect", _DATABASE_SYSTEM, version=__version__, tracer_provider=tracer_provider, # pymssql does not keep the connection attributes in its connection object; # instead, we get the attributes from the connect method (which is done # via PyMSSQLDatabaseApiIntegration.wrapped_connection) db_api_integration_factory=_PyMSSQLDatabaseApiIntegration, ) def _uninstrument(self, **kwargs): """ "Disable pymssql instrumentation""" dbapi.unwrap_connect(pymssql, "connect") @staticmethod def instrument_connection(connection, tracer_provider=None): """Enable instrumentation in a pymssql connection. Args: connection: The connection to instrument. tracer_provider: The optional tracer provider to use. If omitted the current globally configured one is used. Returns: An instrumented connection. """ return dbapi.instrument_connection( __name__, connection, _DATABASE_SYSTEM, version=__version__, tracer_provider=tracer_provider, db_api_integration_factory=_PyMSSQLDatabaseApiIntegration, ) @staticmethod def uninstrument_connection(connection): """Disable instrumentation in a pymssql connection. Args: connection: The connection to uninstrument. Returns: An uninstrumented connection. """ return dbapi.uninstrument_connection(connection) package.py000066400000000000000000000002271527070072300452000ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymssql/src/opentelemetry/instrumentation/pymssql# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("pymssql >= 2.1.5, < 3",) _semconv_status = "migration" version.py000066400000000000000000000001441527070072300452700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymssql/src/opentelemetry/instrumentation/pymssql# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000004011527070072300370300ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymssqlexceptiongroup==1.2.2 iniconfig==2.0.0 packaging==24.2 pluggy==1.6.0 pymssql==2.3.12 pytest==7.4.4 tomli==2.2.1 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-dbapi -e instrumentation/opentelemetry-instrumentation-pymssql opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymssql/tests/000077500000000000000000000000001527070072300340155ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300360350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymssql/teststest_pymssql_integration.py000066400000000000000000000232101527070072300414600ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymssql/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import contextlib from unittest import mock from unittest.mock import Mock, patch import pymssql # type: ignore import opentelemetry.instrumentation.pymssql from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.pymssql import PyMSSQLInstrumentor from opentelemetry.sdk import resources from opentelemetry.test.test_base import TestBase from opentelemetry.util._importlib_metadata import entry_points def mock_connect(*args, **kwargs): class MockConnection: def cursor(self): # pylint: disable=no-self-use return Mock() return MockConnection() @contextlib.contextmanager def use_semconv_opt_in(sem_conv_mode): env_patch = mock.patch.dict( "os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode}, ) _OpenTelemetrySemanticConventionStability._initialized = False env_patch.start() try: yield finally: env_patch.stop() _OpenTelemetrySemanticConventionStability._initialized = False class TestPyMSSQLIntegration(TestBase): def tearDown(self): super().tearDown() with self.disable_logging(): PyMSSQLInstrumentor().uninstrument() def _execute_query_and_get_span(self, cnx): cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.pymssql) return span @patch("pymssql.connect", new=mock_connect) # pylint: disable=unused-argument def test_instrumentor(self): PyMSSQLInstrumentor().instrument() cnx = pymssql.connect( # pylint: disable=no-member host="dbserver.local:1433", database="testdb", user="dbuser", password="dbpassw0rd", charset="UTF-8", tds_version="7.1", ) span = self._execute_query_and_get_span(cnx) self.assertEqual(span.attributes["db.system"], "mssql") self.assertEqual(span.attributes["db.name"], "testdb") self.assertEqual(span.attributes["db.statement"], "SELECT * FROM test") self.assertEqual(span.attributes["db.user"], "dbuser") self.assertEqual(span.attributes["net.peer.name"], "dbserver.local") self.assertEqual(span.attributes["net.peer.port"], 1433) self.assertEqual(span.attributes["db.charset"], "UTF-8") self.assertEqual(span.attributes["db.protocol.tds.version"], "7.1") # check that no spans are generated after uninstrument PyMSSQLInstrumentor().uninstrument() cnx = pymssql.connect(database="test") # pylint: disable=no-member cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) @patch("pymssql.connect", new=mock_connect) # pylint: disable=unused-argument def test_instrumentor_server_param(self): PyMSSQLInstrumentor().instrument() # `server` can be used instead of `host` cnx = pymssql.connect(server="dbserver.local:1433", database="testdb") # pylint: disable=no-member span = self._execute_query_and_get_span(cnx) self.assertEqual(span.attributes["net.peer.name"], "dbserver.local") self.assertEqual(span.attributes["net.peer.port"], 1433) @patch("pymssql.connect", new=mock_connect) # pylint: disable=unused-argument def test_instrumentor_port_param(self): PyMSSQLInstrumentor().instrument() # port can be specified as a parameter cnx = pymssql.connect( # pylint: disable=no-member server="dbserver.local", port="1433", database="testdb" ) span = self._execute_query_and_get_span(cnx) self.assertEqual(span.attributes["net.peer.name"], "dbserver.local") self.assertEqual(span.attributes["net.peer.port"], 1433) @patch("pymssql.connect", new=mock_connect) # pylint: disable=unused-argument def test_instrumentor_windows_server(self): PyMSSQLInstrumentor().instrument() # Windows server names can include special characters cnx = pymssql.connect(server=r"(local)\SQLEXPRESS", database="testdb") # pylint: disable=no-member span = self._execute_query_and_get_span(cnx) self.assertEqual(span.attributes["net.peer.name"], r"(local)\SQLEXPRESS") @patch("pymssql.connect", new=mock_connect) # pylint: disable=unused-argument def test_custom_tracer_provider(self): resource = resources.Resource.create({}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result PyMSSQLInstrumentor().instrument(tracer_provider=tracer_provider) cnx = pymssql.connect(database="test") # pylint: disable=no-member cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertIs(span.resource, resource) @patch("pymssql.connect", new=mock_connect) # pylint: disable=unused-argument def test_instrument_connection(self): cnx = pymssql.connect(database="test") # pylint: disable=no-member query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) cnx = PyMSSQLInstrumentor().instrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) @patch("pymssql.connect", new=mock_connect) # pylint: disable=unused-argument def test_uninstrument_connection(self): PyMSSQLInstrumentor().instrument() cnx = pymssql.connect(database="test") # pylint: disable=no-member query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) cnx = PyMSSQLInstrumentor().uninstrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) def test_load_entry_point(self): self.assertIs( next(iter(entry_points(group="opentelemetry_instrumentor", name="pymssql"))).load(), PyMSSQLInstrumentor, ) @patch("pymssql.connect", new=mock_connect) def test_semconv_stable(self): """database,http opt-in emits only stable attributes.""" with use_semconv_opt_in("database,http"): PyMSSQLInstrumentor().instrument() cnx = pymssql.connect( # pylint: disable=no-member server="dbserver.local", port="1433", database="testdb", user="dbuser", password="dbpassw0rd", tds_version="7.1", ) span = self._execute_query_and_get_span(cnx) self.assertEqual(span.attributes["db.system.name"], "mssql") self.assertEqual(span.attributes["db.namespace"], "testdb") self.assertEqual(span.attributes["db.query.text"], "SELECT * FROM test") self.assertEqual(span.attributes["server.address"], "dbserver.local") self.assertEqual(span.attributes["server.port"], 1433) self.assertEqual(span.attributes["db.protocol.tds.version"], "7.1") self.assertNotIn("db.system", span.attributes) self.assertNotIn("db.name", span.attributes) self.assertNotIn("db.statement", span.attributes) self.assertNotIn("db.user", span.attributes) self.assertNotIn("net.peer.name", span.attributes) self.assertNotIn("net.peer.port", span.attributes) @patch("pymssql.connect", new=mock_connect) def test_semconv_dup(self): """database/dup,http/dup emits both legacy and stable.""" with use_semconv_opt_in("database/dup,http/dup"): PyMSSQLInstrumentor().instrument() cnx = pymssql.connect( # pylint: disable=no-member server="dbserver.local", port="1433", database="testdb", user="dbuser", password="dbpassw0rd", ) span = self._execute_query_and_get_span(cnx) self.assertEqual(span.attributes["db.system"], "mssql") self.assertEqual(span.attributes["db.system.name"], "mssql") self.assertEqual(span.attributes["db.name"], "testdb") self.assertEqual(span.attributes["db.namespace"], "testdb") self.assertEqual(span.attributes["db.statement"], "SELECT * FROM test") self.assertEqual(span.attributes["db.query.text"], "SELECT * FROM test") self.assertEqual(span.attributes["db.user"], "dbuser") self.assertEqual(span.attributes["net.peer.name"], "dbserver.local") self.assertEqual(span.attributes["server.address"], "dbserver.local") self.assertEqual(span.attributes["net.peer.port"], 1433) self.assertEqual(span.attributes["server.port"], 1433) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymysql/000077500000000000000000000000001527070072300326615ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymysql/LICENSE000066400000000000000000000261351527070072300336750ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymysql/README.rst000066400000000000000000000012371527070072300343530ustar00rootroot00000000000000OpenTelemetry PyMySQL Instrumentation ===================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-pymysql.svg :target: https://pypi.org/project/opentelemetry-instrumentation-pymysql/ Installation ------------ :: pip install opentelemetry-instrumentation-pymysql References ---------- * `OpenTelemetry PyMySQL Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031271527070072300355210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymysql[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-pymysql" dynamic = ["version"] description = "OpenTelemetry PyMySQL instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-instrumentation-dbapi == 0.66b2", ] [project.optional-dependencies] instruments = [ "PyMySQL < 2", ] [project.entry-points.opentelemetry_instrumentor] pymysql = "opentelemetry.instrumentation.pymysql:PyMySQLInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-pymysql" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/pymysql/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymysql/src/000077500000000000000000000000001527070072300334505ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300362655ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymysql/srcinstrumentation/000077500000000000000000000000001527070072300415305ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetrypymysql/000077500000000000000000000000001527070072300432465ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation__init__.py000066400000000000000000000251641527070072300453670ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The integration with PyMySQL supports the `PyMySQL`_ library and can be enabled by using ``PyMySQLInstrumentor``. .. _PyMySQL: https://pypi.org/project/PyMySQL/ Usage ----- .. code:: python import pymysql from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor # Call instrument() to wrap all database connections PyMySQLInstrumentor().instrument() cnx = pymysql.connect(database="MySQL_Database") cursor = cnx.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)") cursor.execute("INSERT INTO test (testField) VALUES (123)") cnx.commit() cursor.close() cnx.close() .. code:: python import pymysql from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor # Alternatively, use instrument_connection for an individual connection cnx = pymysql.connect(database="MySQL_Database") instrumented_cnx = PyMySQLInstrumentor().instrument_connection( cnx, enable_commenter=True, commenter_options={ "db_driver": True, "mysql_client_version": True } ) cursor = instrumented_cnx.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)") cursor.execute("INSERT INTO test (testField) VALUES (123)") instrumented_cnx.commit() cursor.close() instrumented_cnx.close() Configuration ------------- SQLCommenter ************ You can optionally enable sqlcommenter which enriches the query with contextual information. Queries made after setting up trace integration with sqlcommenter enabled will have configurable key-value pairs appended to them, e.g. ``"select * from auth_users; /*traceparent=00-01234567-abcd-01*/"``. This supports context propagation between database client and server when database log records are enabled. For more information, see: * `Semantic Conventions - Database Spans `_ * `sqlcommenter `_ .. code:: python import pymysql from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor PyMySQLInstrumentor().instrument(enable_commenter=True) cnx = pymysql.connect(database="MySQL_Database") cursor = cnx.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)") cursor.execute("INSERT INTO test (testField) VALUES (123)") cnx.commit() cursor.close() cnx.close() SQLCommenter with commenter_options *********************************** The key-value pairs appended to the query can be configured using ``commenter_options``. When sqlcommenter is enabled, all available KVs/tags are calculated by default. ``commenter_options`` supports *opting out* of specific KVs. .. code:: python import pymysql from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor # Opts into sqlcomment for PyMySQL trace integration. # Opts out of tags for mysql_client_version, db_driver. PyMySQLInstrumentor().instrument( enable_commenter=True, commenter_options={ "mysql_client_version": False, "db_driver": False, } ) Available commenter_options ########################### The following sqlcomment key-values can be opted out of through ``commenter_options``: +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | Commenter Option | Description | Example | +===========================+===========================================================+===========================================================================+ | ``db_driver`` | Database driver name with version. | ``pymysql='1.2.3'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``dbapi_threadsafety`` | DB-API threadsafety value: 0-3 or unknown. | ``dbapi_threadsafety=2`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``dbapi_level`` | DB-API API level: 1.0, 2.0, or unknown. | ``dbapi_level='2.0'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``driver_paramstyle`` | DB-API paramstyle for SQL statement parameter. | ``driver_paramstyle='pyformat'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``mysql_client_version`` | MySQL client version. | ``mysql_client_version='123'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``opentelemetry_values`` | OpenTelemetry context as traceparent at time of query. | ``traceparent='00-03afa25236b8cd948fa853d67038ac79-405ff022e8247c46-01'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ SQLComment in span attribute **************************** If sqlcommenter is enabled, you can opt into the inclusion of sqlcomment in the query span ``db.statement`` and/or ``db.query.text`` attribute for your needs. If ``commenter_options`` have been set, the span attribute comment will also be configured by this setting. .. code:: python from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor # Opts into sqlcomment for PyMySQL trace integration. # Opts into sqlcomment for `db.statement` and/or `db.query.text` span attribute. PyMySQLInstrumentor().instrument( enable_commenter=True, enable_attribute_commenter=True, ) Warning: Capture of sqlcomment in ``db.statement``/``db.query.text`` may have high cardinality without platform normalization. See `Semantic Conventions for database spans `_ for more information. API --- """ from collections.abc import Collection import pymysql from opentelemetry.instrumentation import dbapi from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.pymysql.package import _instruments from opentelemetry.instrumentation.pymysql.version import __version__ _CONNECTION_ATTRIBUTES = { "database": "db", "port": "port", "host": "host", "user": "user", } _DATABASE_SYSTEM = "mysql" class PyMySQLInstrumentor(BaseInstrumentor): def instrumentation_dependencies(self) -> Collection[str]: # pylint: disable=no-self-use return _instruments def _instrument(self, **kwargs): # pylint: disable=no-self-use """Integrate with the PyMySQL library. https://github.com/PyMySQL/PyMySQL/ """ tracer_provider = kwargs.get("tracer_provider") enable_sqlcommenter = kwargs.get("enable_commenter", False) commenter_options = kwargs.get("commenter_options", {}) enable_attribute_commenter = kwargs.get("enable_attribute_commenter", False) dbapi.wrap_connect( __name__, pymysql, "connect", _DATABASE_SYSTEM, _CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, enable_commenter=enable_sqlcommenter, commenter_options=commenter_options, enable_attribute_commenter=enable_attribute_commenter, ) def _uninstrument(self, **kwargs): # pylint: disable=no-self-use """ "Disable PyMySQL instrumentation""" dbapi.unwrap_connect(pymysql, "connect") @staticmethod def instrument_connection( connection, tracer_provider=None, enable_commenter=None, commenter_options=None, enable_attribute_commenter=None, ): """Enable instrumentation in a PyMySQL connection. Args: connection: The existing PyMySQL connection instance that needs to be instrumented. This connection was typically created using `pymysql.connect()` and is wrapped with OpenTelemetry tracing. tracer_provider: An optional `TracerProvider` instance that specifies which tracer provider should be used. If not provided, the globally configured OpenTelemetry tracer provider is automatically applied. enable_commenter: A flag to enable the SQL Commenter feature. If `True`, query logs will be enriched with additional contextual metadata (e.g., database version, traceparent IDs, driver information). commenter_options: A dictionary containing configuration options for the SQL Commenter feature. You can specify various options, such as enabling driver information, database version logging, traceparent propagation, and other customizable metadata enhancements. See *SQLCommenter Configurations* above for more information. Returns: An instrumented connection. """ return dbapi.instrument_connection( __name__, connection, _DATABASE_SYSTEM, _CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, enable_commenter=enable_commenter, commenter_options=commenter_options, connect_module=pymysql, enable_attribute_commenter=enable_attribute_commenter, ) @staticmethod def uninstrument_connection(connection): """Disable instrumentation in a PyMySQL connection. Args: connection: The connection to uninstrument. Returns: An uninstrumented connection. """ return dbapi.uninstrument_connection(connection) package.py000066400000000000000000000002151527070072300452110ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("PyMySQL < 2",) _semconv_status = "migration" version.py000066400000000000000000000001441527070072300453040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000005231527070072300370430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymysqlasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 PyMySQL==1.1.1 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-dbapi -e instrumentation/opentelemetry-instrumentation-pymysql opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymysql/tests/000077500000000000000000000000001527070072300340235ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300360430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymysql/teststest_pymysql_integration.py000066400000000000000000000536611527070072300415110ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pymysql/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import contextlib from unittest import mock import pymysql import opentelemetry.instrumentation.pymysql from opentelemetry import trace as trace_api from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor from opentelemetry.sdk import resources from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_STATEMENT, DB_SYSTEM, DB_USER, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_NAMESPACE, DB_QUERY_TEXT, DB_SYSTEM_NAME, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.test.test_base import TestBase @contextlib.contextmanager def use_semconv_opt_in(sem_conv_mode): env_patch = mock.patch.dict( "os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode}, ) _OpenTelemetrySemanticConventionStability._initialized = False env_patch.start() try: yield finally: env_patch.stop() _OpenTelemetrySemanticConventionStability._initialized = False def connect_and_execute_query(): cnx = pymysql.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) return cnx, query def make_pymysql_connection_mock(): cnx = mock.MagicMock() cnx.db = "test" cnx.host = "localhost" cnx.port = 3306 cnx.user = "testuser" return cnx class TestPyMysqlIntegration(TestBase): # pylint: disable=invalid-name def tearDown(self): super().tearDown() with self.disable_logging(): PyMySQLInstrumentor().uninstrument() @mock.patch("pymysql.connect") # pylint: disable=unused-argument def test_instrumentor(self, mock_connect): PyMySQLInstrumentor().instrument() cnx = pymysql.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] # Check version and name in span's instrumentation info self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.pymysql) # check that no spans are generated after uninstrument PyMySQLInstrumentor().uninstrument() cnx = pymysql.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) @mock.patch("pymysql.connect") # pylint: disable=unused-argument def test_custom_tracer_provider(self, mock_connect): resource = resources.Resource.create({}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result PyMySQLInstrumentor().instrument(tracer_provider=tracer_provider) cnx = pymysql.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertIs(span.resource, resource) @mock.patch("pymysql.connect") # pylint: disable=unused-argument def test_no_op_tracer_provider(self, mock_connect): PyMySQLInstrumentor().instrument(tracer_provider=trace_api.NoOpTracerProvider()) cnx = pymysql.connect(database="test") cursor = cnx.cursor() query = "SELECT * FROM test" cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) @mock.patch("pymysql.connect") # pylint: disable=unused-argument def test_instrument_connection(self, mock_connect): cnx = pymysql.connect(database="test") query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) cnx = PyMySQLInstrumentor().instrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) @mock.patch("opentelemetry.instrumentation.dbapi.instrument_connection") @mock.patch("pymysql.connect") # pylint: disable=unused-argument def test_instrument_connection_enable_commenter_dbapi_kwargs( self, mock_connect, mock_instrument_connection, ): cnx = pymysql.connect(database="test") cnx = PyMySQLInstrumentor().instrument_connection( cnx, enable_commenter=True, commenter_options={"foo": True}, enable_attribute_commenter=True, ) cursor = cnx.cursor() cursor.execute("SELECT * FROM test") kwargs = mock_instrument_connection.call_args[1] self.assertEqual(kwargs["enable_commenter"], True) self.assertEqual(kwargs["commenter_options"], {"foo": True}) self.assertEqual(kwargs["enable_attribute_commenter"], True) def test_instrument_connection_with_dbapi_sqlcomment_enabled(self): mock_connect_module = mock.MagicMock( __name__="pymysql", __version__="foobar", threadsafety="123", apilevel="123", paramstyle="test", ) mock_connect_module.get_client_info.return_value = "foobaz" mock_cursor = mock_connect_module.connect().cursor() mock_connection = mock.MagicMock() mock_connection.cursor.return_value = mock_cursor with mock.patch( "opentelemetry.instrumentation.pymysql.pymysql", mock_connect_module, ): cnx_proxy = PyMySQLInstrumentor().instrument_connection( mock_connection, enable_commenter=True, ) cnx_proxy.cursor().execute("Select 1;") spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = format(span.get_span_context().trace_flags, "02x") self.assertEqual( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='pymysql%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_instrument_connection_with_dbapi_sqlcomment_enabled_stmt_enabled( self, ): mock_connect_module = mock.MagicMock( __name__="pymysql", __version__="foobar", threadsafety="123", apilevel="123", paramstyle="test", ) mock_connect_module.get_client_info.return_value = "foobaz" mock_cursor = mock_connect_module.connect().cursor() mock_connection = mock.MagicMock() mock_connection.cursor.return_value = mock_cursor with mock.patch( "opentelemetry.instrumentation.pymysql.pymysql", mock_connect_module, ): cnx_proxy = PyMySQLInstrumentor().instrument_connection( mock_connection, enable_commenter=True, enable_attribute_commenter=True, ) cnx_proxy.cursor().execute("Select 1;") spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = format(span.get_span_context().trace_flags, "02x") self.assertEqual( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='pymysql%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) self.assertEqual( span.attributes[DB_STATEMENT], f"Select 1 /*db_driver='pymysql%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) def test_instrument_connection_with_dbapi_sqlcomment_enabled_with_options( self, ): mock_connect_module = mock.MagicMock( __name__="pymysql", __version__="foobar", threadsafety="123", apilevel="123", paramstyle="test", ) mock_connect_module.get_client_info.return_value = "foobaz" mock_cursor = mock_connect_module.connect().cursor() mock_connection = mock.MagicMock() mock_connection.cursor.return_value = mock_cursor with mock.patch( "opentelemetry.instrumentation.pymysql.pymysql", mock_connect_module, ): cnx_proxy = PyMySQLInstrumentor().instrument_connection( mock_connection, enable_commenter=True, commenter_options={ "dbapi_level": False, "dbapi_threadsafety": True, "driver_paramstyle": False, }, ) cnx_proxy.cursor().execute("Select 1;") spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = format(span.get_span_context().trace_flags, "02x") self.assertEqual( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='pymysql%%3Afoobar',dbapi_threadsafety='123',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_instrument_connection_with_dbapi_sqlcomment_not_enabled_default( self, ): mock_connect_module = mock.MagicMock( __name__="pymysql", __version__="foobar", threadsafety="123", apilevel="123", paramstyle="test", ) mock_connect_module.get_client_info.return_value = "foobaz" mock_cursor = mock_connect_module.connect().cursor() mock_connection = mock.MagicMock() mock_connection.cursor.return_value = mock_cursor with mock.patch( "opentelemetry.instrumentation.pymysql.pymysql", mock_connect_module, ): cnx_proxy = PyMySQLInstrumentor().instrument_connection( mock_connection, ) cnx_proxy.cursor().execute("Select 1;") self.assertEqual( mock_cursor.execute.call_args[0][0], "Select 1;", ) spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) @mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect") @mock.patch("pymysql.connect") # pylint: disable=unused-argument def test_instrument_enable_commenter_dbapi_kwargs( self, mock_connect, mock_wrap_connect, ): PyMySQLInstrumentor()._instrument( enable_commenter=True, commenter_options={"foo": True}, enable_attribute_commenter=True, ) kwargs = mock_wrap_connect.call_args[1] self.assertEqual(kwargs["enable_commenter"], True) self.assertEqual(kwargs["commenter_options"], {"foo": True}) self.assertEqual(kwargs["enable_attribute_commenter"], True) def test_instrument_with_dbapi_sqlcomment_enabled( self, ): mock_connect_module = mock.MagicMock( __name__="pymysql", __version__="foobar", threadsafety="123", apilevel="123", paramstyle="test", ) mock_connect_module.get_client_info.return_value = "foobaz" mock_cursor = mock_connect_module.connect().cursor() mock_connection = mock.MagicMock() mock_connection.cursor.return_value = mock_cursor with mock.patch( "opentelemetry.instrumentation.pymysql.pymysql", mock_connect_module, ): PyMySQLInstrumentor()._instrument( enable_commenter=True, ) cnx = mock_connect_module.connect(database="test") cursor = cnx.cursor() cursor.execute("Select 1;") spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = format(span.get_span_context().trace_flags, "02x") self.assertEqual( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='pymysql%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_instrument_with_dbapi_sqlcomment_enabled_stmt_enabled( self, ): mock_connect_module = mock.MagicMock( __name__="pymysql", __version__="foobar", threadsafety="123", apilevel="123", paramstyle="test", ) mock_connect_module.get_client_info.return_value = "foobaz" mock_cursor = mock_connect_module.connect().cursor() mock_connection = mock.MagicMock() mock_connection.cursor.return_value = mock_cursor with mock.patch( "opentelemetry.instrumentation.pymysql.pymysql", mock_connect_module, ): PyMySQLInstrumentor()._instrument( enable_commenter=True, enable_attribute_commenter=True, ) cnx = mock_connect_module.connect(database="test") cursor = cnx.cursor() cursor.execute("Select 1;") spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = format(span.get_span_context().trace_flags, "02x") self.assertEqual( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='pymysql%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) self.assertEqual( span.attributes[DB_STATEMENT], f"Select 1 /*db_driver='pymysql%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) def test_instrument_with_dbapi_sqlcomment_enabled_with_options( self, ): mock_connect_module = mock.MagicMock( __name__="pymysql", __version__="foobar", threadsafety="123", apilevel="123", paramstyle="test", ) mock_connect_module.get_client_info.return_value = "foobaz" mock_cursor = mock_connect_module.connect().cursor() mock_connection = mock.MagicMock() mock_connection.cursor.return_value = mock_cursor with mock.patch( "opentelemetry.instrumentation.pymysql.pymysql", mock_connect_module, ): PyMySQLInstrumentor()._instrument( enable_commenter=True, commenter_options={ "dbapi_level": False, "dbapi_threadsafety": True, "driver_paramstyle": False, }, ) cnx = mock_connect_module.connect(database="test") cursor = cnx.cursor() cursor.execute("Select 1;") spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] span_id = format(span.get_span_context().span_id, "016x") trace_id = format(span.get_span_context().trace_id, "032x") trace_flags = format(span.get_span_context().trace_flags, "02x") self.assertEqual( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='pymysql%%3Afoobar',dbapi_threadsafety='123',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-{trace_flags}'*/;", ) self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) def test_instrument_with_dbapi_sqlcomment_not_enabled_default( self, ): mock_connect_module = mock.MagicMock( __name__="pymysql", __version__="foobar", threadsafety="123", apilevel="123", paramstyle="test", ) mock_connect_module.get_client_info.return_value = "foobaz" mock_cursor = mock_connect_module.connect().cursor() mock_connection = mock.MagicMock() mock_connection.cursor.return_value = mock_cursor with mock.patch( "opentelemetry.instrumentation.pymysql.pymysql", mock_connect_module, ): PyMySQLInstrumentor()._instrument() cnx = mock_connect_module.connect(database="test") cursor = cnx.cursor() cursor.execute("Select 1;") self.assertEqual( mock_cursor.execute.call_args[0][0], "Select 1;", ) spans_list = self.memory_exporter.get_finished_spans() span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "Select 1;", ) @mock.patch("pymysql.connect") # pylint: disable=unused-argument def test_uninstrument_connection(self, mock_connect): PyMySQLInstrumentor().instrument() cnx = pymysql.connect(database="test") query = "SELECT * FROM test" cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) cnx = PyMySQLInstrumentor().uninstrument_connection(cnx) cursor = cnx.cursor() cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) @mock.patch("pymysql.connect") def test_semconv_stable(self, mock_connect): """database,http opt-in emits only stable attributes.""" with use_semconv_opt_in("database,http"): mock_connect.return_value = make_pymysql_connection_mock() PyMySQLInstrumentor().instrument() connect_and_execute_query() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_SYSTEM_NAME], "mysql") self.assertEqual(span.attributes[DB_NAMESPACE], "test") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT * FROM test") self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost") self.assertEqual(span.attributes[SERVER_PORT], 3306) self.assertNotIn(DB_SYSTEM, span.attributes) self.assertNotIn(DB_NAME, span.attributes) self.assertNotIn(DB_STATEMENT, span.attributes) self.assertNotIn(DB_USER, span.attributes) self.assertNotIn(NET_PEER_NAME, span.attributes) self.assertNotIn(NET_PEER_PORT, span.attributes) @mock.patch("pymysql.connect") def test_semconv_dup(self, mock_connect): """database/dup,http/dup opt-in emits both legacy and stable attributes.""" with use_semconv_opt_in("database/dup,http/dup"): mock_connect.return_value = make_pymysql_connection_mock() PyMySQLInstrumentor().instrument() connect_and_execute_query() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_SYSTEM], "mysql") self.assertEqual(span.attributes[DB_SYSTEM_NAME], "mysql") self.assertEqual(span.attributes[DB_NAME], "test") self.assertEqual(span.attributes[DB_NAMESPACE], "test") self.assertEqual(span.attributes[DB_STATEMENT], "SELECT * FROM test") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT * FROM test") self.assertEqual(span.attributes[DB_USER], "testuser") self.assertEqual(span.attributes[NET_PEER_NAME], "localhost") self.assertEqual(span.attributes[NET_PEER_PORT], 3306) self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost") self.assertEqual(span.attributes[SERVER_PORT], 3306) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pyramid/000077500000000000000000000000001527070072300326105ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pyramid/LICENSE000066400000000000000000000261351527070072300336240ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pyramid/README.rst000066400000000000000000000012371527070072300343020ustar00rootroot00000000000000OpenTelemetry Pyramid Instrumentation ===================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-pyramid.svg :target: https://pypi.org/project/opentelemetry-instrumentation-pyramid/ Installation ------------ :: pip install opentelemetry-instrumentation-pyramid References ---------- * `OpenTelemetry Pyramid Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000033731527070072300354530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pyramid[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-pyramid" dynamic = ["version"] description = "OpenTelemetry Pyramid instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-instrumentation-wsgi == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-util-http == 0.66b2", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [ "pyramid >= 1.7", ] [project.entry-points.opentelemetry_instrumentor] pyramid = "opentelemetry.instrumentation.pyramid:PyramidInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-pyramid" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/pyramid/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pyramid/src/000077500000000000000000000000001527070072300333775ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300362145ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pyramid/srcinstrumentation/000077500000000000000000000000001527070072300414575ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetrypyramid/000077500000000000000000000000001527070072300431245ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation__init__.py000066400000000000000000000247551527070072300452520ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Pyramid instrumentation supporting `pyramid`_, it can be enabled by using ``PyramidInstrumentor``. .. _pyramid: https://docs.pylonsproject.org/projects/pyramid/en/latest/ Usage ----- There are two methods to instrument Pyramid: Method 1 (Instrument all Configurators): ---------------------------------------- .. code:: python from pyramid.config import Configurator from opentelemetry.instrumentation.pyramid import PyramidInstrumentor PyramidInstrumentor().instrument() config = Configurator() # use your config as normal config.add_route('index', '/') Method 2 (Instrument one Configurator): --------------------------------------- .. code:: python from pyramid.config import Configurator from opentelemetry.instrumentation.pyramid import PyramidInstrumentor config = Configurator() PyramidInstrumentor().instrument_config(config) # use your config as normal config.add_route('index', '/') Using ``pyramid.tweens`` setting: --------------------------------- If you use Method 2 and then set tweens for your application with the ``pyramid.tweens`` setting, you need to explicitly add ``opentelemetry.instrumentation.pyramid.trace_tween_factory`` to the list, *as well as* instrumenting the config as shown above. For example: .. code:: python from pyramid.config import Configurator from opentelemetry.instrumentation.pyramid import PyramidInstrumentor config = Configurator() config.add_tween('opentelemetry.instrumentation.pyramid.trace_tween_factory') PyramidInstrumentor().instrument_config(config) # use your config as normal. config.add_route('index', '/') Configuration ------------- Exclude lists ************* To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_PYRAMID_EXCLUDED_URLS`` (or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the URLs. For example, :: export OTEL_PYTHON_PYRAMID_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. Capture HTTP request and response headers ***************************************** You can configure the agent to capture specified HTTP headers as span attributes, according to the `semantic conventions `_. Request headers *************** To capture HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. Request header names in Pyramid are case-insensitive and ``-`` characters are replaced by ``_``. So, giving the header name as ``CUStom_Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" Would match all request headers that start with ``Accept`` and ``X-``. To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*" The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a single item list containing all the header values. For example: ``http.request.header.custom_request_header = [","]`` Response headers **************** To capture HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. Response header names in Pyramid are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" Would match all response headers that start with ``Content`` and ``X-``. To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*" The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a single item list containing all the header values. For example: ``http.response.header.custom_response_header = [","]`` Sanitizing headers ****************** In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be matched in a case-insensitive manner. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. Note: The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. API --- """ import platform from collections.abc import Collection from pyramid.config import Configurator from pyramid.path import caller_package from pyramid.settings import aslist from wrapt import wrap_function_wrapper as _wrap from opentelemetry.instrumentation._semconv import ( _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _StabilityMode, ) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.pyramid import callbacks from opentelemetry.instrumentation.pyramid.callbacks import ( SETTING_TRACE_ENABLED, TWEEN_NAME, trace_tween_factory, ) from opentelemetry.instrumentation.pyramid.package import _instruments from opentelemetry.instrumentation.utils import unwrap # test_automatic.TestAutomatic.test_tween_list needs trace_tween_factory to be # imported in this module. The next line is necessary to avoid a lint error # from importing an unused symbol. trace_tween_factory # pylint: disable=pointless-statement if platform.python_implementation() == "PyPy": CALLER_LEVELS = 3 else: CALLER_LEVELS = 2 def _traced_init(wrapped, instance, args, kwargs): settings = kwargs.get("settings", {}) tweens = aslist(settings.get("pyramid.tweens", [])) if tweens and TWEEN_NAME not in settings: # pyramid.tweens.EXCVIEW is the name of built-in exception view provided by # pyramid. We need our tween to be before it, otherwise unhandled # exceptions will be caught before they reach our tween. tweens = [TWEEN_NAME] + tweens settings["pyramid.tweens"] = "\n".join(tweens) kwargs["settings"] = settings # `caller_package` works by walking a fixed amount of frames up the stack # to find the calling package. So if we let the original `__init__` # function call it, our wrapper will mess things up. if not kwargs.get("package", None): # Get the package for the 2nd frame up from this one. # Default is `level=2` one level down (in Configurator.__init__). # We want the 3rd level from _there_. Since we are already 1 level above, # we need the 2nd level up from here, which will give us the package from # `wrapt` instead of the desired package (the caller) kwargs["package"] = caller_package(level=CALLER_LEVELS) wrapped(*args, **kwargs) instance.include("opentelemetry.instrumentation.pyramid.callbacks") class PyramidInstrumentor(BaseInstrumentor): def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): """Integrate with Pyramid Python library. https://docs.pylonsproject.org/projects/pyramid/en/latest/ """ # Initialize semantic conventions opt-in mode _OpenTelemetrySemanticConventionStability._initialize() sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) # Set module-level opt-in mode in callbacks callbacks._sem_conv_opt_in_mode = sem_conv_opt_in_mode _wrap("pyramid.config", "Configurator.__init__", _traced_init) def _uninstrument(self, **kwargs): """ "Disable Pyramid instrumentation""" # Reset module-level opt-in mode to default callbacks._sem_conv_opt_in_mode = _StabilityMode.DEFAULT unwrap(Configurator, "__init__") @staticmethod def instrument_config(config): """Enable instrumentation in a Pyramid configurator. Args: config: The Configurator to instrument. """ # Initialize semantic conventions opt-in mode _OpenTelemetrySemanticConventionStability._initialize() sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) # Set module-level opt-in mode in callbacks callbacks._sem_conv_opt_in_mode = sem_conv_opt_in_mode config.include("opentelemetry.instrumentation.pyramid.callbacks") @staticmethod def uninstrument_config(config): # Reset module-level opt-in mode to default callbacks._sem_conv_opt_in_mode = _StabilityMode.DEFAULT config.add_settings({SETTING_TRACE_ENABLED: False}) callbacks.py000066400000000000000000000303441527070072300454210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import wsgiref.util as wsgiref_util from logging import getLogger from time import time_ns from timeit import default_timer from pyramid.events import BeforeTraversal from pyramid.httpexceptions import HTTPException, HTTPServerError from pyramid.settings import asbool from pyramid.tweens import EXCVIEW import opentelemetry.instrumentation.wsgi as otel_wsgi from opentelemetry import context, trace from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, _get_schema_url, _report_new, _report_old, _set_http_url, _StabilityMode, ) from opentelemetry.instrumentation.propagators import ( get_global_response_propagator, ) from opentelemetry.instrumentation.pyramid.version import __version__ from opentelemetry.instrumentation.utils import _start_internal_or_server_span from opentelemetry.metrics import get_meter from opentelemetry.semconv._incubating.attributes.error_attributes import ( ERROR_TYPE, ) from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_RESPONSE_STATUS_CODE, HTTP_STATUS_CODE, ) from opentelemetry.semconv.attributes.http_attributes import HTTP_ROUTE from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.semconv.metrics.http_metrics import ( HTTP_SERVER_REQUEST_DURATION, ) from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util.http import get_excluded_urls, redact_url TWEEN_NAME = "opentelemetry.instrumentation.pyramid.trace_tween_factory" SETTING_TRACE_ENABLED = "opentelemetry-pyramid.trace_enabled" _SETTING_CALLBACKS_REGISTERED = "opentelemetry-pyramid.callbacks_registered" _ENVIRON_STARTTIME_KEY = "opentelemetry-pyramid.starttime_key" _ENVIRON_SPAN_KEY = "opentelemetry-pyramid.span_key" _ENVIRON_ACTIVATION_KEY = "opentelemetry-pyramid.activation_key" _ENVIRON_ENABLED_KEY = "opentelemetry-pyramid.tracing_enabled_key" _ENVIRON_TOKEN = "opentelemetry-pyramid.token" _logger = getLogger(__name__) _excluded_urls = get_excluded_urls("PYRAMID") _sem_conv_opt_in_mode = _StabilityMode.DEFAULT def includeme(config): config.add_settings({SETTING_TRACE_ENABLED: True}) if config.get_settings().get(_SETTING_CALLBACKS_REGISTERED): return config.add_settings({_SETTING_CALLBACKS_REGISTERED: True}) config.add_subscriber(_before_traversal, BeforeTraversal) _insert_tween(config) def _insert_tween(config): settings = config.get_settings() tweens = settings.get("pyramid.tweens") # If the list is empty, pyramid does not consider the tweens have been # set explicitly. And if our tween is already there, nothing to do if not tweens or not tweens.strip(): # Add our tween just before the default exception handler config.add_tween(TWEEN_NAME, over=EXCVIEW) def _before_traversal(event): request = event.request request_environ = request.environ span_name = otel_wsgi.get_default_span_name(request_environ) enabled = request_environ.get(_ENVIRON_ENABLED_KEY) if enabled is None: _logger.warning( "Opentelemetry pyramid tween 'opentelemetry.instrumentation.pyramid.trace_tween_factory'" "was not called. Make sure that the tween is included in 'pyramid.tweens' if" "the tween list was created manually" ) return if not enabled: # Tracing not enabled, return return start_time = request_environ.get(_ENVIRON_STARTTIME_KEY) tracer = trace.get_tracer( __name__, __version__, schema_url=_get_schema_url(_sem_conv_opt_in_mode), ) if request.matched_route: span_name = request.matched_route.pattern else: span_name = otel_wsgi.get_default_span_name(request_environ) attributes = otel_wsgi.collect_request_attributes(request_environ, _sem_conv_opt_in_mode) if request.matched_route: attributes[HTTP_ROUTE] = request.matched_route.pattern span, token = _start_internal_or_server_span( tracer=tracer, span_name=span_name, start_time=start_time, context_carrier=request_environ, context_getter=otel_wsgi.wsgi_getter, attributes=attributes, ) if span.is_recording(): attributes = {} _set_http_url( attributes, redact_url(wsgiref_util.request_uri(request_environ)), _sem_conv_opt_in_mode, ) span.set_attributes(attributes) if span.kind == trace.SpanKind.SERVER: custom_attributes = otel_wsgi.collect_custom_request_headers_attributes(request_environ) if len(custom_attributes) > 0: span.set_attributes(custom_attributes) activation = trace.use_span(span, end_on_exit=True) activation.__enter__() # pylint: disable=unnecessary-dunder-call request_environ[_ENVIRON_ACTIVATION_KEY] = activation request_environ[_ENVIRON_SPAN_KEY] = span if token: request_environ[_ENVIRON_TOKEN] = token def trace_tween_factory(handler, registry): # pylint: disable=too-many-statements settings = registry.settings enabled = asbool(settings.get(SETTING_TRACE_ENABLED, True)) # Create meters and histograms based on opt-in mode duration_histogram_old = None if _report_old(_sem_conv_opt_in_mode): meter_old = get_meter( __name__, __version__, schema_url=_get_schema_url(_StabilityMode.DEFAULT), ) duration_histogram_old = meter_old.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", description="Measures the duration of inbound HTTP requests.", ) duration_histogram_new = None if _report_new(_sem_conv_opt_in_mode): meter_new = get_meter( __name__, __version__, schema_url=_get_schema_url(_StabilityMode.HTTP), ) duration_histogram_new = meter_new.create_histogram( name=HTTP_SERVER_REQUEST_DURATION, unit="s", description="Duration of HTTP server requests.", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) # Use a single meter for active requests counter (attributes are compatible) meter = get_meter( __name__, __version__, schema_url=_get_schema_url(_sem_conv_opt_in_mode), ) active_requests_counter = meter.create_up_down_counter( name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, unit="{request}", description="Number of active HTTP server requests.", ) if not enabled: # If disabled, make a tween that signals to the # BeforeTraversal subscriber that tracing is disabled def disabled_tween(request): request.environ[_ENVIRON_ENABLED_KEY] = False return handler(request) return disabled_tween # make a request tracing function # pylint: disable=too-many-branches def trace_tween(request): # pylint: disable=unnecessary-dunder-call, too-many-locals if _excluded_urls.url_disabled(request.url): request.environ[_ENVIRON_ENABLED_KEY] = False # short-circuit when we don't want to trace anything return handler(request) attributes = otel_wsgi.collect_request_attributes(request.environ, _sem_conv_opt_in_mode) request.environ[_ENVIRON_ENABLED_KEY] = True request.environ[_ENVIRON_STARTTIME_KEY] = time_ns() active_requests_count_attrs = otel_wsgi._parse_active_request_count_attrs(attributes, _sem_conv_opt_in_mode) duration_attrs = otel_wsgi._parse_duration_attrs(attributes, _sem_conv_opt_in_mode) start = default_timer() active_requests_counter.add(1, active_requests_count_attrs) response = None status = None recordable_exc = None try: response = handler(request) except HTTPException as exc: # If the exception is a pyramid HTTPException, # that's still valuable information that isn't necessarily # a 500. For instance, HTTPFound is a 302. # As described in docs, Pyramid exceptions are all valid # response types response = exc if isinstance(exc, HTTPServerError): recordable_exc = exc raise except BaseException as exc: # In the case that a non-HTTPException is bubbled up we # should infer a internal server error and raise status = "500 InternalServerError" recordable_exc = exc if _report_new(_sem_conv_opt_in_mode): attributes[ERROR_TYPE] = type(exc).__qualname__ raise finally: duration_s = default_timer() - start status = getattr(response, "status", status) status_code = otel_wsgi._parse_status_code(status) if status_code is not None: duration_attrs[HTTP_STATUS_CODE] = status_code duration_attrs[HTTP_RESPONSE_STATUS_CODE] = status_code # Record metrics for old semconv (milliseconds) if duration_histogram_old: duration_attrs_old = otel_wsgi._parse_duration_attrs(duration_attrs, _StabilityMode.DEFAULT) duration_histogram_old.record(max(round(duration_s * 1000), 0), duration_attrs_old) # Record metrics for new semconv (seconds) if duration_histogram_new: duration_attrs_new = otel_wsgi._parse_duration_attrs(duration_attrs, _StabilityMode.HTTP) duration_histogram_new.record(max(duration_s, 0), duration_attrs_new) active_requests_counter.add(-1, active_requests_count_attrs) span = request.environ.get(_ENVIRON_SPAN_KEY) enabled = request.environ.get(_ENVIRON_ENABLED_KEY) if not span and enabled: _logger.warning( "Pyramid environ's OpenTelemetry span missing." "If the OpenTelemetry tween was added manually, make sure" "PyramidInstrumentor().instrument_config(config) is called" ) elif enabled: if status is not None: otel_wsgi.add_response_attributes( span, status, getattr(response, "headerlist", None), duration_attrs, _sem_conv_opt_in_mode, ) if recordable_exc is not None: if _report_new(_sem_conv_opt_in_mode) and span.is_recording(): span.set_attribute( ERROR_TYPE, type(recordable_exc).__qualname__, ) span.set_status(Status(StatusCode.ERROR, str(recordable_exc))) span.record_exception(recordable_exc) if span.is_recording() and span.kind == trace.SpanKind.SERVER: custom_attributes = otel_wsgi.collect_custom_response_headers_attributes( getattr(response, "headerlist", None) ) if len(custom_attributes) > 0: span.set_attributes(custom_attributes) propagator = get_global_response_propagator() if propagator and hasattr(response, "headers"): propagator.inject(response.headers) activation = request.environ.get(_ENVIRON_ACTIVATION_KEY) # Only considering HTTPServerError # to make sure 200, 300 and 400 exceptions are not reported as error if isinstance(response, HTTPServerError): activation.__exit__( type(response), response, getattr(response, "__traceback__", None), ) else: activation.__exit__(None, None, None) env_token = request.environ.get(_ENVIRON_TOKEN, None) if env_token is not None: context.detach(env_token) return response return trace_tween package.py000066400000000000000000000002521527070072300450700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("pyramid >= 1.7",) _supports_metrics = True _semconv_status = "migration" version.py000066400000000000000000000001441527070072300451620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000012101527070072300367640ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pyramidasgiref==3.8.1 Deprecated==1.2.14 hupper==1.12.1 iniconfig==2.0.0 packaging==24.0 PasteDeploy==3.1.0 plaster==1.1.2 plaster-pastedeploy==1.0.1 pluggy==1.6.0 py-cpuinfo==9.0.0 pyramid==2.0.2 pytest==7.4.4 setuptools==81.0.0 tomli==2.0.1 translationstring==1.4 typing_extensions==4.12.2 venusian==3.1.0 WebOb==1.8.9 Werkzeug==3.0.6 wrapt==1.17.3 zipp==3.19.2 zope.deprecation==6.0 zope.interface==8.2 ; python_version >= "3.10" zope.interface==8.0 ; python_version < "3.10" -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-wsgi -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-pyramid opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pyramid/tests/000077500000000000000000000000001527070072300337525ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300357720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pyramid/testspyramid_base_test.py000066400000000000000000000045541527070072300377530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pyramid/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import pyramid.httpexceptions as exc from pyramid.response import Response from werkzeug.test import Client, TestResponse class InstrumentationTest: @staticmethod def _hello_endpoint(request): helloid = int(request.matchdict["helloid"]) if helloid == 500: raise exc.HTTPInternalServerError() if helloid == 404: raise exc.HTTPNotFound() if helloid == 302: raise exc.HTTPFound() if helloid == 204: raise exc.HTTPNoContent() if helloid == 900: raise NotImplementedError("error message") return Response("Hello: " + str(helloid)) @staticmethod def _custom_response_header_endpoint(request): headers = { "content-type": "text/plain; charset=utf-8", "content-length": "7", "my-custom-header": "my-custom-value-1,my-custom-header-2", "my-custom-regex-header-1": "my-custom-regex-value-1,my-custom-regex-value-2", "My-Custom-Regex-Header-2": "my-custom-regex-value-3,my-custom-regex-value-4", "my-secret-header": "my-secret-value", "dont-capture-me": "test-value", } return Response("Testing", headers=headers) def _common_initialization(self, config): # pylint: disable=unused-argument def excluded_endpoint(request): return Response("excluded") # pylint: disable=unused-argument def excluded2_endpoint(request): return Response("excluded2") config.add_route("hello", "/hello/{helloid}") config.add_view(self._hello_endpoint, route_name="hello") config.add_route("excluded_arg", "/excluded/{helloid}") config.add_view(self._hello_endpoint, route_name="excluded_arg") config.add_route("excluded", "/excluded_noarg") config.add_view(excluded_endpoint, route_name="excluded") config.add_route("excluded2", "/excluded_noarg2") config.add_view(excluded2_endpoint, route_name="excluded2") config.add_route("custom_response_headers", "/test_custom_response_headers") config.add_view( self._custom_response_header_endpoint, route_name="custom_response_headers", ) self.client = Client(config.make_wsgi_app(), TestResponse) test_automatic.py000066400000000000000000000634641527070072300373070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pyramid/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from timeit import default_timer from unittest.mock import patch from pyramid.config import Configurator from pyramid.config.adapters import AdaptersConfiguratorMixin from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, _server_active_requests_count_attrs_old, _server_duration_attrs_old, _StabilityMode, ) from opentelemetry.instrumentation.pyramid import PyramidInstrumentor from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, NumberDataPoint, ) from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_FLAVOR, HTTP_HOST, HTTP_METHOD, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_TARGET, HTTP_URL, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_HOST_PORT, ) from opentelemetry.semconv._incubating.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.semconv._incubating.attributes.url_attributes import ( URL_FULL, URL_PATH, URL_QUERY, ) from opentelemetry.semconv._incubating.attributes.user_agent_attributes import ( USER_AGENT_ORIGINAL, ) from opentelemetry.semconv._incubating.metrics.http_metrics import ( HTTP_SERVER_ACTIVE_REQUESTS, HTTP_SERVER_REQUEST_DURATION, ) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, HTTP_ROUTE, ) from opentelemetry.semconv.attributes.network_attributes import ( NETWORK_PROTOCOL_VERSION, ) from opentelemetry.semconv.attributes.url_attributes import ( URL_SCHEME, ) from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.test.globals_test import reset_trace_globals from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.trace import SpanKind from opentelemetry.trace.status import StatusCode from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, ) # pylint: disable=import-error from .pyramid_base_test import InstrumentationTest _expected_metric_names = [ HTTP_SERVER_ACTIVE_REQUESTS, MetricInstruments.HTTP_SERVER_DURATION, ] _recommended_attrs = { HTTP_SERVER_ACTIVE_REQUESTS: _server_active_requests_count_attrs_old, MetricInstruments.HTTP_SERVER_DURATION: _server_duration_attrs_old, } SCOPE = "opentelemetry.instrumentation.pyramid.callbacks" class TestAutomatic(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() PyramidInstrumentor().instrument() self.config = Configurator() self._common_initialization(self.config) def tearDown(self): super().tearDown() with self.disable_logging(): PyramidInstrumentor().uninstrument() def test_uninstrument(self): # pylint: disable=access-member-before-definition resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) self.assertEqual([b"Hello: 123"], list(resp.response)) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) PyramidInstrumentor().uninstrument() self.config = Configurator() self._common_initialization(self.config) resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) self.assertEqual([b"Hello: 123"], list(resp.response)) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) def test_tween_list(self): tween_list = "pyramid.tweens.excview_tween_factory" config = Configurator(settings={"pyramid.tweens": tween_list}) self._common_initialization(config) resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) self.assertEqual([b"Hello: 123"], list(resp.response)) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) PyramidInstrumentor().uninstrument() self.config = Configurator() self._common_initialization(self.config) resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) self.assertEqual([b"Hello: 123"], list(resp.response)) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) def test_registry_name_is_this_module(self): config = Configurator() self.assertEqual(config.registry.__name__, __name__.rsplit(".", maxsplit=1)[0]) def test_before_traversal_subscriber_not_duplicated_after_commit(self): registrations = [] def library_that_commits(config): config.add_route("example", "/example") config.commit() def another_library(config): pass original_add_subscriber = AdaptersConfiguratorMixin.add_subscriber def counting_add_subscriber(config, subscriber, iface=None, **kwargs): if getattr(subscriber, "__name__", "") == "_before_traversal": registrations.append(subscriber) return original_add_subscriber(config, subscriber, iface=iface, **kwargs) with patch.object( AdaptersConfiguratorMixin, "add_subscriber", counting_add_subscriber, ): config = Configurator() config.include(library_that_commits) config.include(another_library) self.assertEqual(len(registrations), 1) def test_redirect_response_is_not_an_error(self): tween_list = "pyramid.tweens.excview_tween_factory" config = Configurator(settings={"pyramid.tweens": tween_list}) self._common_initialization(config) resp = self.client.get("/hello/302") self.assertEqual(302, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].status.status_code, StatusCode.UNSET) self.assertEqual(len(span_list[0].events), 0) PyramidInstrumentor().uninstrument() self.config = Configurator() self._common_initialization(self.config) resp = self.client.get("/hello/302") self.assertEqual(302, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) def test_204_empty_response_is_not_an_error(self): tween_list = "pyramid.tweens.excview_tween_factory" config = Configurator(settings={"pyramid.tweens": tween_list}) self._common_initialization(config) resp = self.client.get("/hello/204") self.assertEqual(204, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].status.status_code, StatusCode.UNSET) PyramidInstrumentor().uninstrument() self.config = Configurator() self._common_initialization(self.config) resp = self.client.get("/hello/204") self.assertEqual(204, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) def test_400s_response_is_not_an_error(self): tween_list = "pyramid.tweens.excview_tween_factory" config = Configurator(settings={"pyramid.tweens": tween_list}) self._common_initialization(config) resp = self.client.get("/hello/404") self.assertEqual(404, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].status.status_code, StatusCode.UNSET) PyramidInstrumentor().uninstrument() self.config = Configurator() self._common_initialization(self.config) resp = self.client.get("/hello/404") self.assertEqual(404, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) def test_pyramid_metric(self): self.client.get("/hello/756") self.client.get("/hello/756") self.client.get("/hello/756") metrics = self.get_sorted_metrics(SCOPE) number_data_point_seen = False histogram_data_point_seen = False self.assertEqual(len(metrics), 2) for metric in metrics: self.assertIn(metric.name, _expected_metric_names) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 3) histogram_data_point_seen = True if isinstance(point, NumberDataPoint): number_data_point_seen = True for attr in point.attributes: self.assertIn(attr, _recommended_attrs[metric.name]) self.assertTrue(number_data_point_seen and histogram_data_point_seen) def test_basic_metric_success(self): start = default_timer() self.client.get("/hello/756") duration = max(round((default_timer() - start) * 1000), 0) expected_duration_attributes = { "http.method": "GET", "http.host": "localhost", "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "localhost", "net.host.port": 80, "http.status_code": 200, "net.host.name": "localhost", } expected_requests_count_attributes = { "http.method": "GET", "http.host": "localhost", "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "localhost", } metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertDictEqual( expected_duration_attributes, dict(point.attributes), ) self.assertEqual(point.count, 1) self.assertAlmostEqual(duration, point.sum, delta=20) if isinstance(point, NumberDataPoint): self.assertDictEqual( expected_requests_count_attributes, dict(point.attributes), ) self.assertEqual(point.value, 0) def test_metric_uninstrument(self): self.client.get("/hello/756") PyramidInstrumentor().uninstrument() self.config = Configurator() self._common_initialization(self.config) self.client.get("/hello/756") metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) if isinstance(point, NumberDataPoint): self.assertEqual(point.value, 0) class TestWrappedWithOtherFramework(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() PyramidInstrumentor().instrument() self.config = Configurator() self._common_initialization(self.config) def tearDown(self) -> None: super().tearDown() with self.disable_logging(): PyramidInstrumentor().uninstrument() def test_with_existing_span(self): tracer_provider, _ = self.create_tracer_provider() tracer = tracer_provider.get_tracer(__name__) with tracer.start_as_current_span("test", kind=SpanKind.SERVER) as parent_span: resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(SpanKind.INTERNAL, span_list[0].kind) self.assertEqual( parent_span.get_span_context().span_id, span_list[0].parent.span_id, ) @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,invalid-header,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", }, ) class TestCustomRequestResponseHeaders(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() PyramidInstrumentor().instrument() self.config = Configurator() self._common_initialization(self.config) def tearDown(self) -> None: super().tearDown() with self.disable_logging(): PyramidInstrumentor().uninstrument() def test_custom_request_header_added_in_server_span(self): headers = { "Custom-Test-Header-1": "Test Value 1", "Custom-Test-Header-2": "TestValue2,TestValue3", "Custom-Test-Header-3": "TestValue4", "Regex-Test-Header-1": "Regex Test Value 1", "regex-test-header-2": "RegexTestValue2,RegexTestValue3", "My-Secret-Header": "My Secret Value", } resp = self.client.get("/hello/123", headers=headers) self.assertEqual(200, resp.status_code) span = self.memory_exporter.get_finished_spans()[0] expected = { "http.request.header.custom_test_header_1": ("Test Value 1",), "http.request.header.custom_test_header_2": ("TestValue2,TestValue3",), "http.request.header.regex_test_header_1": ("Regex Test Value 1",), "http.request.header.regex_test_header_2": ("RegexTestValue2,RegexTestValue3",), "http.request.header.my_secret_header": ("[REDACTED]",), } not_expected = { "http.request.header.custom_test_header_3": ("TestValue4",), } self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) def test_custom_request_header_not_added_in_internal_span(self): tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("test", kind=SpanKind.SERVER): headers = { "Custom-Test-Header-1": "Test Value 1", "Custom-Test-Header-2": "TestValue2,TestValue3", } resp = self.client.get("/hello/123", headers=headers) self.assertEqual(200, resp.status_code) span = self.memory_exporter.get_finished_spans()[0] not_expected = { "http.request.header.custom_test_header_1": ("Test Value 1",), "http.request.header.custom_test_header_2": ("TestValue2,TestValue3",), } self.assertEqual(span.kind, SpanKind.INTERNAL) for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) def test_custom_response_header_added_in_server_span(self): resp = self.client.get("/test_custom_response_headers") self.assertEqual(200, resp.status_code) span = self.memory_exporter.get_finished_spans()[0] expected = { "http.response.header.content_type": ("text/plain; charset=utf-8",), "http.response.header.content_length": ("7",), "http.response.header.my_custom_header": ("my-custom-value-1,my-custom-header-2",), "http.response.header.my_custom_regex_header_1": ("my-custom-regex-value-1,my-custom-regex-value-2",), "http.response.header.my_custom_regex_header_2": ("my-custom-regex-value-3,my-custom-regex-value-4",), "http.response.header.my_secret_header": ("[REDACTED]",), } not_expected = {"http.response.header.dont_capture_me": ("test-value",)} self.assertEqual(span.kind, SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) def test_custom_response_header_not_added_in_internal_span(self): tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("test", kind=SpanKind.SERVER): resp = self.client.get("/test_custom_response_headers") self.assertEqual(200, resp.status_code) span = self.memory_exporter.get_finished_spans()[0] not_expected = { "http.response.header.content_type": ("text/plain; charset=utf-8",), "http.response.header.content_length": ("7",), "http.response.header.my_custom_header": ("my-custom-value-1,my-custom-header-2",), } self.assertEqual(span.kind, SpanKind.INTERNAL) for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) class _SemConvTestBase(InstrumentationTest, WsgiTestBase): semconv_mode = _StabilityMode.DEFAULT def setUp(self): super().setUp() self.env_patch = patch.dict( "os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: self.semconv_mode.value}, ) _OpenTelemetrySemanticConventionStability._initialized = False self.env_patch.start() PyramidInstrumentor().instrument() self.config = Configurator() self._common_initialization(self.config) def tearDown(self): super().tearDown() self.env_patch.stop() with self.disable_logging(): PyramidInstrumentor().uninstrument() def _verify_metric_names(self, metrics, expected_names, not_expected_names=None): metric_names = [] for metric in metrics: metric_names.append(metric.name) if expected_names: self.assertIn(metric.name, expected_names) if not_expected_names: self.assertNotIn(metric.name, not_expected_names) return metric_names def _verify_duration_point(self, point): self.assertIn(HTTP_REQUEST_METHOD, point.attributes) self.assertIn(HTTP_RESPONSE_STATUS_CODE, point.attributes) self.assertIn(URL_SCHEME, point.attributes) self.assertNotIn(HTTP_METHOD, point.attributes) self.assertNotIn(HTTP_SCHEME, point.attributes) self.assertNotIn(HTTP_STATUS_CODE, point.attributes) def _verify_metric_duration(self, metric): if "duration" in metric.name: for point in metric.data.data_points: if isinstance(point, HistogramDataPoint): self._verify_duration_point(point) def _verify_duration_attributes(self, metrics): for metric in metrics: self._verify_metric_duration(metric) class TestSemConvDefault(_SemConvTestBase): semconv_mode = _StabilityMode.DEFAULT def test_basic_old_semconv(self): resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) span = self.memory_exporter.get_finished_spans()[0] old_attrs = { HTTP_METHOD: "GET", HTTP_SCHEME: "http", HTTP_HOST: "localhost", HTTP_TARGET: "/hello/123", HTTP_URL: "http://localhost/hello/123", NET_HOST_PORT: 80, HTTP_STATUS_CODE: 200, HTTP_FLAVOR: "1.1", HTTP_ROUTE: "/hello/{helloid}", } for attr, value in old_attrs.items(): self.assertEqual(span.attributes[attr], value) for attr in [SERVER_ADDRESS, SERVER_PORT, URL_SCHEME, URL_FULL]: self.assertNotIn(attr, span.attributes) self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.11.0", ) def test_metrics_old_semconv(self): self.client.get("/hello/123") metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 2) expected_metrics = [ HTTP_SERVER_ACTIVE_REQUESTS, MetricInstruments.HTTP_SERVER_DURATION, ] self._verify_metric_names(metrics, expected_metrics, [HTTP_SERVER_REQUEST_DURATION]) for metric in metrics: for point in metric.data.data_points: if isinstance(point, HistogramDataPoint): self.assertIn("http.method", point.attributes) self.assertIn("http.scheme", point.attributes) self.assertIn(HTTP_STATUS_CODE, point.attributes) self.assertNotIn(HTTP_REQUEST_METHOD, point.attributes) self.assertNotIn(HTTP_RESPONSE_STATUS_CODE, point.attributes) class TestSemConvNew(_SemConvTestBase): semconv_mode = _StabilityMode.HTTP def test_basic_new_semconv(self): resp = self.client.get("/hello/456?query=test", headers={"User-Agent": "test-agent"}) self.assertEqual(200, resp.status_code) span = self.memory_exporter.get_finished_spans()[0] new_attrs = { HTTP_REQUEST_METHOD: "GET", URL_SCHEME: "http", URL_PATH: "/hello/456", URL_QUERY: "query=test", URL_FULL: "http://localhost/hello/456?query=test", SERVER_ADDRESS: "localhost", SERVER_PORT: 80, HTTP_RESPONSE_STATUS_CODE: 200, NETWORK_PROTOCOL_VERSION: "1.1", HTTP_ROUTE: "/hello/{helloid}", USER_AGENT_ORIGINAL: "test-agent", } for attr, value in new_attrs.items(): self.assertEqual(span.attributes[attr], value) old_attrs = [ HTTP_METHOD, HTTP_HOST, HTTP_TARGET, HTTP_URL, ] for attr in old_attrs: self.assertNotIn(attr, span.attributes) self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.21.0", ) def test_metrics_new_semconv(self): self.client.get("/hello/456") metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 2) expected_metrics = [ HTTP_SERVER_REQUEST_DURATION, HTTP_SERVER_ACTIVE_REQUESTS, ] metric_names = self._verify_metric_names(metrics, expected_metrics) self.assertIn(HTTP_SERVER_REQUEST_DURATION, metric_names) self.assertIn(HTTP_SERVER_ACTIVE_REQUESTS, metric_names) self._verify_duration_attributes(metrics) class TestSemConvDup(_SemConvTestBase): semconv_mode = _StabilityMode.HTTP_DUP def test_basic_both_semconv(self): resp = self.client.get("/hello/789?query=test", headers={"User-Agent": "test-agent"}) self.assertEqual(200, resp.status_code) span = self.memory_exporter.get_finished_spans()[0] expected_attrs = { HTTP_METHOD: "GET", HTTP_SCHEME: "http", HTTP_HOST: "localhost", HTTP_URL: "http://localhost/hello/789?query=test", HTTP_STATUS_CODE: 200, HTTP_REQUEST_METHOD: "GET", URL_SCHEME: "http", URL_PATH: "/hello/789", URL_QUERY: "query=test", URL_FULL: "http://localhost/hello/789?query=test", SERVER_ADDRESS: "localhost", HTTP_RESPONSE_STATUS_CODE: 200, HTTP_ROUTE: "/hello/{helloid}", USER_AGENT_ORIGINAL: "test-agent", } for attr, value in expected_attrs.items(): self.assertEqual(span.attributes[attr], value) self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.21.0", ) def test_metrics_both_semconv(self): self.client.get("/hello/789") metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 3) expected_metrics = [ MetricInstruments.HTTP_SERVER_DURATION, HTTP_SERVER_REQUEST_DURATION, HTTP_SERVER_ACTIVE_REQUESTS, ] metric_names = self._verify_metric_names(metrics, None) for metric_name in expected_metrics: self.assertIn(metric_name, metric_names) @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,invalid-header,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", }, ) class TestCustomHeadersNonRecordingSpan(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() # This is done because set_tracer_provider cannot override the # current tracer provider. reset_trace_globals() tracer_provider = trace.NoOpTracerProvider() trace.set_tracer_provider(tracer_provider) PyramidInstrumentor().instrument() self.config = Configurator() self._common_initialization(self.config) def tearDown(self) -> None: super().tearDown() with self.disable_logging(): PyramidInstrumentor().uninstrument() def test_custom_header_non_recording_span(self): try: resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) except Exception as exc: # pylint: disable=W0703 self.fail(f"Exception raised with NonRecordingSpan {exc}") test_programmatic.py000066400000000000000000000247661527070072300400100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-pyramid/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest.mock import Mock, patch from pyramid.config import Configurator from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.propagators import ( TraceResponsePropagator, get_global_response_propagator, set_global_response_propagator, ) from opentelemetry.instrumentation.pyramid import PyramidInstrumentor from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_FLAVOR, HTTP_HOST, HTTP_METHOD, HTTP_ROUTE, HTTP_SCHEME, HTTP_SERVER_NAME, HTTP_STATUS_CODE, HTTP_TARGET, HTTP_URL, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_HOST_NAME, NET_HOST_PORT, ) from opentelemetry.semconv.attributes import exception_attributes from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.util.http import get_excluded_urls # pylint: disable=import-error from .pyramid_base_test import InstrumentationTest def expected_attributes(override_attributes): default_attributes = { HTTP_METHOD: "GET", HTTP_SERVER_NAME: "localhost", HTTP_SCHEME: "http", NET_HOST_PORT: 80, NET_HOST_NAME: "localhost", HTTP_HOST: "localhost", HTTP_TARGET: "/", HTTP_FLAVOR: "1.1", HTTP_STATUS_CODE: 200, HTTP_URL: "http://localhost/", } for key, val in override_attributes.items(): default_attributes[key] = val return default_attributes class TestProgrammatic(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() # Reset semconv stability to ensure clean state _OpenTelemetrySemanticConventionStability._initialized = False config = Configurator() PyramidInstrumentor().instrument_config(config) self.config = config self._common_initialization(self.config) self.env_patch = patch.dict( "os.environ", {"OTEL_PYTHON_PYRAMID_EXCLUDED_URLS": "http://localhost/excluded_arg/123,excluded_noarg"}, ) self.env_patch.start() self.exclude_patch = patch( "opentelemetry.instrumentation.pyramid.callbacks._excluded_urls", get_excluded_urls("PYRAMID"), ) self.exclude_patch.start() def tearDown(self): super().tearDown() with self.disable_logging(): PyramidInstrumentor().uninstrument_config(self.config) def test_uninstrument(self): resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) self.assertEqual([b"Hello: 123"], list(resp.response)) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) PyramidInstrumentor().uninstrument_config(self.config) # Need to remake the WSGI app export self._common_initialization(self.config) resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) self.assertEqual([b"Hello: 123"], list(resp.response)) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) def test_simple(self): expected_attrs = expected_attributes( { HTTP_TARGET: "/hello/123", HTTP_ROUTE: "/hello/{helloid}", HTTP_URL: "http://localhost/hello/123", } ) self.client.get("/hello/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, "/hello/{helloid}") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) def test_response_headers(self): orig = get_global_response_propagator() set_global_response_propagator(TraceResponsePropagator()) response = self.client.get("/hello/500") self.assertTraceResponseHeaderMatchesSpan(response.headers, self.memory_exporter.get_finished_spans()[0]) set_global_response_propagator(orig) def test_not_recording(self): mock_tracer = Mock() mock_span = Mock() mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span with patch("opentelemetry.trace.get_tracer"): self.client.get("/hello/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) def test_404(self): expected_attrs = expected_attributes( { HTTP_METHOD: "POST", HTTP_TARGET: "/bye", HTTP_STATUS_CODE: 404, HTTP_URL: "http://localhost/bye", } ) resp = self.client.post("/bye") self.assertEqual(404, resp.status_code) resp.close() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, "POST /bye") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) self.assertEqual(len(span_list[0].events), 0) def test_internal_error(self): expected_attrs = expected_attributes( { HTTP_TARGET: "/hello/500", HTTP_ROUTE: "/hello/{helloid}", HTTP_STATUS_CODE: 500, HTTP_URL: "http://localhost/hello/500", } ) resp = self.client.get("/hello/500") self.assertEqual(500, resp.status_code) resp.close() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, "/hello/{helloid}") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) self.assertEqual(span_list[0].status.status_code, trace.StatusCode.ERROR) self.assertIn("HTTPInternalServerError", span_list[0].status.description) self.assertEqual( span_list[0].events[0].attributes[exception_attributes.EXCEPTION_TYPE], "pyramid.httpexceptions.HTTPInternalServerError", ) def test_internal_exception(self): expected_attrs = expected_attributes( { HTTP_TARGET: "/hello/900", HTTP_ROUTE: "/hello/{helloid}", HTTP_STATUS_CODE: 500, HTTP_URL: "http://localhost/hello/900", } ) with self.assertRaises(NotImplementedError): resp = self.client.get("/hello/900") resp.close() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, "/hello/{helloid}") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) self.assertEqual(span_list[0].status.status_code, trace.StatusCode.ERROR) self.assertEqual(span_list[0].status.description, "error message") expected_error_event_attrs = { exception_attributes.EXCEPTION_TYPE: "NotImplementedError", exception_attributes.EXCEPTION_MESSAGE: "error message", } self.assertEqual(span_list[0].events[0].name, "exception") # Ensure exception event has specific attributes, but allow additional ones self.assertLess( expected_error_event_attrs.items(), dict(span_list[0].events[0].attributes).items(), ) def test_tween_list(self): tween_list = "opentelemetry.instrumentation.pyramid.trace_tween_factory\npyramid.tweens.excview_tween_factory" config = Configurator(settings={"pyramid.tweens": tween_list}) PyramidInstrumentor().instrument_config(config) self._common_initialization(config) resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) self.assertEqual([b"Hello: 123"], list(resp.response)) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) PyramidInstrumentor().uninstrument_config(config) # Need to remake the WSGI app export self._common_initialization(config) resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) self.assertEqual([b"Hello: 123"], list(resp.response)) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) @patch("opentelemetry.instrumentation.pyramid.callbacks._logger") def test_warnings(self, mock_logger): tween_list = "pyramid.tweens.excview_tween_factory" config = Configurator(settings={"pyramid.tweens": tween_list}) PyramidInstrumentor().instrument_config(config) self._common_initialization(config) self.client.get("/hello/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) self.assertEqual(mock_logger.warning.called, True) mock_logger.warning.called = False tween_list = "opentelemetry.instrumentation.pyramid.trace_tween_factory" config = Configurator(settings={"pyramid.tweens": tween_list}) self._common_initialization(config) self.client.get("/hello/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) self.assertEqual(mock_logger.warning.called, True) def test_exclude_lists(self): self.client.get("/excluded_arg/123") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) self.client.get("/excluded_arg/125") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.client.get("/excluded_noarg") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.client.get("/excluded_noarg2") span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-redis/000077500000000000000000000000001527070072300322515ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-redis/LICENSE000066400000000000000000000261351527070072300332650ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-redis/README.rst000066400000000000000000000013201527070072300337340ustar00rootroot00000000000000OpenTelemetry Redis Instrumentation =================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-redis.svg :target: https://pypi.org/project/opentelemetry-instrumentation-redis/ This library allows tracing requests made by the Redis library. Installation ------------ :: pip install opentelemetry-instrumentation-redis References ---------- * `OpenTelemetry Redis Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031361527070072300351110ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-redis[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-redis" dynamic = ["version"] description = "OpenTelemetry Redis instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "wrapt >= 1.12.1", ] [project.optional-dependencies] instruments = [ "redis >= 2.6", ] [project.entry-points.opentelemetry_instrumentor] redis = "opentelemetry.instrumentation.redis:RedisInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-redis" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/redis/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-redis/src/000077500000000000000000000000001527070072300330405ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300356555ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-redis/srcinstrumentation/000077500000000000000000000000001527070072300411205ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetryredis/000077500000000000000000000000001527070072300422265ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation__init__.py000066400000000000000000000634051527070072300443470ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # """ Instrument `redis`_ to report Redis queries. .. _redis: https://pypi.org/project/redis/ Instrument All Clients ---------------------- The easiest way to instrument all redis client instances is by ``RedisInstrumentor().instrument()``: .. code:: python from opentelemetry.instrumentation.redis import RedisInstrumentor import redis # Instrument redis RedisInstrumentor().instrument() # This will report a span with the default settings client = redis.StrictRedis(host="localhost", port=6379) client.get("my-key") Async Redis clients (i.e. ``redis.asyncio.Redis``) are also instrumented in the same way: .. code:: python from opentelemetry.instrumentation.redis import RedisInstrumentor import redis.asyncio # Instrument redis RedisInstrumentor().instrument() # This will report a span with the default settings async def redis_get(): client = redis.asyncio.Redis(host="localhost", port=6379) await client.get("my-key") .. note:: Calling the ``instrument`` method will instrument the client classes, so any client created after the ``instrument`` call will be instrumented. To instrument only a single client, use :func:`RedisInstrumentor.instrument_client` method. Instrument Single Client ------------------------ The :func:`RedisInstrumentor.instrument_client` can instrument a connection instance. This is useful when there are multiple clients with a different redis database index. Or, you might have a different connection pool used for an application function you don't want instrumented. .. code:: python from opentelemetry.instrumentation.redis import RedisInstrumentor import redis instrumented_client = redis.Redis() not_instrumented_client = redis.Redis() # Instrument redis RedisInstrumentor.instrument_client(client=instrumented_client) # This will report a span with the default settings instrumented_client.get("my-key") # This will not have a span not_instrumented_client.get("my-key") .. warning:: All client instances created after calling ``RedisInstrumentor().instrument`` will be instrumented. To avoid instrumenting all clients, use :func:`RedisInstrumentor.instrument_client` . Request/Response Hooks ---------------------- .. code:: python from opentelemetry.instrumentation.redis import RedisInstrumentor import redis def request_hook(span, instance, args, kwargs): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_request_hook", "some-value") def response_hook(span, instance, response): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_response_hook", "some-value") # Instrument redis with hooks RedisInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook) # This will report a span with the default settings and the custom attributes added from the hooks client = redis.StrictRedis(host="localhost", port=6379) client.get("my-key") Suppress Instrumentation ------------------------ You can use the ``suppress_instrumentation`` context manager to prevent instrumentation from being applied to specific Redis operations. This is useful when you want to avoid creating spans for internal operations, health checks, or during specific code paths. .. code:: python from opentelemetry.instrumentation.redis import RedisInstrumentor from opentelemetry.instrumentation.utils import suppress_instrumentation import redis # Instrument redis RedisInstrumentor().instrument() client = redis.StrictRedis(host="localhost", port=6379) # This will report a span client.get("my-key") # This will NOT report a span with suppress_instrumentation(): client.get("internal-key") client.set("cache-key", "value") # This will report a span again client.get("another-key") API --- """ from __future__ import annotations import logging from collections.abc import Callable, Collection from typing import TYPE_CHECKING, Any import redis from wrapt import wrap_function_wrapper from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( _get_schema_url_for_signal_types, _get_semconv_opt_in_modes, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _set_db_statement, ) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.redis.package import _instruments from opentelemetry.instrumentation.redis.util import ( _add_create_attributes, _add_search_attributes, _build_span_meta_data_for_pipeline, _build_span_name, _format_command_args, _set_connection_attributes, ) from opentelemetry.instrumentation.redis.version import __version__ from opentelemetry.instrumentation.utils import ( is_instrumentation_enabled, unwrap, ) from opentelemetry.trace import ( StatusCode, Tracer, TracerProvider, get_tracer, ) if TYPE_CHECKING: from collections.abc import Awaitable import redis.asyncio.client import redis.asyncio.cluster import redis.client import redis.cluster import redis.connection from opentelemetry.instrumentation.redis.custom_types import ( AsyncPipelineInstance, AsyncRedisInstance, PipelineInstance, R, RedisInstance, RequestHook, ResponseHook, ) _logger = logging.getLogger(__name__) _REDIS_ASYNCIO_VERSION = (4, 2, 0) _REDIS_CLUSTER_VERSION = (4, 1, 0) _REDIS_ASYNCIO_CLUSTER_VERSION = (4, 3, 2) _CLIENT_ASYNCIO_SUPPORT = redis.VERSION >= _REDIS_ASYNCIO_VERSION _CLIENT_ASYNCIO_CLUSTER_SUPPORT = redis.VERSION >= _REDIS_ASYNCIO_CLUSTER_VERSION _CLIENT_CLUSTER_SUPPORT = redis.VERSION >= _REDIS_CLUSTER_VERSION _CLIENT_BEFORE_V3 = redis.VERSION < (3, 0, 0) if _CLIENT_ASYNCIO_SUPPORT: import redis.asyncio _INSTRUMENTATION_ATTR = "_is_instrumented_by_opentelemetry" def _execute_hook(hook: Callable[..., None], *args: Any) -> None: try: hook(*args) # pylint: disable-next=broad-exception-caught except Exception: _logger.warning("Exception raised by hook %r", hook, exc_info=True) def _traced_execute_factory( tracer: Tracer, request_hook: RequestHook | None = None, response_hook: ResponseHook | None = None, ): sem_conv_opt_in_modes = _get_semconv_opt_in_modes( ( _OpenTelemetryStabilitySignalType.DATABASE, _OpenTelemetryStabilitySignalType.HTTP, ) ) db_sem_conv_opt_in_mode = sem_conv_opt_in_modes[_OpenTelemetryStabilitySignalType.DATABASE] http_sem_conv_opt_in_mode = sem_conv_opt_in_modes[_OpenTelemetryStabilitySignalType.HTTP] def _traced_execute_command( func: Callable[..., R], instance: RedisInstance, args: tuple[Any, ...], kwargs: dict[str, Any], ) -> R: if not is_instrumentation_enabled(): return func(*args, **kwargs) query = _format_command_args(args) name = _build_span_name(instance, args) with tracer.start_as_current_span(name, kind=trace.SpanKind.CLIENT) as span: if span.is_recording(): span_attrs = {} _set_db_statement(span_attrs, query, db_sem_conv_opt_in_mode) span_attrs["db.redis.args_length"] = len(args) # Set all DB attributes for key, value in span_attrs.items(): span.set_attribute(key, value) _set_connection_attributes( span, instance, db_sem_conv_opt_in_mode, http_sem_conv_opt_in_mode, ) if span.name == "redis.create_index": _add_create_attributes(span, args) if callable(request_hook): _execute_hook(request_hook, span, instance, args, kwargs) response = func(*args, **kwargs) if span.is_recording(): if span.name == "redis.search": _add_search_attributes(span, response, args) if callable(response_hook): _execute_hook(response_hook, span, instance, response) return response return _traced_execute_command def _traced_execute_pipeline_factory( tracer: Tracer, request_hook: RequestHook | None = None, response_hook: ResponseHook | None = None, ): sem_conv_opt_in_modes = _get_semconv_opt_in_modes( ( _OpenTelemetryStabilitySignalType.DATABASE, _OpenTelemetryStabilitySignalType.HTTP, ) ) db_sem_conv_opt_in_mode = sem_conv_opt_in_modes[_OpenTelemetryStabilitySignalType.DATABASE] http_sem_conv_opt_in_mode = sem_conv_opt_in_modes[_OpenTelemetryStabilitySignalType.HTTP] def _traced_execute_pipeline( func: Callable[..., R], instance: PipelineInstance, args: tuple[Any, ...], kwargs: dict[str, Any], ) -> R: if not is_instrumentation_enabled(): return func(*args, **kwargs) ( command_stack, resource, span_name, ) = _build_span_meta_data_for_pipeline(instance) exception = None with tracer.start_as_current_span(span_name, kind=trace.SpanKind.CLIENT) as span: if span.is_recording(): span_attrs = {} _set_db_statement(span_attrs, resource, db_sem_conv_opt_in_mode) span_attrs["db.redis.pipeline_length"] = len(command_stack) # Set all DB attributes for key, value in span_attrs.items(): span.set_attribute(key, value) _set_connection_attributes( span, instance, db_sem_conv_opt_in_mode, http_sem_conv_opt_in_mode, ) response = None try: response = func(*args, **kwargs) except redis.WatchError as watch_exception: span.set_status(StatusCode.UNSET) exception = watch_exception if callable(response_hook): _execute_hook(response_hook, span, instance, response) if exception: raise exception return response return _traced_execute_pipeline def _async_traced_execute_factory( tracer: Tracer, request_hook: RequestHook | None = None, response_hook: ResponseHook | None = None, ): sem_conv_opt_in_modes = _get_semconv_opt_in_modes( ( _OpenTelemetryStabilitySignalType.DATABASE, _OpenTelemetryStabilitySignalType.HTTP, ) ) db_sem_conv_opt_in_mode = sem_conv_opt_in_modes[_OpenTelemetryStabilitySignalType.DATABASE] http_sem_conv_opt_in_mode = sem_conv_opt_in_modes[_OpenTelemetryStabilitySignalType.HTTP] async def _async_traced_execute_command( func: Callable[..., Awaitable[R]], instance: AsyncRedisInstance, args: tuple[Any, ...], kwargs: dict[str, Any], ) -> Awaitable[R]: if not is_instrumentation_enabled(): return await func(*args, **kwargs) query = _format_command_args(args) name = _build_span_name(instance, args) with tracer.start_as_current_span(name, kind=trace.SpanKind.CLIENT) as span: if span.is_recording(): span_attrs = {} _set_db_statement(span_attrs, query, db_sem_conv_opt_in_mode) span_attrs["db.redis.args_length"] = len(args) # Set all DB attributes for key, value in span_attrs.items(): span.set_attribute(key, value) _set_connection_attributes( span, instance, db_sem_conv_opt_in_mode, http_sem_conv_opt_in_mode, ) if callable(request_hook): _execute_hook(request_hook, span, instance, args, kwargs) response = await func(*args, **kwargs) if callable(response_hook): _execute_hook(response_hook, span, instance, response) return response return _async_traced_execute_command def _async_traced_execute_pipeline_factory( tracer: Tracer, request_hook: RequestHook | None = None, response_hook: ResponseHook | None = None, ): sem_conv_opt_in_modes = _get_semconv_opt_in_modes( ( _OpenTelemetryStabilitySignalType.DATABASE, _OpenTelemetryStabilitySignalType.HTTP, ) ) db_sem_conv_opt_in_mode = sem_conv_opt_in_modes[_OpenTelemetryStabilitySignalType.DATABASE] http_sem_conv_opt_in_mode = sem_conv_opt_in_modes[_OpenTelemetryStabilitySignalType.HTTP] async def _async_traced_execute_pipeline( func: Callable[..., Awaitable[R]], instance: AsyncPipelineInstance, args: tuple[Any, ...], kwargs: dict[str, Any], ) -> Awaitable[R]: if not is_instrumentation_enabled(): return await func(*args, **kwargs) ( command_stack, resource, span_name, ) = _build_span_meta_data_for_pipeline(instance) exception = None with tracer.start_as_current_span(span_name, kind=trace.SpanKind.CLIENT) as span: if span.is_recording(): span_attrs = {} _set_db_statement(span_attrs, resource, db_sem_conv_opt_in_mode) span_attrs["db.redis.pipeline_length"] = len(command_stack) # Set all DB attributes for key, value in span_attrs.items(): span.set_attribute(key, value) _set_connection_attributes( span, instance, db_sem_conv_opt_in_mode, http_sem_conv_opt_in_mode, ) response = None try: response = await func(*args, **kwargs) except redis.WatchError as watch_exception: span.set_status(StatusCode.UNSET) exception = watch_exception if callable(response_hook): _execute_hook(response_hook, span, instance, response) if exception: raise exception return response return _async_traced_execute_pipeline # pylint: disable=R0915 def _instrument( tracer: Tracer, request_hook: RequestHook | None = None, response_hook: ResponseHook | None = None, ): _traced_execute_command = _traced_execute_factory(tracer, request_hook, response_hook) _traced_execute_pipeline = _traced_execute_pipeline_factory(tracer, request_hook, response_hook) pipeline_class = "BasePipeline" if _CLIENT_BEFORE_V3 else "Pipeline" redis_class = "StrictRedis" if _CLIENT_BEFORE_V3 else "Redis" wrap_function_wrapper("redis", f"{redis_class}.execute_command", _traced_execute_command) wrap_function_wrapper( "redis.client", f"{pipeline_class}.execute", _traced_execute_pipeline, ) wrap_function_wrapper( "redis.client", f"{pipeline_class}.immediate_execute_command", _traced_execute_command, ) if _CLIENT_CLUSTER_SUPPORT: wrap_function_wrapper( "redis.cluster", "RedisCluster.execute_command", _traced_execute_command, ) wrap_function_wrapper( "redis.cluster", "ClusterPipeline.execute", _traced_execute_pipeline, ) _async_traced_execute_command = _async_traced_execute_factory(tracer, request_hook, response_hook) _async_traced_execute_pipeline = _async_traced_execute_pipeline_factory(tracer, request_hook, response_hook) if _CLIENT_ASYNCIO_SUPPORT: wrap_function_wrapper( "redis.asyncio", f"{redis_class}.execute_command", _async_traced_execute_command, ) wrap_function_wrapper( "redis.asyncio.client", f"{pipeline_class}.execute", _async_traced_execute_pipeline, ) wrap_function_wrapper( "redis.asyncio.client", f"{pipeline_class}.immediate_execute_command", _async_traced_execute_command, ) if _CLIENT_ASYNCIO_CLUSTER_SUPPORT: wrap_function_wrapper( "redis.asyncio.cluster", "RedisCluster.execute_command", _async_traced_execute_command, ) wrap_function_wrapper( "redis.asyncio.cluster", "ClusterPipeline.execute", _async_traced_execute_pipeline, ) def _instrument_client( client, tracer: Tracer, request_hook: RequestHook | None = None, response_hook: ResponseHook | None = None, ): # first, handle async clients and cluster clients _async_traced_execute = _async_traced_execute_factory(tracer, request_hook, response_hook) _async_traced_execute_pipeline = _async_traced_execute_pipeline_factory(tracer, request_hook, response_hook) if _CLIENT_ASYNCIO_SUPPORT and isinstance(client, redis.asyncio.Redis): def _async_pipeline_wrapper(func, instance, args, kwargs): result = func(*args, **kwargs) wrap_function_wrapper(result, "execute", _async_traced_execute_pipeline) wrap_function_wrapper(result, "immediate_execute_command", _async_traced_execute) return result wrap_function_wrapper(client, "execute_command", _async_traced_execute) wrap_function_wrapper(client, "pipeline", _async_pipeline_wrapper) return if _CLIENT_ASYNCIO_CLUSTER_SUPPORT and isinstance(client, redis.asyncio.RedisCluster): def _async_cluster_pipeline_wrapper(func, instance, args, kwargs): result = func(*args, **kwargs) wrap_function_wrapper(result, "execute", _async_traced_execute_pipeline) return result wrap_function_wrapper(client, "execute_command", _async_traced_execute) wrap_function_wrapper(client, "pipeline", _async_cluster_pipeline_wrapper) return # for redis.client.Redis, redis.Cluster and v3.0.0 redis.client.StrictRedis # the wrappers are the same _traced_execute = _traced_execute_factory(tracer, request_hook, response_hook) _traced_execute_pipeline = _traced_execute_pipeline_factory(tracer, request_hook, response_hook) def _pipeline_wrapper(func, instance, args, kwargs): result = func(*args, **kwargs) wrap_function_wrapper(result, "execute", _traced_execute_pipeline) wrap_function_wrapper(result, "immediate_execute_command", _traced_execute) return result wrap_function_wrapper( client, "execute_command", _traced_execute, ) wrap_function_wrapper( client, "pipeline", _pipeline_wrapper, ) class RedisInstrumentor(BaseInstrumentor): @staticmethod def _get_tracer(**kwargs): # Initialize semantic conventions opt-in if needed _OpenTelemetrySemanticConventionStability._initialize() # Redis instrumentation supports both DATABASE and HTTP signal types signal_types = [ _OpenTelemetryStabilitySignalType.DATABASE, _OpenTelemetryStabilitySignalType.HTTP, ] tracer_provider = kwargs.get("tracer_provider") return get_tracer( __name__, __version__, tracer_provider=tracer_provider, schema_url=_get_schema_url_for_signal_types(signal_types), ) def instrument( self, tracer_provider: TracerProvider | None = None, request_hook: RequestHook | None = None, response_hook: ResponseHook | None = None, **kwargs, ): """Instruments all Redis/StrictRedis/RedisCluster and async client instances. Args: tracer_provider: A TracerProvider, defaults to global. request_hook: a function with extra user-defined logic to run before performing the request. The ``args`` is a tuple, where items are command arguments. For example ``client.set("mykey", "value", ex=5)`` would have ``args`` as ``('SET', 'mykey', 'value', 'EX', 5)``. The ``kwargs`` represents occasional ``options`` passed by redis. For example, if you use ``client.set("mykey", "value", get=True)``, the ``kwargs`` would be ``{'get': True}``. response_hook: a function with extra user-defined logic to run after the request is complete. The ``args`` represents the response. """ super().instrument( tracer_provider=tracer_provider, request_hook=request_hook, response_hook=response_hook, **kwargs, ) def _instrument(self, **kwargs: Any): """Instruments the redis module Args: **kwargs: Optional arguments ``tracer_provider``: a TracerProvider, defaults to global. ``request_hook``: An optional callback that is invoked right after a span is created. ``response_hook``: An optional callback which is invoked right before the span is finished processing a response. """ _instrument( self._get_tracer(**kwargs), request_hook=kwargs.get("request_hook"), response_hook=kwargs.get("response_hook"), ) def _uninstrument(self, **kwargs: Any): if _CLIENT_BEFORE_V3: unwrap(redis.StrictRedis, "execute_command") unwrap(redis.StrictRedis, "pipeline") unwrap(redis.Redis, "pipeline") unwrap( redis.client.BasePipeline, # pylint:disable=no-member "execute", ) unwrap( redis.client.BasePipeline, # pylint:disable=no-member "immediate_execute_command", ) else: unwrap(redis.Redis, "execute_command") unwrap(redis.Redis, "pipeline") unwrap(redis.client.Pipeline, "execute") unwrap(redis.client.Pipeline, "immediate_execute_command") if _CLIENT_CLUSTER_SUPPORT: unwrap(redis.cluster.RedisCluster, "execute_command") unwrap(redis.cluster.ClusterPipeline, "execute") if _CLIENT_ASYNCIO_SUPPORT: unwrap(redis.asyncio.Redis, "execute_command") unwrap(redis.asyncio.Redis, "pipeline") unwrap(redis.asyncio.client.Pipeline, "execute") unwrap(redis.asyncio.client.Pipeline, "immediate_execute_command") if _CLIENT_ASYNCIO_CLUSTER_SUPPORT: unwrap(redis.asyncio.cluster.RedisCluster, "execute_command") unwrap(redis.asyncio.cluster.ClusterPipeline, "execute") @staticmethod def instrument_client( client: redis.StrictRedis | redis.Redis | redis.asyncio.Redis | redis.cluster.RedisCluster | redis.asyncio.cluster.RedisCluster, tracer_provider: TracerProvider | None = None, request_hook: RequestHook | None = None, response_hook: ResponseHook | None = None, ): """Instrument the provided Redis Client. The client can be sync or async. Cluster client is also supported. Args: client: The redis client. tracer_provider: A TracerProvider, defaults to global. request_hook: a function with extra user-defined logic to run before performing the request. The ``args`` is a tuple, where items are command arguments. For example ``client.set("mykey", "value", ex=5)`` would have ``args`` as ``('SET', 'mykey', 'value', 'EX', 5)``. The ``kwargs`` represents occasional ``options`` passed by redis. For example, if you use ``client.set("mykey", "value", get=True)``, the ``kwargs`` would be ``{'get': True}``. response_hook: a function with extra user-defined logic to run after the request is complete. The ``args`` represents the response. """ if not hasattr(client, _INSTRUMENTATION_ATTR): setattr(client, _INSTRUMENTATION_ATTR, False) if not getattr(client, _INSTRUMENTATION_ATTR): _instrument_client( client, RedisInstrumentor._get_tracer(tracer_provider=tracer_provider), request_hook=request_hook, response_hook=response_hook, ) setattr(client, _INSTRUMENTATION_ATTR, True) else: _logger.warning("Attempting to instrument Redis connection while already instrumented") @staticmethod def uninstrument_client( client: redis.StrictRedis | redis.Redis | redis.asyncio.Redis | redis.cluster.RedisCluster | redis.asyncio.cluster.RedisCluster, ): """Disables instrumentation for the given client instance Args: client: The redis client """ if getattr(client, _INSTRUMENTATION_ATTR): # for all clients we need to unwrap execute_command and pipeline functions unwrap(client, "execute_command") # the method was creating a pipeline and wrapping the functions of the # created instance. any pipelines created before un-instrumenting will # remain instrumented (pipelines should usually have a short span) unwrap(client, "pipeline") else: _logger.warning("Attempting to un-instrument Redis connection that wasn't instrumented") def instrumentation_dependencies(self) -> Collection[str]: """Return a list of python packages with versions that the will be instrumented.""" return _instruments custom_types.py000066400000000000000000000017161527070072300453430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Callable from typing import Any, TypeVar import redis.asyncio.client import redis.asyncio.cluster import redis.client import redis.cluster import redis.connection from opentelemetry.trace import Span RequestHook = Callable[[Span, redis.connection.Connection, list[Any], dict[str, Any]], None] ResponseHook = Callable[[Span, redis.connection.Connection, Any], None] AsyncPipelineInstance = TypeVar( "AsyncPipelineInstance", redis.asyncio.client.Pipeline, redis.asyncio.cluster.ClusterPipeline, ) AsyncRedisInstance = TypeVar("AsyncRedisInstance", redis.asyncio.Redis, redis.asyncio.RedisCluster) PipelineInstance = TypeVar( "PipelineInstance", redis.client.Pipeline, redis.cluster.ClusterPipeline, ) RedisInstance = TypeVar("RedisInstance", redis.client.Redis, redis.cluster.RedisCluster) R = TypeVar("R") package.py000066400000000000000000000002161527070072300441720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("redis >= 2.6",) _semconv_status = "migration" py.typed000066400000000000000000000000001527070072300437130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redisutil.py000066400000000000000000000173371527070072300435700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # """ Some utils used by the redis integration """ from __future__ import annotations from typing import TYPE_CHECKING, Any from opentelemetry.instrumentation._semconv import ( _set_db_redis_database_index, _set_db_system, _set_http_net_peer_name_client, _set_http_peer_port_client, _set_net_transport, ) from opentelemetry.semconv.attributes.network_attributes import ( NetworkTransportValues, ) from opentelemetry.semconv.trace import ( DbSystemValues, NetTransportValues, ) from opentelemetry.trace import Span if TYPE_CHECKING: from opentelemetry.instrumentation.redis.custom_types import ( AsyncPipelineInstance, AsyncRedisInstance, PipelineInstance, RedisInstance, ) _FIELD_TYPES = ["NUMERIC", "TEXT", "GEO", "TAG", "VECTOR"] def _extract_conn_attributes(conn_kwargs, db_sem_conv_opt_in_mode, http_sem_conv_opt_in_mode): """Transform redis conn info into dict""" attributes = {} _set_db_system(attributes, DbSystemValues.REDIS.value, db_sem_conv_opt_in_mode) # redis-py may pass db=None explicitly (e.g. from_url); treat that as 0. db = conn_kwargs.get("db") if db is None: db = 0 _set_db_redis_database_index(attributes, db, db_sem_conv_opt_in_mode) if "path" in conn_kwargs: _set_http_net_peer_name_client(attributes, conn_kwargs.get("path", ""), http_sem_conv_opt_in_mode) _set_net_transport( attributes, NetTransportValues.OTHER.value, NetworkTransportValues.UNIX.value, http_sem_conv_opt_in_mode, ) else: _set_http_net_peer_name_client( attributes, conn_kwargs.get("host", "localhost"), http_sem_conv_opt_in_mode, ) _set_http_peer_port_client( attributes, conn_kwargs.get("port", 6379), http_sem_conv_opt_in_mode, ) _set_net_transport( attributes, NetTransportValues.IP_TCP.value, NetworkTransportValues.TCP.value, http_sem_conv_opt_in_mode, ) return attributes def _format_command_args(args: list[str]): """Format and sanitize command arguments, and trim them as needed""" cmd_max_len = 1000 value_too_long_mark = "..." # Sanitized query format: "COMMAND ? ?" args_length = len(args) if args_length > 0: out = [str(args[0])] + ["?"] * (args_length - 1) out_str = " ".join(out) if len(out_str) > cmd_max_len: out_str = out_str[: cmd_max_len - len(value_too_long_mark)] + value_too_long_mark else: out_str = "" return out_str def _set_span_attribute_if_value(span, name, value): if value is not None and value != "": span.set_attribute(name, value) def _value_or_none(values, n): try: return values[n] except IndexError: return None def _set_connection_attributes( span: Span, conn: RedisInstance | AsyncRedisInstance, db_sem_conv_opt_in_mode, http_sem_conv_opt_in_mode, ) -> None: if ( not span.is_recording() or not hasattr(conn, "connection_pool") or not hasattr(conn.connection_pool, "connection_kwargs") ): return for key, value in _extract_conn_attributes( conn.connection_pool.connection_kwargs, db_sem_conv_opt_in_mode, http_sem_conv_opt_in_mode, ).items(): span.set_attribute(key, value) def _build_span_name(instance: RedisInstance | AsyncRedisInstance, cmd_args: tuple[Any, ...]) -> str: if len(cmd_args) > 0 and cmd_args[0]: if cmd_args[0] == "FT.SEARCH": name = "redis.search" elif cmd_args[0] == "FT.CREATE": name = "redis.create_index" else: name = cmd_args[0] else: name = instance.connection_pool.connection_kwargs.get("db") if name is None: name = 0 return str(name) def _add_create_attributes(span: Span, args: tuple[Any, ...]): _set_span_attribute_if_value(span, "redis.create_index.index", _value_or_none(args, 1)) # According to: https://github.com/redis/redis-py/blob/master/redis/commands/search/commands.py#L155 schema is last argument for execute command try: schema_index = args.index("SCHEMA") except ValueError: return schema = args[schema_index:] field_attribute = "" # Schema in format: # [first_field_name, first_field_type, first_field_some_attribute1, first_field_some_attribute2, second_field_name, ...] field_attribute = "".join( f"Field(name: {schema[index - 1]}, type: {schema[index]});" for index in range(1, len(schema)) if schema[index] in _FIELD_TYPES ) _set_span_attribute_if_value( span, "redis.create_index.fields", field_attribute, ) def _add_search_attributes(span: Span, response, args): _set_span_attribute_if_value(span, "redis.search.index", _value_or_none(args, 1)) _set_span_attribute_if_value(span, "redis.search.query", _value_or_none(args, 2)) # Parse response from search # https://redis.io/docs/latest/commands/ft.search/ # Response in format: # [number_of_returned_documents, index_of_first_returned_doc, first_doc(as a list), index_of_second_returned_doc, second_doc(as a list) ...] # Returned documents in array format: # [first_field_name, first_field_value, second_field_name, second_field_value ...] number_of_returned_documents = _value_or_none(response, 0) _set_span_attribute_if_value(span, "redis.search.total", number_of_returned_documents) if "NOCONTENT" in args or not number_of_returned_documents: return for document_number in range(number_of_returned_documents): document_index = _value_or_none(response, 1 + 2 * document_number) if document_index: document = response[2 + 2 * document_number] for attribute_name_index in range(0, len(document), 2): _set_span_attribute_if_value( span, f"redis.search.xdoc_{document_index}.{document[attribute_name_index]}", document[attribute_name_index + 1], ) def _build_span_meta_data_for_pipeline( instance: PipelineInstance | AsyncPipelineInstance, ) -> tuple[list[Any], str, str]: try: # redis-py 6+ ClusterPipeline no longer updates ``command_stack``; # queued commands are tracked on the execution strategy instead. The # sync cluster strategy exposes them via a public ``command_queue`` # property, while the async cluster strategy only has the private # ``_command_queue`` attribute. Fall back to ``command_stack`` / # ``_command_stack`` for non-cluster pipelines and older redis-py. execution_strategy = getattr(instance, "_execution_strategy", None) if execution_strategy is not None and hasattr(execution_strategy, "command_queue"): command_stack = execution_strategy.command_queue elif execution_strategy is not None and hasattr(execution_strategy, "_command_queue"): command_stack = execution_strategy._command_queue elif hasattr(instance, "command_stack"): command_stack = instance.command_stack else: command_stack = instance._command_stack cmds = [_format_command_args(c.args if hasattr(c, "args") else c[0]) for c in command_stack] resource = "\n".join(cmds) span_name = " ".join([(c.args[0] if hasattr(c, "args") else c[0][0]) for c in command_stack]) except (AttributeError, IndexError): command_stack = [] resource = "" span_name = "" return command_stack, resource, span_name or "redis" version.py000066400000000000000000000001441527070072300442640ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000004771527070072300364430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-redisasgiref==3.8.1 async-timeout==4.0.3 Deprecated==1.2.14 fakeredis==2.23.3 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 redis==5.0.1 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-redis opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-redis/tests/000077500000000000000000000000001527070072300334135ustar00rootroot00000000000000__init__.py000066400000000000000000000001141527070072300354410ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-redis/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 test_redis.py000066400000000000000000001774001527070072300360640ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-redis/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines import asyncio import os from unittest import IsolatedAsyncioTestCase, mock from unittest.mock import AsyncMock, patch import fakeredis import pytest import redis import redis.asyncio from fakeredis.aioredis import FakeRedis from redis.exceptions import ConnectionError as redis_ConnectionError from redis.exceptions import WatchError from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.redis import RedisInstrumentor from opentelemetry.instrumentation.redis.util import ( _build_span_meta_data_for_pipeline, ) from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_REDIS_DATABASE_INDEX, DB_STATEMENT, DB_SYSTEM, DbSystemValues, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, NET_TRANSPORT, NetTransportValues, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_QUERY_TEXT, DB_SYSTEM_NAME, ) from opentelemetry.semconv.attributes.network_attributes import ( NETWORK_TRANSPORT, NetworkTransportValues, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind def stability_mode(mode): def decorator(test_case): @patch.dict(os.environ, {OTEL_SEMCONV_STABILITY_OPT_IN: mode}) def wrapper(*args, **kwargs): _OpenTelemetrySemanticConventionStability._initialized = False _OpenTelemetrySemanticConventionStability._initialize() return test_case(*args, **kwargs) return wrapper return decorator # pylint: disable=too-many-public-methods class TestRedis(TestBase): def assert_span_count(self, count: int): """ Assert that the memory exporter has the expected number of spans. Returns the spans list if assertion passes """ spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), count) return spans def setUp(self): super().setUp() RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) def tearDown(self): super().tearDown() RedisInstrumentor().uninstrument() def test_span_properties(self): redis_client = redis.Redis() with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "GET") self.assertEqual(span.kind, SpanKind.CLIENT) def test_not_recording(self): redis_client = redis.Redis() mock_tracer = mock.Mock() mock_span = mock.Mock() mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span with mock.patch("opentelemetry.trace.get_tracer") as tracer: with mock.patch.object(redis_client, "connection"): tracer.return_value = mock_tracer redis_client.get("key") self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) def test_instrument_uninstrument(self): redis_client = redis.Redis() with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.memory_exporter.clear() # Test uninstrument RedisInstrumentor().uninstrument() with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) self.memory_exporter.clear() # Test instrument again RedisInstrumentor().instrument() with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) def test_instrument_uninstrument_async_client_command(self): redis_client = redis.asyncio.Redis() with mock.patch.object(redis_client, "connection", AsyncMock()): asyncio.run(redis_client.get("key")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.memory_exporter.clear() # Test uninstrument RedisInstrumentor().uninstrument() with mock.patch.object(redis_client, "connection", AsyncMock()): asyncio.run(redis_client.get("key")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) self.memory_exporter.clear() # Test instrument again RedisInstrumentor().instrument() with mock.patch.object(redis_client, "connection", AsyncMock()): asyncio.run(redis_client.get("key")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) def test_response_hook(self): redis_client = redis.Redis() connection = redis.connection.Connection() redis_client.connection = connection response_attribute_name = "db.redis.response" def response_hook(span, conn, response): span.set_attribute(response_attribute_name, response) RedisInstrumentor().uninstrument() RedisInstrumentor().instrument(tracer_provider=self.tracer_provider, response_hook=response_hook) test_value = "test_value" with mock.patch.object(connection, "send_command"): with mock.patch.object(redis_client, "parse_response", return_value=test_value): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes.get(response_attribute_name), test_value) def test_request_hook(self): redis_client = redis.Redis() connection = redis.connection.Connection() redis_client.connection = connection custom_attribute_name = "my.request.attribute" def request_hook(span, conn, args, kwargs): if span and span.is_recording(): span.set_attribute(custom_attribute_name, args[0]) RedisInstrumentor().uninstrument() RedisInstrumentor().instrument(tracer_provider=self.tracer_provider, request_hook=request_hook) test_value = "test_value" with mock.patch.object(connection, "send_command"): with mock.patch.object(redis_client, "parse_response", return_value=test_value): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes.get(custom_attribute_name), "GET") def test_request_hook_exception(self): def request_hook(_span, _conn, _args, _kwargs): raise ValueError("hook error") redis_client = redis.Redis() connection = redis.connection.Connection() redis_client.connection = connection RedisInstrumentor().uninstrument() RedisInstrumentor().instrument(tracer_provider=self.tracer_provider, request_hook=request_hook) with self.assertLogs("opentelemetry.instrumentation.redis", level="WARNING") as log_ctx: with mock.patch.object(connection, "send_command"): with mock.patch.object(redis_client, "parse_response", return_value="ok"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertTrue(any("request_hook" in msg for msg in log_ctx.output)) def test_response_hook_exception(self): def response_hook(_span, _conn, _response): raise ValueError("hook error") redis_client = redis.Redis() connection = redis.connection.Connection() redis_client.connection = connection RedisInstrumentor().uninstrument() RedisInstrumentor().instrument(tracer_provider=self.tracer_provider, response_hook=response_hook) with self.assertLogs("opentelemetry.instrumentation.redis", level="WARNING") as log_ctx: with mock.patch.object(connection, "send_command"): with mock.patch.object(redis_client, "parse_response", return_value="ok"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertTrue(any("response_hook" in msg for msg in log_ctx.output)) def test_query_sanitizer_enabled(self): redis_client = redis.Redis() connection = redis.connection.Connection() redis_client.connection = connection RedisInstrumentor().uninstrument() RedisInstrumentor().instrument( tracer_provider=self.tracer_provider, sanitize_query=True, ) with mock.patch.object(redis_client, "connection"): redis_client.set("key", "value") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes.get("db.statement"), "SET ? ?") def test_query_sanitizer(self): redis_client = redis.Redis() connection = redis.connection.Connection() redis_client.connection = connection with mock.patch.object(redis_client, "connection"): redis_client.set("key", "value") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes.get("db.statement"), "SET ? ?") def test_no_op_tracer_provider(self): RedisInstrumentor().uninstrument() tracer_provider = trace.NoOpTracerProvider() RedisInstrumentor().instrument(tracer_provider=tracer_provider) redis_client = redis.Redis() with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_attributes_default(self): redis_client = redis.Redis() with mock.patch.object(redis_client, "connection"): redis_client.set("key", "value") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual( span.attributes[DB_SYSTEM], DbSystemValues.REDIS.value, ) self.assertEqual(span.attributes[DB_REDIS_DATABASE_INDEX], 0) self.assertEqual(span.attributes[NET_PEER_NAME], "localhost") self.assertEqual(span.attributes[NET_PEER_PORT], 6379) self.assertEqual( span.attributes[NET_TRANSPORT], NetTransportValues.IP_TCP.value, ) def test_attributes_db_none(self): """db=None in connection kwargs should default to index 0.""" redis_client = redis.Redis() redis_client.connection_pool.connection_kwargs["db"] = None with self.assertNoLogs("opentelemetry.attributes", level="WARNING"): with mock.patch.object(redis_client, "connection"): redis_client.set("key", "value") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.attributes[DB_REDIS_DATABASE_INDEX], 0) self.assertEqual( span.attributes[DB_SYSTEM], DbSystemValues.REDIS.value, ) def test_attributes_tcp(self): redis_client = redis.Redis.from_url("redis://foo:bar@1.1.1.1:6380/1") with mock.patch.object(redis_client, "connection"): redis_client.set("key", "value") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual( span.attributes[DB_SYSTEM], DbSystemValues.REDIS.value, ) self.assertEqual(span.attributes[DB_REDIS_DATABASE_INDEX], 1) self.assertEqual(span.attributes[NET_PEER_NAME], "1.1.1.1") self.assertEqual(span.attributes[NET_PEER_PORT], 6380) self.assertEqual( span.attributes[NET_TRANSPORT], NetTransportValues.IP_TCP.value, ) def test_attributes_unix_socket(self): redis_client = redis.Redis.from_url("unix://foo@/path/to/socket.sock?db=3&password=bar") with mock.patch.object(redis_client, "connection"): redis_client.set("key", "value") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual( span.attributes[DB_SYSTEM], DbSystemValues.REDIS.value, ) self.assertEqual(span.attributes[DB_REDIS_DATABASE_INDEX], 3) self.assertEqual( span.attributes[NET_PEER_NAME], "/path/to/socket.sock", ) self.assertEqual( span.attributes[NET_TRANSPORT], NetTransportValues.OTHER.value, ) def test_connection_pool_without_connection_kwargs(self): redis_client = redis.Redis() redis_client.connection_pool = mock.Mock(spec=["disconnect"]) with mock.patch.object(redis_client, "connection"): redis_client.set("key", "value") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.status.status_code, trace.StatusCode.UNSET) self.assertEqual(span.events, ()) # The base db.statement is still recorded; connection-derived # attributes are simply absent when they cannot be extracted. self.assertIn(DB_STATEMENT, span.attributes) self.assertNotIn(DB_SYSTEM, span.attributes) self.assertNotIn(NET_PEER_NAME, span.attributes) def test_connection_pool_without_connection_kwargs_async(self): redis_client = redis.asyncio.Redis() redis_client.connection_pool = mock.Mock(spec=["disconnect"]) with mock.patch.object(redis_client, "connection", AsyncMock()): asyncio.run(redis_client.set("key", "value")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.status.status_code, trace.StatusCode.UNSET) self.assertEqual(span.events, ()) self.assertIn(DB_STATEMENT, span.attributes) self.assertNotIn(DB_SYSTEM, span.attributes) self.assertNotIn(NET_PEER_NAME, span.attributes) def test_connection_error(self): server = fakeredis.FakeServer() server.connected = False redis_client = fakeredis.FakeStrictRedis(server=server) try: redis_client.set("foo", "bar") except redis_ConnectionError: pass spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "SET") self.assertEqual(span.kind, SpanKind.CLIENT) self.assertEqual(span.status.status_code, trace.StatusCode.ERROR) def test_response_error(self): redis_client = fakeredis.FakeStrictRedis() redis_client.lpush("mylist", "value") try: redis_client.incr("mylist") # Trying to increment a list, which is invalid except redis.ResponseError: pass spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) span = spans[0] self.assertEqual(span.name, "LPUSH") self.assertEqual(span.kind, SpanKind.CLIENT) self.assertEqual(span.status.status_code, trace.StatusCode.UNSET) span = spans[1] self.assertEqual(span.name, "INCRBY") self.assertEqual(span.kind, SpanKind.CLIENT) self.assertEqual(span.status.status_code, trace.StatusCode.ERROR) def test_watch_error_sync(self): def redis_operations(): with pytest.raises(WatchError): redis_client = fakeredis.FakeStrictRedis() pipe = redis_client.pipeline(transaction=True) pipe.watch("a") redis_client.set("a", "bad") # This will cause the WatchError pipe.multi() pipe.set("a", "1") pipe.execute() redis_operations() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) # there should be 3 tests, we start watch operation and have 2 set operation on same key self.assertEqual(len(spans), 3) self.assertEqual(spans[0].attributes.get("db.statement"), "WATCH ?") self.assertEqual(spans[0].kind, SpanKind.CLIENT) self.assertEqual(spans[0].status.status_code, trace.StatusCode.UNSET) for span in spans[1:]: self.assertEqual(span.attributes.get("db.statement"), "SET ? ?") self.assertEqual(span.kind, SpanKind.CLIENT) self.assertEqual(span.status.status_code, trace.StatusCode.UNSET) def test_span_name_empty_pipeline(self): redis_client = fakeredis.FakeStrictRedis() pipe = redis_client.pipeline() pipe.execute() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].name, "redis") self.assertEqual(spans[0].kind, SpanKind.CLIENT) self.assertEqual(spans[0].status.status_code, trace.StatusCode.UNSET) def test_suppress_instrumentation_command(self): redis_client = redis.Redis() with mock.patch.object(redis_client, "connection"): # Execute command with suppression with suppress_instrumentation(): redis_client.get("key") # No spans should be created spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) # Verify that instrumentation works again after exiting the context with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) def test_suppress_instrumentation_pipeline(self): redis_client = fakeredis.FakeStrictRedis() with suppress_instrumentation(): pipe = redis_client.pipeline() pipe.set("key1", "value1") pipe.set("key2", "value2") pipe.get("key1") pipe.execute() # No spans should be created spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) # Verify that instrumentation works again after exiting the context pipe = redis_client.pipeline() pipe.set("key3", "value3") pipe.execute() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) # Pipeline span could be "SET" or "redis.pipeline" depending on implementation self.assertIn(spans[0].name, ["SET", "redis.pipeline"]) def test_suppress_instrumentation_mixed(self): redis_client = redis.Redis() # Regular instrumented call with mock.patch.object(redis_client, "connection"): redis_client.set("key1", "value1") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.memory_exporter.clear() # Suppressed call with suppress_instrumentation(): with mock.patch.object(redis_client, "connection"): redis_client.set("key2", "value2") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) # Another regular instrumented call with mock.patch.object(redis_client, "connection"): redis_client.get("key1") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) class TestRedisAsync(TestBase, IsolatedAsyncioTestCase): def assert_span_count(self, count: int): """ Assert that the memory exporter has the expected number of spans. Returns the spans list if assertion passes """ spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), count) return spans def setUp(self): super().setUp() self.instrumentor = RedisInstrumentor() self.client: FakeRedis = FakeRedis() @staticmethod async def _redis_pipeline_operations(client: FakeRedis): with pytest.raises(WatchError): async with client.pipeline(transaction=False) as pipe: await pipe.watch("a") await client.set("a", "bad") pipe.multi() await pipe.set("a", "1") await pipe.execute() @pytest.mark.asyncio async def test_watch_error_async(self): # this tests also ensures the response_hook is called response_attr = "my.response.attribute" count = 0 def response_hook(span, conn, args): nonlocal count if span and span.is_recording(): span.set_attribute(response_attr, count) count += 1 self.instrumentor.instrument(tracer_provider=self.tracer_provider, response_hook=response_hook) redis_client = FakeRedis() await self._redis_pipeline_operations(redis_client) # there should be 3 tests, we start watch operation and have 2 set operation on same key spans = self.assert_span_count(3) self.assertEqual(spans[0].attributes.get("db.statement"), "WATCH ?") self.assertEqual(spans[0].kind, SpanKind.CLIENT) self.assertEqual(spans[0].status.status_code, trace.StatusCode.UNSET) self.assertEqual(spans[0].attributes.get(response_attr), 0) for span_index, span in enumerate(spans[1:], 1): self.assertEqual(span.attributes.get("db.statement"), "SET ? ?") self.assertEqual(span.kind, SpanKind.CLIENT) self.assertEqual(span.status.status_code, trace.StatusCode.UNSET) self.assertEqual(span.attributes.get(response_attr), span_index) RedisInstrumentor().uninstrument() @pytest.mark.asyncio async def test_watch_error_async_only_client(self): self.instrumentor.instrument_client(tracer_provider=self.tracer_provider, client=self.client) redis_client = FakeRedis() await self._redis_pipeline_operations(redis_client) spans = self.memory_exporter.get_finished_spans() # there should be 3 tests, we start watch operation and have 2 set operation on same key self.assertEqual(len(spans), 0) # now with the instrumented client we should get proper spans await self._redis_pipeline_operations(self.client) spans = self.memory_exporter.get_finished_spans() # there should be 3 tests, we start watch operation and have 2 set operation on same key self.assertEqual(len(spans), 3) self.assertEqual(spans[0].attributes.get("db.statement"), "WATCH ?") self.assertEqual(spans[0].kind, SpanKind.CLIENT) self.assertEqual(spans[0].status.status_code, trace.StatusCode.UNSET) for span in spans[1:]: self.assertEqual(span.attributes.get("db.statement"), "SET ? ?") self.assertEqual(span.kind, SpanKind.CLIENT) self.assertEqual(span.status.status_code, trace.StatusCode.UNSET) RedisInstrumentor().uninstrument_client(self.client) @pytest.mark.asyncio async def test_request_response_hooks(self): request_attr = "my.request.attribute" response_attr = "my.response.attribute" def request_hook(span, conn, args, kwargs): if span and span.is_recording(): span.set_attribute(request_attr, args[0]) def response_hook(span, conn, args): if span and span.is_recording(): span.set_attribute(response_attr, args) self.instrumentor.instrument( tracer_provider=self.tracer_provider, request_hook=request_hook, response_hook=response_hook, ) await self.client.set("key", "value") spans = self.assert_span_count(1) span = spans[0] self.assertEqual(span.attributes.get(request_attr), "SET") self.assertEqual(span.attributes.get(response_attr), True) self.instrumentor.uninstrument() @pytest.mark.asyncio async def test_request_response_hooks_connection_only(self): request_attr = "my.request.attribute" response_attr = "my.response.attribute" def request_hook(span, conn, args, kwargs): if span and span.is_recording(): span.set_attribute(request_attr, args[0]) def response_hook(span, conn, args): if span and span.is_recording(): span.set_attribute(response_attr, args) self.instrumentor.instrument_client( client=self.client, tracer_provider=self.tracer_provider, request_hook=request_hook, response_hook=response_hook, ) await self.client.set("key", "value") spans = self.assert_span_count(1) span = spans[0] self.assertEqual(span.attributes.get(request_attr), "SET") self.assertEqual(span.attributes.get(response_attr), True) # fresh client should not record any spans fresh_client = FakeRedis() self.memory_exporter.clear() await fresh_client.set("key", "value") self.assert_span_count(0) self.instrumentor.uninstrument_client(self.client) # after un-instrumenting the query should not be recorder await self.client.set("key", "value") spans = self.assert_span_count(0) @pytest.mark.asyncio async def test_request_hook_exception(self): def request_hook(_span, _conn, _args, _kwargs): raise ValueError("hook error") self.instrumentor.instrument(tracer_provider=self.tracer_provider, request_hook=request_hook) with self.assertLogs("opentelemetry.instrumentation.redis", level="WARNING") as log_ctx: await self.client.set("key", "value") self.assert_span_count(1) self.assertTrue(any("request_hook" in msg for msg in log_ctx.output)) self.instrumentor.uninstrument() @pytest.mark.asyncio async def test_response_hook_exception(self): def response_hook(_span, _conn, _response): raise ValueError("hook error") self.instrumentor.instrument(tracer_provider=self.tracer_provider, response_hook=response_hook) with self.assertLogs("opentelemetry.instrumentation.redis", level="WARNING") as log_ctx: await self.client.set("key", "value") self.assert_span_count(1) self.assertTrue(any("response_hook" in msg for msg in log_ctx.output)) self.instrumentor.uninstrument() @pytest.mark.asyncio async def test_span_name_empty_pipeline(self): redis_client = fakeredis.aioredis.FakeRedis() self.instrumentor.instrument_client(client=redis_client, tracer_provider=self.tracer_provider) async with redis_client.pipeline() as pipe: await pipe.execute() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].name, "redis") self.assertEqual(spans[0].kind, SpanKind.CLIENT) self.assertEqual(spans[0].status.status_code, trace.StatusCode.UNSET) self.instrumentor.uninstrument_client(client=redis_client) @pytest.mark.asyncio async def test_suppress_instrumentation_async_command(self): self.instrumentor.instrument(tracer_provider=self.tracer_provider) redis_client = FakeRedis() # Execute command with suppression with suppress_instrumentation(): await redis_client.get("key") # No spans should be created self.assert_span_count(0) # Verify that instrumentation works again after exiting the context await redis_client.set("key", "value") self.assert_span_count(1) self.instrumentor.uninstrument() @pytest.mark.asyncio async def test_suppress_instrumentation_async_pipeline(self): self.instrumentor.instrument(tracer_provider=self.tracer_provider) redis_client = FakeRedis() # Execute pipeline with suppression with suppress_instrumentation(): async with redis_client.pipeline() as pipe: await pipe.set("key1", "value1") await pipe.set("key2", "value2") await pipe.get("key1") await pipe.execute() # No spans should be created self.assert_span_count(0) # Verify that instrumentation works again after exiting the context async with redis_client.pipeline() as pipe: await pipe.set("key3", "value3") await pipe.execute() spans = self.assert_span_count(1) # Pipeline span could be "SET" or "redis.pipeline" depending on implementation self.assertIn(spans[0].name, ["SET", "redis.pipeline"]) self.instrumentor.uninstrument() @pytest.mark.asyncio async def test_suppress_instrumentation_async_mixed(self): self.instrumentor.instrument(tracer_provider=self.tracer_provider) redis_client = FakeRedis() # Regular instrumented call await redis_client.set("key1", "value1") self.assert_span_count(1) self.memory_exporter.clear() # Suppressed call with suppress_instrumentation(): await redis_client.set("key2", "value2") self.assert_span_count(0) # Another regular instrumented call await redis_client.get("key1") self.assert_span_count(1) self.instrumentor.uninstrument() class TestRedisInstance(TestBase): def assert_span_count(self, count: int): """ Assert that the memory exporter has the expected number of spans. Returns the spans list if assertion passes """ spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), count) return spans def setUp(self): super().setUp() self.client = fakeredis.FakeStrictRedis() RedisInstrumentor().instrument_client(client=self.client, tracer_provider=self.tracer_provider) def tearDown(self): super().tearDown() RedisInstrumentor().uninstrument_client(self.client) def test_only_client_instrumented(self): redis_client = redis.Redis() with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.assert_span_count(0) # now use the test client with mock.patch.object(self.client, "connection"): self.client.get("key") spans = self.assert_span_count(1) span = spans[0] self.assertEqual(span.name, "GET") self.assertEqual(span.kind, SpanKind.CLIENT) @staticmethod def redis_operations(client): with pytest.raises(WatchError): pipe = client.pipeline(transaction=True) pipe.watch("a") client.set("a", "bad") # This will cause the WatchError pipe.multi() pipe.set("a", "1") pipe.execute() def test_watch_error_sync_only_client(self): redis_client = fakeredis.FakeStrictRedis() self.redis_operations(redis_client) self.assert_span_count(0) self.redis_operations(self.client) # there should be 3 tests, we start watch operation and have 2 set operation on same key spans = self.assert_span_count(3) self.assertEqual(spans[0].attributes.get("db.statement"), "WATCH ?") self.assertEqual(spans[0].kind, SpanKind.CLIENT) self.assertEqual(spans[0].status.status_code, trace.StatusCode.UNSET) for span in spans[1:]: self.assertEqual(span.attributes.get("db.statement"), "SET ? ?") self.assertEqual(span.kind, SpanKind.CLIENT) self.assertEqual(span.status.status_code, trace.StatusCode.UNSET) class TestRedisSemconvConfiguration(TestBase): """Tests semconv migration for both Redis pipeline and db_statement""" def setUp(self): super().setUp() RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) def tearDown(self): super().tearDown() RedisInstrumentor().uninstrument() def re_instrument_and_clear_exporter(self): # Re-instrument to pick up the environment variable change RedisInstrumentor().uninstrument() self.memory_exporter.clear() # Clear previous spans RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) @stability_mode("") def test_pipeline_default_mode(self): self.re_instrument_and_clear_exporter() redis_client = fakeredis.FakeStrictRedis() pipe = redis_client.pipeline() pipe.get("key1") pipe.set("key2", "value2") pipe.execute() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIn(DB_STATEMENT, span.attributes) self.assertIn("GET ?", span.attributes[DB_STATEMENT]) self.assertIn("SET ? ?", span.attributes[DB_STATEMENT]) self.assertNotIn(DB_QUERY_TEXT, span.attributes) self.assertIn(DB_SYSTEM, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM], DbSystemValues.REDIS.value) self.assertNotIn(DB_SYSTEM_NAME, span.attributes) @stability_mode("database") def test_pipeline_database_stable_mode(self): self.re_instrument_and_clear_exporter() redis_client = fakeredis.FakeStrictRedis() pipe = redis_client.pipeline() pipe.get("key1") pipe.set("key2", "value2") pipe.execute() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertNotIn(DB_STATEMENT, span.attributes) self.assertIn(DB_QUERY_TEXT, span.attributes) self.assertIn("GET ?", span.attributes[DB_QUERY_TEXT]) self.assertIn("SET ? ?", span.attributes[DB_QUERY_TEXT]) self.assertNotIn(DB_SYSTEM, span.attributes) self.assertIn(DB_SYSTEM_NAME, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM_NAME], DbSystemValues.REDIS.value) @stability_mode("database/dup") def test_pipeline_database_dup_mode(self): self.re_instrument_and_clear_exporter() redis_client = fakeredis.FakeStrictRedis() pipe = redis_client.pipeline() pipe.get("key1") pipe.set("key2", "value2") pipe.execute() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIn(DB_STATEMENT, span.attributes) self.assertIn("GET ?", span.attributes[DB_STATEMENT]) self.assertIn("SET ? ?", span.attributes[DB_STATEMENT]) self.assertIn(DB_QUERY_TEXT, span.attributes) self.assertIn("GET ?", span.attributes[DB_QUERY_TEXT]) self.assertIn("SET ? ?", span.attributes[DB_QUERY_TEXT]) self.assertIn(DB_SYSTEM, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM], DbSystemValues.REDIS.value) self.assertIn(DB_SYSTEM_NAME, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM_NAME], DbSystemValues.REDIS.value) @stability_mode("") def test_db_statement_default_mode(self): self.re_instrument_and_clear_exporter() redis_client = redis.Redis() with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIn(DB_STATEMENT, span.attributes) self.assertEqual(span.attributes[DB_STATEMENT], "GET ?") self.assertNotIn(DB_QUERY_TEXT, span.attributes) self.assertIn(DB_SYSTEM, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM], DbSystemValues.REDIS.value) self.assertNotIn(DB_SYSTEM_NAME, span.attributes) @stability_mode("database") def test_db_statement_database_stable_mode(self): self.re_instrument_and_clear_exporter() redis_client = redis.Redis() with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertNotIn(DB_STATEMENT, span.attributes) self.assertIn(DB_QUERY_TEXT, span.attributes) self.assertEqual(span.attributes[DB_QUERY_TEXT], "GET ?") self.assertNotIn(DB_SYSTEM, span.attributes) self.assertIn(DB_SYSTEM_NAME, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM_NAME], DbSystemValues.REDIS.value) @stability_mode("database/dup") def test_db_statement_database_dup_mode(self): self.re_instrument_and_clear_exporter() redis_client = redis.Redis() with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIn(DB_STATEMENT, span.attributes) self.assertEqual(span.attributes[DB_STATEMENT], "GET ?") self.assertIn(DB_QUERY_TEXT, span.attributes) self.assertEqual(span.attributes[DB_QUERY_TEXT], "GET ?") self.assertIn(DB_SYSTEM, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM], DbSystemValues.REDIS.value) self.assertIn(DB_SYSTEM_NAME, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM_NAME], DbSystemValues.REDIS.value) @stability_mode("") def test_db_namespace_default_mode(self): self.re_instrument_and_clear_exporter() redis_client = redis.Redis() with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIn(DB_REDIS_DATABASE_INDEX, span.attributes) self.assertEqual(span.attributes[DB_REDIS_DATABASE_INDEX], 0) @stability_mode("database") def test_db_namespace_database_stable_mode(self): self.re_instrument_and_clear_exporter() redis_client = redis.Redis() with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertNotIn(DB_REDIS_DATABASE_INDEX, span.attributes) @stability_mode("database/dup") def test_db_namespace_database_dup_mode(self): self.re_instrument_and_clear_exporter() redis_client = redis.Redis() with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIn(DB_REDIS_DATABASE_INDEX, span.attributes) self.assertEqual(span.attributes[DB_REDIS_DATABASE_INDEX], 0) @stability_mode("http") def test_db_statement_http_stable_mode(self): # HTTP signal type should not affect database attributes; they stay in default behavior self.re_instrument_and_clear_exporter() redis_client = redis.Redis() with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] # HTTP signal type doesn't affect database attributes - they remain in default mode self.assertIn(DB_STATEMENT, span.attributes) self.assertEqual(span.attributes[DB_STATEMENT], "GET ?") self.assertNotIn(DB_QUERY_TEXT, span.attributes) self.assertIn(DB_SYSTEM, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM], DbSystemValues.REDIS.value) self.assertNotIn(DB_SYSTEM_NAME, span.attributes) # Network attributes should still be present (HTTP signal type for network attributes) self.assertIn(SERVER_ADDRESS, span.attributes) self.assertIn(SERVER_PORT, span.attributes) self.assertNotIn(NET_TRANSPORT, span.attributes) self.assertIn(NETWORK_TRANSPORT, span.attributes) self.assertEqual( span.attributes[NETWORK_TRANSPORT], NetworkTransportValues.TCP.value, ) @stability_mode("http") def test_net_transport_http_stable_mode_unix_socket(self): # HTTP signal type should suppress old net.transport for unix socket connections too self.re_instrument_and_clear_exporter() redis_client = redis.Redis.from_url("unix://foo@/path/to/socket.sock?db=3&password=bar") with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIn(SERVER_ADDRESS, span.attributes) self.assertNotIn(NET_TRANSPORT, span.attributes) self.assertIn(NETWORK_TRANSPORT, span.attributes) self.assertEqual( span.attributes[NETWORK_TRANSPORT], NetworkTransportValues.UNIX.value, ) @stability_mode("http/dup") def test_db_statement_http_dup_mode(self): # HTTP signal type should not affect database attributes; they stay in default behavior self.re_instrument_and_clear_exporter() redis_client = redis.Redis() with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] # HTTP signal type doesn't affect database attributes - they remain in default mode self.assertIn(DB_STATEMENT, span.attributes) self.assertEqual(span.attributes[DB_STATEMENT], "GET ?") self.assertNotIn(DB_QUERY_TEXT, span.attributes) self.assertIn(DB_SYSTEM, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM], DbSystemValues.REDIS.value) self.assertNotIn(DB_SYSTEM_NAME, span.attributes) # Network attributes should still be present (HTTP signal type for network attributes) self.assertIn(SERVER_ADDRESS, span.attributes) self.assertIn(SERVER_PORT, span.attributes) self.assertIn(NET_TRANSPORT, span.attributes) self.assertEqual( span.attributes[NET_TRANSPORT], NetTransportValues.IP_TCP.value, ) self.assertIn(NETWORK_TRANSPORT, span.attributes) self.assertEqual( span.attributes[NETWORK_TRANSPORT], NetworkTransportValues.TCP.value, ) @stability_mode("http,database") def test_db_statement_combined_http_database_mode(self): # Both HTTP and DATABASE signal types should be active self.re_instrument_and_clear_exporter() redis_client = redis.Redis() with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] # DATABASE signal type should use stable attributes self.assertNotIn(DB_STATEMENT, span.attributes) self.assertIn(DB_QUERY_TEXT, span.attributes) self.assertEqual(span.attributes[DB_QUERY_TEXT], "GET ?") self.assertNotIn(DB_SYSTEM, span.attributes) self.assertIn(DB_SYSTEM_NAME, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM_NAME], DbSystemValues.REDIS.value) # Network attributes should still be present (HTTP signal type) self.assertIn(SERVER_ADDRESS, span.attributes) self.assertIn(SERVER_PORT, span.attributes) @stability_mode("database,http") def test_db_statement_combined_database_http_mode(self): # Both DATABASE and HTTP signal types should be active (order shouldn't matter) self.re_instrument_and_clear_exporter() redis_client = redis.Redis() with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] # DATABASE signal type should use stable attributes self.assertNotIn(DB_STATEMENT, span.attributes) self.assertIn(DB_QUERY_TEXT, span.attributes) self.assertEqual(span.attributes[DB_QUERY_TEXT], "GET ?") self.assertNotIn(DB_SYSTEM, span.attributes) self.assertIn(DB_SYSTEM_NAME, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM_NAME], DbSystemValues.REDIS.value) # Network attributes should still be present (HTTP signal type) self.assertIn(SERVER_ADDRESS, span.attributes) self.assertIn(SERVER_PORT, span.attributes) @stability_mode("database/dup,http") def test_db_statement_combined_database_dup_http_mode(self): # Both DATABASE (dup) and HTTP signal types should be active self.re_instrument_and_clear_exporter() redis_client = redis.Redis() with mock.patch.object(redis_client, "connection"): redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] # DATABASE signal type in dup mode should have both attributes self.assertIn(DB_STATEMENT, span.attributes) self.assertEqual(span.attributes[DB_STATEMENT], "GET ?") self.assertIn(DB_QUERY_TEXT, span.attributes) self.assertEqual(span.attributes[DB_QUERY_TEXT], "GET ?") self.assertIn(DB_SYSTEM, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM], DbSystemValues.REDIS.value) self.assertIn(DB_SYSTEM_NAME, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM_NAME], DbSystemValues.REDIS.value) # Network attributes should still be present (HTTP signal type) self.assertIn(SERVER_ADDRESS, span.attributes) self.assertIn(SERVER_PORT, span.attributes) @stability_mode("http") def test_pipeline_http_stable_mode(self): # HTTP signal type should not affect database attributes in pipeline self.re_instrument_and_clear_exporter() redis_client = fakeredis.FakeStrictRedis() pipe = redis_client.pipeline() pipe.get("key1") pipe.set("key2", "value2") pipe.execute() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] # HTTP signal type doesn't affect database attributes - they remain in default mode self.assertIn(DB_STATEMENT, span.attributes) self.assertIn("GET ?", span.attributes[DB_STATEMENT]) self.assertIn("SET ? ?", span.attributes[DB_STATEMENT]) self.assertNotIn(DB_QUERY_TEXT, span.attributes) self.assertIn(DB_SYSTEM, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM], DbSystemValues.REDIS.value) self.assertNotIn(DB_SYSTEM_NAME, span.attributes) @stability_mode("http,database") def test_pipeline_combined_http_database_mode(self): # Both HTTP and DATABASE signal types should be active in pipeline self.re_instrument_and_clear_exporter() redis_client = fakeredis.FakeStrictRedis() pipe = redis_client.pipeline() pipe.get("key1") pipe.set("key2", "value2") pipe.execute() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] # DATABASE signal type should use stable attributes self.assertNotIn(DB_STATEMENT, span.attributes) self.assertIn(DB_QUERY_TEXT, span.attributes) self.assertIn("GET ?", span.attributes[DB_QUERY_TEXT]) self.assertIn("SET ? ?", span.attributes[DB_QUERY_TEXT]) self.assertNotIn(DB_SYSTEM, span.attributes) self.assertIn(DB_SYSTEM_NAME, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM_NAME], DbSystemValues.REDIS.value) @stability_mode("database") def test_async_db_statement_database_stable_mode(self): self.re_instrument_and_clear_exporter() redis_client = fakeredis.aioredis.FakeRedis() asyncio.run(redis_client.get("key")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertNotIn(DB_STATEMENT, span.attributes) self.assertIn(DB_QUERY_TEXT, span.attributes) self.assertEqual(span.attributes[DB_QUERY_TEXT], "GET ?") self.assertNotIn(DB_SYSTEM, span.attributes) self.assertIn(DB_SYSTEM_NAME, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM_NAME], DbSystemValues.REDIS.value) self.assertNotIn(DB_REDIS_DATABASE_INDEX, span.attributes) self.assertIn(NET_TRANSPORT, span.attributes) self.assertEqual( span.attributes[NET_TRANSPORT], NetTransportValues.IP_TCP.value, ) self.assertNotIn(NETWORK_TRANSPORT, span.attributes) self.assertNotIn(SERVER_ADDRESS, span.attributes) self.assertNotIn(SERVER_PORT, span.attributes) @stability_mode("") def test_async_db_statement_default_mode(self): self.re_instrument_and_clear_exporter() redis_client = fakeredis.aioredis.FakeRedis() asyncio.run(redis_client.get("key")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIn(DB_STATEMENT, span.attributes) self.assertEqual(span.attributes[DB_STATEMENT], "GET ?") self.assertNotIn(DB_QUERY_TEXT, span.attributes) self.assertIn(DB_SYSTEM, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM], DbSystemValues.REDIS.value) self.assertNotIn(DB_SYSTEM_NAME, span.attributes) self.assertIn(DB_REDIS_DATABASE_INDEX, span.attributes) self.assertEqual(span.attributes[DB_REDIS_DATABASE_INDEX], 0) self.assertIn(NET_TRANSPORT, span.attributes) self.assertEqual( span.attributes[NET_TRANSPORT], NetTransportValues.IP_TCP.value, ) self.assertNotIn(NETWORK_TRANSPORT, span.attributes) self.assertNotIn(SERVER_ADDRESS, span.attributes) self.assertNotIn(SERVER_PORT, span.attributes) @stability_mode("database/dup") def test_async_db_statement_database_dup_mode(self): self.re_instrument_and_clear_exporter() redis_client = fakeredis.aioredis.FakeRedis() asyncio.run(redis_client.get("key")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIn(DB_STATEMENT, span.attributes) self.assertEqual(span.attributes[DB_STATEMENT], "GET ?") self.assertIn(DB_QUERY_TEXT, span.attributes) self.assertEqual(span.attributes[DB_QUERY_TEXT], "GET ?") self.assertIn(DB_SYSTEM, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM], DbSystemValues.REDIS.value) self.assertIn(DB_SYSTEM_NAME, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM_NAME], DbSystemValues.REDIS.value) self.assertIn(DB_REDIS_DATABASE_INDEX, span.attributes) self.assertEqual(span.attributes[DB_REDIS_DATABASE_INDEX], 0) self.assertIn(NET_TRANSPORT, span.attributes) self.assertEqual( span.attributes[NET_TRANSPORT], NetTransportValues.IP_TCP.value, ) self.assertNotIn(NETWORK_TRANSPORT, span.attributes) self.assertNotIn(SERVER_ADDRESS, span.attributes) self.assertNotIn(SERVER_PORT, span.attributes) @stability_mode("database") def test_async_pipeline_database_stable_mode(self): self.re_instrument_and_clear_exporter() redis_client = fakeredis.aioredis.FakeRedis() async def _run_pipeline(): async with redis_client.pipeline() as pipe: await pipe.get("key1") await pipe.set("key2", "value2") await pipe.execute() asyncio.run(_run_pipeline()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertNotIn(DB_STATEMENT, span.attributes) self.assertIn(DB_QUERY_TEXT, span.attributes) self.assertIn("GET ?", span.attributes[DB_QUERY_TEXT]) self.assertIn("SET ? ?", span.attributes[DB_QUERY_TEXT]) self.assertNotIn(DB_SYSTEM, span.attributes) self.assertIn(DB_SYSTEM_NAME, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM_NAME], DbSystemValues.REDIS.value) self.assertNotIn(DB_REDIS_DATABASE_INDEX, span.attributes) self.assertIn(NET_TRANSPORT, span.attributes) self.assertEqual( span.attributes[NET_TRANSPORT], NetTransportValues.IP_TCP.value, ) self.assertNotIn(NETWORK_TRANSPORT, span.attributes) self.assertNotIn(SERVER_ADDRESS, span.attributes) self.assertNotIn(SERVER_PORT, span.attributes) @stability_mode("") def test_async_pipeline_default_mode(self): self.re_instrument_and_clear_exporter() redis_client = fakeredis.aioredis.FakeRedis() async def _run_pipeline(): async with redis_client.pipeline() as pipe: await pipe.get("key1") await pipe.set("key2", "value2") await pipe.execute() asyncio.run(_run_pipeline()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIn(DB_STATEMENT, span.attributes) self.assertIn("GET ?", span.attributes[DB_STATEMENT]) self.assertIn("SET ? ?", span.attributes[DB_STATEMENT]) self.assertNotIn(DB_QUERY_TEXT, span.attributes) self.assertIn(DB_SYSTEM, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM], DbSystemValues.REDIS.value) self.assertNotIn(DB_SYSTEM_NAME, span.attributes) self.assertIn(DB_REDIS_DATABASE_INDEX, span.attributes) self.assertEqual(span.attributes[DB_REDIS_DATABASE_INDEX], 0) self.assertIn(NET_TRANSPORT, span.attributes) self.assertEqual( span.attributes[NET_TRANSPORT], NetTransportValues.IP_TCP.value, ) self.assertNotIn(NETWORK_TRANSPORT, span.attributes) self.assertNotIn(SERVER_ADDRESS, span.attributes) self.assertNotIn(SERVER_PORT, span.attributes) @stability_mode("database/dup") def test_async_pipeline_database_dup_mode(self): self.re_instrument_and_clear_exporter() redis_client = fakeredis.aioredis.FakeRedis() async def _run_pipeline(): async with redis_client.pipeline() as pipe: await pipe.get("key1") await pipe.set("key2", "value2") await pipe.execute() asyncio.run(_run_pipeline()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertIn(DB_STATEMENT, span.attributes) self.assertIn("GET ?", span.attributes[DB_STATEMENT]) self.assertIn("SET ? ?", span.attributes[DB_STATEMENT]) self.assertIn(DB_QUERY_TEXT, span.attributes) self.assertIn("GET ?", span.attributes[DB_QUERY_TEXT]) self.assertIn("SET ? ?", span.attributes[DB_QUERY_TEXT]) self.assertIn(DB_SYSTEM, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM], DbSystemValues.REDIS.value) self.assertIn(DB_SYSTEM_NAME, span.attributes) self.assertEqual(span.attributes[DB_SYSTEM_NAME], DbSystemValues.REDIS.value) self.assertIn(DB_REDIS_DATABASE_INDEX, span.attributes) self.assertEqual(span.attributes[DB_REDIS_DATABASE_INDEX], 0) self.assertIn(NET_TRANSPORT, span.attributes) self.assertEqual( span.attributes[NET_TRANSPORT], NetTransportValues.IP_TCP.value, ) self.assertNotIn(NETWORK_TRANSPORT, span.attributes) self.assertNotIn(SERVER_ADDRESS, span.attributes) self.assertNotIn(SERVER_PORT, span.attributes) @stability_mode("") def test_schema_url_default_mode(self): """Test schema URL assignment in default stability mode.""" self.re_instrument_and_clear_exporter() with mock.patch("opentelemetry.instrumentation.redis.get_tracer") as mock_get_tracer: mock_tracer = mock.Mock() mock_get_tracer.return_value = mock_tracer RedisInstrumentor._get_tracer(tracer_provider=self.tracer_provider) # Verify get_tracer was called with legacy schema URL in default mode mock_get_tracer.assert_called_once() call_args = mock_get_tracer.call_args self.assertEqual( call_args[1]["schema_url"], "https://opentelemetry.io/schemas/1.11.0", ) @stability_mode("database") def test_schema_url_database_stable_mode(self): """Test schema URL assignment in database stable mode.""" self.re_instrument_and_clear_exporter() with mock.patch("opentelemetry.instrumentation.redis.get_tracer") as mock_get_tracer: mock_tracer = mock.Mock() mock_get_tracer.return_value = mock_tracer RedisInstrumentor._get_tracer(tracer_provider=self.tracer_provider) # Verify get_tracer was called with stable schema URL mock_get_tracer.assert_called_once() call_args = mock_get_tracer.call_args self.assertEqual( call_args[1]["schema_url"], "https://opentelemetry.io/schemas/1.25.0", ) @stability_mode("database/dup") def test_schema_url_database_dup_mode(self): """Test schema URL assignment in database duplicate mode.""" self.re_instrument_and_clear_exporter() with mock.patch("opentelemetry.instrumentation.redis.get_tracer") as mock_get_tracer: mock_tracer = mock.Mock() mock_get_tracer.return_value = mock_tracer RedisInstrumentor._get_tracer(tracer_provider=self.tracer_provider) # Verify get_tracer was called with stable schema URL mock_get_tracer.assert_called_once() call_args = mock_get_tracer.call_args self.assertEqual( call_args[1]["schema_url"], "https://opentelemetry.io/schemas/1.25.0", ) @stability_mode("http") def test_schema_url_http_mode(self): """Test schema URL assignment in HTTP stability mode.""" self.re_instrument_and_clear_exporter() with mock.patch("opentelemetry.instrumentation.redis.get_tracer") as mock_get_tracer: mock_tracer = mock.Mock() mock_get_tracer.return_value = mock_tracer RedisInstrumentor._get_tracer(tracer_provider=self.tracer_provider) # Verify get_tracer was called with stable schema URL mock_get_tracer.assert_called_once() call_args = mock_get_tracer.call_args self.assertEqual( call_args[1]["schema_url"], "https://opentelemetry.io/schemas/1.21.0", ) @stability_mode("http,database") def test_schema_url_combined_mode(self): """Test schema URL assignment in combined HTTP and database mode.""" self.re_instrument_and_clear_exporter() with mock.patch("opentelemetry.instrumentation.redis.get_tracer") as mock_get_tracer: mock_tracer = mock.Mock() mock_get_tracer.return_value = mock_tracer RedisInstrumentor._get_tracer(tracer_provider=self.tracer_provider) # Verify get_tracer was called with stable schema URL mock_get_tracer.assert_called_once() call_args = mock_get_tracer.call_args self.assertEqual( call_args[1]["schema_url"], "https://opentelemetry.io/schemas/1.25.0", ) class _FakeCommand: def __init__(self, *args): self.args = args class _FakeExecutionStrategy: def __init__(self, commands): self.command_queue = commands class _FakeClusterPipeline: """Mimics redis-py 6+ ClusterPipeline: queued commands live on ``_execution_strategy.command_queue`` while ``command_stack`` stays empty.""" def __init__(self, commands): self.command_stack = [] self._execution_strategy = _FakeExecutionStrategy(commands) class _FakeAsyncExecutionStrategy: """Mimics redis-py 6+ async cluster strategy: unlike the sync strategy (public ``command_queue`` property), it only exposes the private ``_command_queue`` attribute.""" def __init__(self, commands): self._command_queue = commands class _FakeAsyncClusterPipeline: """Mimics redis-py 6+ async ClusterPipeline: queued commands live on ``_execution_strategy._command_queue`` and there is no ``command_stack``.""" def __init__(self, commands): self._execution_strategy = _FakeAsyncExecutionStrategy(commands) class _FakeLegacyPipeline: def __init__(self, commands): self.command_stack = commands class TestBuildSpanMetaDataForPipeline(TestBase): def test_cluster_pipeline_reads_execution_strategy(self): # Regression test for issue #4084: redis-py 6+ ClusterPipeline no # longer populates command_stack, so commands must be read from # _execution_strategy.command_queue. commands = [_FakeCommand("SET", "k1", "v1"), _FakeCommand("GET", "k1")] instance = _FakeClusterPipeline(commands) command_stack, resource, span_name = _build_span_meta_data_for_pipeline(instance) self.assertEqual(len(command_stack), 2) self.assertEqual(resource, "SET ? ?\nGET ?") self.assertEqual(span_name, "SET GET") def test_async_cluster_pipeline_reads_private_command_queue(self): # Regression test for issue #4084 on the async path: the redis-py 6+ # async cluster strategy exposes only the private ``_command_queue`` # (no public ``command_queue`` property), so reading only # ``command_queue`` left the async ClusterPipeline span empty. commands = [_FakeCommand("SET", "k1", "v1"), _FakeCommand("GET", "k1")] instance = _FakeAsyncClusterPipeline(commands) command_stack, resource, span_name = _build_span_meta_data_for_pipeline(instance) self.assertEqual(len(command_stack), 2) self.assertEqual(resource, "SET ? ?\nGET ?") self.assertEqual(span_name, "SET GET") def test_legacy_pipeline_still_reads_command_stack(self): commands = [_FakeCommand("SET", "k1", "v1")] instance = _FakeLegacyPipeline(commands) command_stack, resource, span_name = _build_span_meta_data_for_pipeline(instance) self.assertEqual(len(command_stack), 1) self.assertEqual(resource, "SET ? ?") self.assertEqual(span_name, "SET") def test_empty_cluster_pipeline_falls_back_to_redis_span_name(self): instance = _FakeClusterPipeline([]) command_stack, resource, span_name = _build_span_meta_data_for_pipeline(instance) self.assertEqual(command_stack, []) self.assertEqual(resource, "") self.assertEqual(span_name, "redis") opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-remoulade/000077500000000000000000000000001527070072300331205ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-remoulade/LICENSE000066400000000000000000000261351527070072300341340ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000013641527070072300345340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-remouladeOpenTelemetry Remoulade Instrumentation ======================================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-remoulade.svg :target: https://pypi.org/project/opentelemetry-instrumentation-remoulade/ This library allows tracing requests made by the Remoulade library. Installation ------------ :: pip install opentelemetry-instrumentation-remoulade References ---------- * `OpenTelemetry Remoulade Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031521527070072300357560ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-remoulade[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-remoulade" dynamic = ["version"] description = "OpenTelemetry Remoulade instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", ] [project.optional-dependencies] instruments = [ "remoulade >= 0.50", ] [project.entry-points.opentelemetry_instrumentor] remoulade = "opentelemetry.instrumentation.remoulade:RemouladeInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-remoulade" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/remoulade/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-remoulade/src/000077500000000000000000000000001527070072300337075ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300365245ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-remoulade/srcinstrumentation/000077500000000000000000000000001527070072300417675ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetryremoulade/000077500000000000000000000000001527070072300437445ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry/instrumentation__init__.py000066400000000000000000000123531527070072300460610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry/instrumentation/remoulade# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Usage ----- Start broker backend :: docker run -p 5672:5672 rabbitmq Run instrumented actor .. code-block:: python from remoulade.brokers.rabbitmq import RabbitmqBroker import remoulade from opentelemetry.instrumentation.remoulade import RemouladeInstrumentor RemouladeInstrumentor().instrument() broker = RabbitmqBroker() remoulade.set_broker(broker) @remoulade.actor def multiply(x, y): return x * y broker.declare_actor(multiply) multiply.send(43, 51) """ from collections.abc import Collection from remoulade import Middleware, broker from opentelemetry import trace from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.remoulade import utils from opentelemetry.instrumentation.remoulade.package import _instruments from opentelemetry.instrumentation.remoulade.version import __version__ from opentelemetry.propagate import extract, inject from opentelemetry.semconv._incubating.attributes import messaging_attributes _REMOULADE_MESSAGE_TAG_KEY = "remoulade.action" _REMOULADE_MESSAGE_SEND = "send" _REMOULADE_MESSAGE_RUN = "run" _REMOULADE_MESSAGE_NAME_KEY = "remoulade.actor_name" _REMOULADE_MESSAGE_RETRY_COUNT_KEY = "remoulade.retry_count" class _InstrumentationMiddleware(Middleware): def __init__(self, _tracer): self._tracer = _tracer self._span_registry = {} def before_process_message(self, _broker, message): if "trace_ctx" not in message.options: return trace_ctx = extract(message.options["trace_ctx"]) retry_count = message.options.get("retries", 0) operation_name = utils.get_operation_name("before_process_message", retry_count) span_attributes = {_REMOULADE_MESSAGE_RETRY_COUNT_KEY: retry_count} span = self._tracer.start_span( operation_name, kind=trace.SpanKind.CONSUMER, context=trace_ctx, attributes=span_attributes, ) activation = trace.use_span(span, end_on_exit=True) activation.__enter__() # pylint: disable=unnecessary-dunder-call utils.attach_span(self._span_registry, message.message_id, (span, activation)) def after_process_message(self, _broker, message, *, result=None, exception=None): span, activation = utils.retrieve_span(self._span_registry, message.message_id) if span is None: # no existing span found for message_id return if span.is_recording(): span.set_attributes( { _REMOULADE_MESSAGE_TAG_KEY: _REMOULADE_MESSAGE_RUN, _REMOULADE_MESSAGE_NAME_KEY: message.actor_name, messaging_attributes.MESSAGING_MESSAGE_ID: message.message_id, } ) activation.__exit__(None, None, None) utils.detach_span(self._span_registry, message.message_id) def before_enqueue(self, _broker, message, delay): retry_count = message.options.get("retries", 0) operation_name = utils.get_operation_name("before_enqueue", retry_count) span_attributes = {_REMOULADE_MESSAGE_RETRY_COUNT_KEY: retry_count} span = self._tracer.start_span( operation_name, kind=trace.SpanKind.PRODUCER, attributes=span_attributes, ) if span.is_recording(): span.set_attributes( { _REMOULADE_MESSAGE_TAG_KEY: _REMOULADE_MESSAGE_SEND, _REMOULADE_MESSAGE_NAME_KEY: message.actor_name, messaging_attributes.MESSAGING_MESSAGE_ID: message.message_id, } ) activation = trace.use_span(span, end_on_exit=True) activation.__enter__() # pylint: disable=unnecessary-dunder-call utils.attach_span( self._span_registry, message.message_id, (span, activation), is_publish=True, ) if "trace_ctx" not in message.options: message.options["trace_ctx"] = {} inject(message.options["trace_ctx"]) def after_enqueue(self, _broker, message, delay, exception=None): _, activation = utils.retrieve_span(self._span_registry, message.message_id, is_publish=True) if activation is None: # no existing span found for message_id return activation.__exit__(None, None, None) utils.detach_span(self._span_registry, message.message_id, is_publish=True) class RemouladeInstrumentor(BaseInstrumentor): def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): tracer_provider = kwargs.get("tracer_provider") self._tracer = trace.get_tracer( __name__, __version__, tracer_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) instrumentation_middleware = _InstrumentationMiddleware(self._tracer) broker.add_extra_default_middleware(instrumentation_middleware) def _uninstrument(self, **kwargs): broker.remove_extra_default_middleware(_InstrumentationMiddleware) package.py000066400000000000000000000001631527070072300457110ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry/instrumentation/remoulade# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("remoulade >= 0.50",) utils.py000066400000000000000000000014641527070072300454630ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry/instrumentation/remoulade# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 def attach_span(span_registry, message_id, span_and_activation, is_publish=False): span_registry[(message_id, is_publish)] = span_and_activation def detach_span(span_registry, message_id, is_publish=False): span_registry.pop((message_id, is_publish)) def retrieve_span(span_registry, message_id, is_publish=False): return span_registry.get((message_id, is_publish), (None, None)) def get_operation_name(hook_name, retry_count): if hook_name == "before_process_message": return "remoulade/process" if retry_count == 0 else f"remoulade/process(retry-{retry_count})" if hook_name == "before_enqueue": return "remoulade/send" if retry_count == 0 else f"remoulade/send(retry-{retry_count})" return "" version.py000066400000000000000000000001441527070072300460020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry/instrumentation/remoulade# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000005521527070072300373040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-remouladeasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 prometheus_client==0.20.0 py-cpuinfo==9.0.0 pytest==7.4.4 python-dateutil==2.8.2 pytz==2024.1 remoulade==3.2.0 six==1.16.0 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-remoulade opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-remoulade/tests/000077500000000000000000000000001527070072300342625ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300363020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-remoulade/teststest_messages.py000066400000000000000000000064231527070072300374300ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-remoulade/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import remoulade from remoulade.brokers.local import LocalBroker from opentelemetry.instrumentation.remoulade import RemouladeInstrumentor from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind @remoulade.actor(max_retries=3) def actor_div(dividend, divisor): return dividend / divisor class TestRemouladeInstrumentation(TestBase): def setUp(self): super().setUp() RemouladeInstrumentor().instrument() broker = LocalBroker() remoulade.set_broker(broker) broker.declare_actor(actor_div) def tearDown(self): RemouladeInstrumentor().uninstrument() super().tearDown() def test_message(self): actor_div.send(2, 3) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 2) consumer, producer = spans self.assertEqual(consumer.name, "remoulade/process") self.assertEqual(consumer.kind, SpanKind.CONSUMER) self.assertSpanHasAttributes( consumer, { "remoulade.action": "run", "remoulade.actor_name": "actor_div", }, ) self.assertEqual(producer.name, "remoulade/send") self.assertEqual(producer.kind, SpanKind.PRODUCER) self.assertSpanHasAttributes( producer, { "remoulade.action": "send", "remoulade.actor_name": "actor_div", }, ) self.assertNotEqual(consumer.parent, producer.context) self.assertEqual(consumer.parent.span_id, producer.context.span_id) self.assertEqual(consumer.context.trace_id, producer.context.trace_id) def test_retries(self): try: actor_div.send(1, 0) except ZeroDivisionError: pass spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 8) consumer_spans = spans[::2] producer_spans = spans[1::2] self.assertEqual(consumer_spans[0].name, "remoulade/process(retry-3)") self.assertSpanHasAttributes(consumer_spans[0], {"remoulade.retry_count": 3}) self.assertEqual(consumer_spans[1].name, "remoulade/process(retry-2)") self.assertSpanHasAttributes(consumer_spans[1], {"remoulade.retry_count": 2}) self.assertEqual(consumer_spans[3].name, "remoulade/process") self.assertEqual(producer_spans[0].name, "remoulade/send(retry-3)") self.assertSpanHasAttributes(producer_spans[0], {"remoulade.retry_count": 3}) self.assertEqual(producer_spans[1].name, "remoulade/send(retry-2)") self.assertSpanHasAttributes(producer_spans[1], {"remoulade.retry_count": 2}) self.assertEqual(producer_spans[3].name, "remoulade/send") def test_uninstrument(self): RemouladeInstrumentor().instrument() actor_div.send(2, 3) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) self.memory_exporter.clear() RemouladeInstrumentor().uninstrument() actor_div.send(2, 3) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) test_uninstrument.py000066400000000000000000000026051527070072300403720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-remoulade/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import remoulade from remoulade.brokers.local import LocalBroker from opentelemetry.instrumentation.remoulade import RemouladeInstrumentor from opentelemetry.test.test_base import TestBase @remoulade.actor(max_retries=3) def actor_div(dividend, divisor): return dividend / divisor class TestRemouladeUninstrumentation(TestBase): def setUp(self): super().setUp() RemouladeInstrumentor().instrument() broker = LocalBroker() remoulade.set_broker(broker) broker.declare_actor(actor_div) RemouladeInstrumentor().uninstrument() def test_uninstrument_existing_broker(self): actor_div.send(1, 1) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 0) def test_uninstrument_new_brokers(self): new_broker = LocalBroker() remoulade.set_broker(new_broker) new_broker.declare_actor(actor_div) actor_div.send(1, 1) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 0) def test_reinstrument_existing_broker(self): RemouladeInstrumentor().instrument() actor_div.send(1, 1) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 2) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requests/000077500000000000000000000000001527070072300330165ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requests/LICENSE000066400000000000000000000261351527070072300340320ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000023771527070072300344370ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requestsOpenTelemetry Requests Instrumentation ====================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-requests.svg :target: https://pypi.org/project/opentelemetry-instrumentation-requests/ This library allows tracing HTTP requests made by the `requests `_ library. Installation ------------ :: pip install opentelemetry-instrumentation-requests Configuration ------------- Exclude lists ************* To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_REQUESTS_EXCLUDED_URLS`` (or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude. For example, :: export OTEL_PYTHON_REQUESTS_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. References ---------- * `OpenTelemetry requests Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ benchmark-requirements.txt000066400000000000000000000002231527070072300401500ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requestspytest-benchmark==4.0.0 -e opentelemetry-instrumentation -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-requests benchmarks/000077500000000000000000000000001527070072300350545ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requeststest_benchmark_sanitize_method.py000066400000000000000000000024151527070072300436670ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requests/benchmarks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import pytest import requests from requests.adapters import BaseAdapter from requests.models import Response from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) _URL = "http://mock/status/200" class _FakeAdapter(BaseAdapter): def __init__(self): super().__init__() resp = Response() resp.status_code = 200 self._response = resp def send(self, *args, **kwargs): return self._response def close(self): pass @pytest.fixture(name="http_session") def _http_session_fixture(): exporter = InMemorySpanExporter() provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(exporter)) RequestsInstrumentor().instrument(tracer_provider=provider) session = requests.Session() session.mount("http://", _FakeAdapter()) yield session RequestsInstrumentor().uninstrument() def test_instrumented_send(benchmark, http_session: requests.Session): benchmark(http_session.get, _URL) pyproject.toml000066400000000000000000000032101527070072300356470ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requests[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-requests" dynamic = ["version"] description = "OpenTelemetry requests instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-util-http == 0.66b2", ] [project.optional-dependencies] instruments = [ "requests ~= 2.0", ] [project.entry-points.opentelemetry_instrumentor] requests = "opentelemetry.instrumentation.requests:RequestsInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-requests" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/requests/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requests/src/000077500000000000000000000000001527070072300336055ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300364225ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requests/srcinstrumentation/000077500000000000000000000000001527070072300416655ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetryrequests/000077500000000000000000000000001527070072300435405ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation__init__.py000066400000000000000000000550071527070072300456600ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This library allows tracing HTTP requests made by the `requests `_ library. Usage ----- .. code-block:: python import requests from opentelemetry.instrumentation.requests import RequestsInstrumentor # You can optionally pass a custom TracerProvider to instrument(). RequestsInstrumentor().instrument() response = requests.get(url="https://www.example.org/") Configuration ------------- Request/Response hooks ********************** The requests instrumentation supports extending tracing behavior with the help of request and response hooks. These are functions that are called back by the instrumentation right after a Span is created for a request and right before the span is finished processing a response respectively. The hooks can be configured as follows: .. code:: python import requests from opentelemetry.instrumentation.requests import RequestsInstrumentor # `request_obj` is an instance of requests.PreparedRequest def request_hook(span, request_obj): pass # `request_obj` is an instance of requests.PreparedRequest # `response` is an instance of requests.Response def response_hook(span, request_obj, response): pass RequestsInstrumentor().instrument( request_hook=request_hook, response_hook=response_hook ) Capture HTTP request and response headers ***************************************** You can configure the agent to capture specified HTTP headers as span attributes, according to the `semantic conventions `_. Request headers *************** To capture HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST`` to a comma delimited list of HTTP header names. For example using the environment variable, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST="content-type,custom_request_header" will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. Request header names in Requests are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST="Accept.*,X-.*" Would match all request headers that start with ``Accept`` and ``X-``. To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST=".*" The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a single item list containing all the header values. For example: ``http.request.header.custom_request_header = ["", ""]`` Response headers **************** To capture HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE`` to a comma delimited list of HTTP header names. For example using the environment variable, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE="content-type,custom_response_header" will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. Response header names in Requests are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE="Content.*,X-.*" Would match all response headers that start with ``Content`` and ``X-``. To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE=".*" The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a list containing the header values. For example: ``http.response.header.custom_response_header = ["", ""]`` Sanitizing headers ****************** In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be matched in a case-insensitive manner. For example using the environment variable, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. Note: The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. Custom Duration Histogram Boundaries ************************************ To customize the duration histogram bucket boundaries used for HTTP client request duration metrics, you can provide a list of values when instrumenting: .. code:: python import requests from opentelemetry.instrumentation.requests import RequestsInstrumentor custom_boundaries = [0.0, 5.0, 10.0, 25.0, 50.0, 100.0] RequestsInstrumentor().instrument( duration_histogram_boundaries=custom_boundaries ) Exclude lists ************* To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_REQUESTS_EXCLUDED_URLS`` (or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude. For example, :: export OTEL_PYTHON_REQUESTS_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. API --- """ from __future__ import annotations import functools import types from collections.abc import Callable, Collection from timeit import default_timer from typing import Any from urllib.parse import urlparse from requests.models import PreparedRequest, Response from requests.sessions import Session from requests.structures import CaseInsensitiveDict from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, _client_duration_attrs_new, _client_duration_attrs_old, _filter_semconv_duration_attrs, _get_schema_url, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _report_new, _report_old, _set_http_host_client, _set_http_method, _set_http_net_peer_name_client, _set_http_network_protocol_version, _set_http_peer_port_client, _set_http_scheme, _set_http_url, _set_status, _StabilityMode, ) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.requests.package import _instruments from opentelemetry.instrumentation.requests.version import __version__ from opentelemetry.instrumentation.utils import ( is_http_instrumentation_enabled, suppress_http_instrumentation, ) from opentelemetry.metrics import Histogram, get_meter from opentelemetry.propagate import inject from opentelemetry.semconv._incubating.attributes.user_agent_attributes import ( USER_AGENT_SYNTHETIC_TYPE, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.network_attributes import ( NETWORK_PEER_ADDRESS, NETWORK_PEER_PORT, ) from opentelemetry.semconv.attributes.user_agent_attributes import ( USER_AGENT_ORIGINAL, ) from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.semconv.metrics.http_metrics import ( HTTP_CLIENT_REQUEST_DURATION, ) from opentelemetry.trace import SpanKind, Tracer, get_tracer from opentelemetry.trace.span import Span from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, ExcludeList, detect_synthetic_user_agent, get_custom_header_attributes, get_custom_headers, get_excluded_urls, normalise_request_header_name, normalise_response_header_name, normalize_user_agent, parse_excluded_urls, redact_url, sanitize_method, ) from opentelemetry.util.http.httplib import set_ip_on_next_http_connection _excluded_urls_from_env = get_excluded_urls("REQUESTS") _RequestHookT = Callable[[Span, PreparedRequest], None] | None _ResponseHookT = Callable[[Span, PreparedRequest, Response], None] | None def _set_http_status_code_attribute( span, status_code, metric_attributes=None, sem_conv_opt_in_mode=_StabilityMode.DEFAULT, ): status_code_str = str(status_code) try: status_code = int(status_code) except ValueError: status_code = -1 if metric_attributes is None: metric_attributes = {} # When we have durations we should set metrics only once # Also the decision to include status code on a histogram should # not be dependent on tracing decisions. _set_status( span, metric_attributes, status_code, status_code_str, server_span=False, sem_conv_opt_in_mode=sem_conv_opt_in_mode, ) # pylint: disable=unused-argument # pylint: disable=R0915 def _instrument( tracer: Tracer, duration_histogram_old: Histogram, duration_histogram_new: Histogram, request_hook: _RequestHookT = None, response_hook: _ResponseHookT = None, excluded_urls: ExcludeList | None = None, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, captured_request_headers: list[str] | None = None, captured_response_headers: list[str] | None = None, sensitive_headers: list[str] | None = None, ): """Enables tracing of all requests calls that go through :code:`requests.session.Session.request` (this includes :code:`requests.get`, etc.).""" # Since # https://github.com/psf/requests/commit/d72d1162142d1bf8b1b5711c664fbbd674f349d1 # (v0.7.0, Oct 23, 2011), get, post, etc are implemented via request which # again, is implemented via Session.request (`Session` was named `session` # before v1.0.0, Dec 17, 2012, see # https://github.com/psf/requests/commit/4e5c4a6ab7bb0195dececdd19bb8505b872fe120) wrapped_send = Session.send # pylint: disable-msg=too-many-locals,too-many-branches @functools.wraps(wrapped_send) def instrumented_send(self: Session, request: PreparedRequest, **kwargs: Any): if excluded_urls and excluded_urls.url_disabled(request.url): return wrapped_send(self, request, **kwargs) def get_or_create_headers(): request.headers = request.headers if request.headers is not None else CaseInsensitiveDict() return request.headers if not is_http_instrumentation_enabled(): return wrapped_send(self, request, **kwargs) # See # https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#http-client method = request.method sanitized_method = sanitize_method(method.strip()) span_name = get_default_span_name(sanitized_method) url = redact_url(request.url) span_attributes = {} _set_http_method( span_attributes, method, sanitized_method, sem_conv_opt_in_mode, ) _set_http_url(span_attributes, url, sem_conv_opt_in_mode) # Check for synthetic user agent type headers = get_or_create_headers() user_agent_value = headers.get("User-Agent") user_agent = normalize_user_agent(user_agent_value) synthetic_type = detect_synthetic_user_agent(user_agent) if synthetic_type: span_attributes[USER_AGENT_SYNTHETIC_TYPE] = synthetic_type if user_agent: span_attributes[USER_AGENT_ORIGINAL] = user_agent span_attributes.update( get_custom_header_attributes( headers, captured_request_headers, sensitive_headers, normalise_request_header_name, ) ) metric_labels = {} _set_http_method( metric_labels, method, sanitized_method, sem_conv_opt_in_mode, ) try: parsed_url = urlparse(url) if parsed_url.scheme: if _report_old(sem_conv_opt_in_mode): # TODO: Support opt-in for url.scheme in new semconv _set_http_scheme(metric_labels, parsed_url.scheme, sem_conv_opt_in_mode) if parsed_url.hostname: _set_http_host_client(metric_labels, parsed_url.hostname, sem_conv_opt_in_mode) _set_http_net_peer_name_client(metric_labels, parsed_url.hostname, sem_conv_opt_in_mode) if _report_new(sem_conv_opt_in_mode): _set_http_host_client( span_attributes, parsed_url.hostname, sem_conv_opt_in_mode, ) # Use semconv library when available span_attributes[NETWORK_PEER_ADDRESS] = parsed_url.hostname if parsed_url.port: _set_http_peer_port_client(metric_labels, parsed_url.port, sem_conv_opt_in_mode) if _report_new(sem_conv_opt_in_mode): _set_http_peer_port_client(span_attributes, parsed_url.port, sem_conv_opt_in_mode) # Use semconv library when available span_attributes[NETWORK_PEER_PORT] = parsed_url.port except ValueError: pass with ( tracer.start_as_current_span(span_name, kind=SpanKind.CLIENT, attributes=span_attributes) as span, set_ip_on_next_http_connection(span), ): exception = None if callable(request_hook): request_hook(span, request) inject(headers) with suppress_http_instrumentation(): start_time = default_timer() try: result = wrapped_send(self, request, **kwargs) # *** PROCEED except Exception as exc: # pylint: disable=W0703 exception = exc result = getattr(exc, "response", None) finally: elapsed_time = max(default_timer() - start_time, 0) if isinstance(result, Response): span_attributes = {} _set_http_status_code_attribute( span, result.status_code, metric_labels, sem_conv_opt_in_mode, ) if result.raw is not None: version = getattr(result.raw, "version", None) if version: # Only HTTP/1 is supported by requests version_text = "1.1" if version == 11 else "1.0" _set_http_network_protocol_version(metric_labels, version_text, sem_conv_opt_in_mode) if _report_new(sem_conv_opt_in_mode): _set_http_network_protocol_version( span_attributes, version_text, sem_conv_opt_in_mode, ) span_attributes.update( get_custom_header_attributes( result.headers, captured_response_headers, sensitive_headers, normalise_response_header_name, ) ) for key, val in span_attributes.items(): span.set_attribute(key, val) if callable(response_hook): response_hook(span, request, result) if exception is not None and _report_new(sem_conv_opt_in_mode): span.set_attribute(ERROR_TYPE, type(exception).__qualname__) metric_labels[ERROR_TYPE] = type(exception).__qualname__ if duration_histogram_old is not None: duration_attrs_old = _filter_semconv_duration_attrs( metric_labels, _client_duration_attrs_old, _client_duration_attrs_new, _StabilityMode.DEFAULT, ) duration_histogram_old.record( max(round(elapsed_time * 1000), 0), attributes=duration_attrs_old, ) if duration_histogram_new is not None: duration_attrs_new = _filter_semconv_duration_attrs( metric_labels, _client_duration_attrs_old, _client_duration_attrs_new, _StabilityMode.HTTP, ) duration_histogram_new.record(elapsed_time, attributes=duration_attrs_new) if exception is not None: raise exception.with_traceback(exception.__traceback__) return result instrumented_send.opentelemetry_instrumentation_requests_applied = True Session.send = instrumented_send def _uninstrument(): """Disables instrumentation of :code:`requests` through this module. Note that this only works if no other module also patches requests.""" _uninstrument_from(Session) def _uninstrument_from(instr_root, restore_as_bound_func: bool = False): for instr_func_name in ("request", "send"): instr_func = getattr(instr_root, instr_func_name) if not getattr( instr_func, "opentelemetry_instrumentation_requests_applied", False, ): continue original = instr_func.__wrapped__ # pylint:disable=no-member if restore_as_bound_func: original = types.MethodType(original, instr_root) setattr(instr_root, instr_func_name, original) def get_default_span_name(method: str) -> str: """ Default implementation for name_callback, returns HTTP {method_name}. https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#name Args: method: string representing HTTP method Returns: span name """ if method == "_OTHER": return "HTTP" return method class RequestsInstrumentor(BaseInstrumentor): """An instrumentor for requests See `BaseInstrumentor` """ def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs: Any): """Instruments requests module Args: **kwargs: Optional arguments ``tracer_provider``: a TracerProvider, defaults to global ``request_hook``: An optional callback that is invoked right after a span is created. ``response_hook``: An optional callback which is invoked right before the span is finished processing a response. ``excluded_urls``: A string containing a comma-delimited list of regexes used to exclude URLs from tracking ``duration_histogram_boundaries``: A list of float values representing the explicit bucket boundaries for the duration histogram. """ semconv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) schema_url = _get_schema_url(semconv_opt_in_mode) tracer_provider = kwargs.get("tracer_provider") tracer = get_tracer( __name__, __version__, tracer_provider, schema_url=schema_url, ) excluded_urls = kwargs.get("excluded_urls") meter_provider = kwargs.get("meter_provider") duration_histogram_boundaries = kwargs.get("duration_histogram_boundaries") meter = get_meter( __name__, __version__, meter_provider, schema_url=schema_url, ) duration_histogram_old = None if _report_old(semconv_opt_in_mode): duration_histogram_old = meter.create_histogram( name=MetricInstruments.HTTP_CLIENT_DURATION, unit="ms", description="measures the duration of the outbound HTTP request", explicit_bucket_boundaries_advisory=duration_histogram_boundaries or HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, ) duration_histogram_new = None if _report_new(semconv_opt_in_mode): duration_histogram_new = meter.create_histogram( name=HTTP_CLIENT_REQUEST_DURATION, unit="s", description="Duration of HTTP client requests.", explicit_bucket_boundaries_advisory=duration_histogram_boundaries or HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) _instrument( tracer, duration_histogram_old, duration_histogram_new, request_hook=kwargs.get("request_hook"), response_hook=kwargs.get("response_hook"), excluded_urls=(_excluded_urls_from_env if excluded_urls is None else parse_excluded_urls(excluded_urls)), sem_conv_opt_in_mode=semconv_opt_in_mode, captured_request_headers=get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST), captured_response_headers=get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE), sensitive_headers=get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS), ) def _uninstrument(self, **kwargs: Any): _uninstrument() @staticmethod def uninstrument_session(session: Session): """Disables instrumentation on the session object.""" _uninstrument_from(session, restore_as_bound_func=True) package.py000066400000000000000000000002531527070072300455050ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("requests ~= 2.0",) _supports_metrics = True _semconv_status = "migration" py.typed000066400000000000000000000000001527070072300452250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requestsversion.py000066400000000000000000000001441527070072300455760ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000006231527070072300372010ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requestsasgiref==3.8.1 certifi==2024.7.4 charset-normalizer==3.3.2 Deprecated==1.2.14 mocket==3.14.1 idna==3.7 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 requests==2.33.1 tomli==2.0.1 typing_extensions==4.12.2 urllib3==2.7.0 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-requests opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requests/tests/000077500000000000000000000000001527070072300341605ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300362000ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requests/teststest_requests_integration.py000066400000000000000000001150671527070072300420020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requests/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines import abc from unittest import mock import requests from mocket import Mocket, Mocketizer from mocket.mocks.mockhttp import Entry from requests.adapters import BaseAdapter from requests.models import Response import opentelemetry.instrumentation.requests from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.instrumentation.utils import ( suppress_http_instrumentation, suppress_instrumentation, ) from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.sdk import resources from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_FLAVOR, HTTP_HOST, HTTP_METHOD, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_URL, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_REQUEST_METHOD_ORIGINAL, HTTP_RESPONSE_STATUS_CODE, ) from opentelemetry.semconv.attributes.network_attributes import ( NETWORK_PEER_ADDRESS, NETWORK_PEER_PORT, NETWORK_PROTOCOL_VERSION, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.semconv.attributes.url_attributes import URL_FULL from opentelemetry.semconv.attributes.user_agent_attributes import ( USER_AGENT_ORIGINAL, ) from opentelemetry.test.mock_textmap import MockTextMapPropagator from opentelemetry.test.test_base import TestBase from opentelemetry.trace import StatusCode from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, get_excluded_urls, ) class TransportMock: def read(self, *args, **kwargs): pass class MyAdapter(BaseAdapter): def __init__(self, response): super().__init__() self._response = response def send(self, *args, **kwargs): # pylint:disable=signature-differs return self._response def close(self): pass class InvalidResponseObjectException(Exception): def __init__(self): super().__init__() self.response = {} SCOPE = "opentelemetry.instrumentation.requests" class RequestsIntegrationTestBase(abc.ABC): # pylint: disable=no-member # pylint: disable=too-many-public-methods URL = "http://mock/status/200" # pylint: disable=invalid-name def setUp(self): super().setUp() test_name = "" if hasattr(self, "_testMethodName"): test_name = self._testMethodName sem_conv_mode = "default" if "new_semconv" in test_name: sem_conv_mode = "http" elif "both_semconv" in test_name: sem_conv_mode = "http/dup" self.env_patch = mock.patch.dict( "os.environ", { "OTEL_PYTHON_REQUESTS_EXCLUDED_URLS": "http://localhost/env_excluded_arg/123,env_excluded_noarg", OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) _OpenTelemetrySemanticConventionStability._initialized = False self.env_patch.start() self.exclude_patch = mock.patch( "opentelemetry.instrumentation.requests._excluded_urls_from_env", get_excluded_urls("REQUESTS"), ) self.exclude_patch.start() RequestsInstrumentor().instrument() self.mocketizer = Mocketizer(strict_mode=True) self.mocketizer.enter() Entry.single_register(Entry.GET, self.URL, body="Hello!") # pylint: disable=invalid-name def tearDown(self): super().tearDown() self.env_patch.stop() _OpenTelemetrySemanticConventionStability._initialized = False RequestsInstrumentor().uninstrument() self.mocketizer.exit() def assert_span(self, exporter=None, num_spans=1): if exporter is None: exporter = self.memory_exporter span_list = exporter.get_finished_spans() self.assertEqual(num_spans, len(span_list)) if num_spans == 0: return None if num_spans == 1: return span_list[0] return span_list @staticmethod @abc.abstractmethod def perform_request(url: str, session: requests.Session = None): pass def test_basic(self): result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "GET") self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.11.0", ) self.assertEqual( span.attributes, { HTTP_METHOD: "GET", HTTP_URL: self.URL, HTTP_STATUS_CODE: 200, USER_AGENT_ORIGINAL: f"python-requests/{requests.__version__}", }, ) self.assertIs(span.status.status_code, trace.StatusCode.UNSET) self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.requests) def test_basic_new_semconv(self): url_with_port = "http://mock:80/status/200" Entry.single_register(Entry.GET, url_with_port, status=200, body="Hello!") result = self.perform_request(url_with_port) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "GET") self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.21.0", ) self.assertEqual( span.attributes, { HTTP_REQUEST_METHOD: "GET", URL_FULL: url_with_port, SERVER_ADDRESS: "mock", NETWORK_PEER_ADDRESS: "mock", HTTP_RESPONSE_STATUS_CODE: 200, NETWORK_PROTOCOL_VERSION: "1.1", SERVER_PORT: 80, NETWORK_PEER_PORT: 80, USER_AGENT_ORIGINAL: f"python-requests/{requests.__version__}", }, ) self.assertIs(span.status.status_code, trace.StatusCode.UNSET) self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.requests) def test_basic_both_semconv(self): url_with_port = "http://mock:80/status/200" Entry.single_register(Entry.GET, url_with_port, status=200, body="Hello!") result = self.perform_request(url_with_port) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "GET") self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.21.0", ) self.assertEqual( span.attributes, { HTTP_METHOD: "GET", HTTP_REQUEST_METHOD: "GET", HTTP_URL: url_with_port, URL_FULL: url_with_port, HTTP_HOST: "mock", SERVER_ADDRESS: "mock", NETWORK_PEER_ADDRESS: "mock", NET_PEER_PORT: 80, HTTP_STATUS_CODE: 200, HTTP_RESPONSE_STATUS_CODE: 200, HTTP_FLAVOR: "1.1", NETWORK_PROTOCOL_VERSION: "1.1", SERVER_PORT: 80, NETWORK_PEER_PORT: 80, USER_AGENT_ORIGINAL: f"python-requests/{requests.__version__}", }, ) self.assertIs(span.status.status_code, trace.StatusCode.UNSET) self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.requests) @mock.patch( "mocket.mocks.mockhttp.Entry.METHODS", Entry.METHODS + ("NONSTANDARD",), ) def test_nonstandard_http_method(self): Entry.single_register("NONSTANDARD", self.URL, status=405) session = requests.Session() session.request("NONSTANDARD", self.URL) span = self.assert_span() self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "HTTP") self.assertEqual( span.attributes, { HTTP_METHOD: "_OTHER", HTTP_URL: self.URL, HTTP_STATUS_CODE: 405, USER_AGENT_ORIGINAL: f"python-requests/{requests.__version__}", }, ) self.assertIs(span.status.status_code, trace.StatusCode.ERROR) @mock.patch( "mocket.mocks.mockhttp.Entry.METHODS", Entry.METHODS + ("NONSTANDARD",), ) def test_nonstandard_http_method_new_semconv(self): Entry.single_register("NONSTANDARD", self.URL, status=405) session = requests.Session() session.request("NONSTANDARD", self.URL) span = self.assert_span() self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "HTTP") self.assertEqual( span.attributes, { HTTP_REQUEST_METHOD: "_OTHER", URL_FULL: self.URL, SERVER_ADDRESS: "mock", NETWORK_PEER_ADDRESS: "mock", HTTP_RESPONSE_STATUS_CODE: 405, NETWORK_PROTOCOL_VERSION: "1.1", ERROR_TYPE: "405", HTTP_REQUEST_METHOD_ORIGINAL: "NONSTANDARD", USER_AGENT_ORIGINAL: f"python-requests/{requests.__version__}", }, ) self.assertIs(span.status.status_code, trace.StatusCode.ERROR) def test_hooks(self): def request_hook(span, request_obj): span.update_name("name set from hook") def response_hook(span, request_obj, response): span.set_attribute("response_hook_attr", "value") RequestsInstrumentor().uninstrument() RequestsInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook) result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertEqual(span.name, "name set from hook") self.assertEqual(span.attributes["response_hook_attr"], "value") def test_excluded_urls_explicit(self): url_404 = "http://mock/status/404" Entry.single_register( Entry.GET, url_404, status=404, ) RequestsInstrumentor().uninstrument() RequestsInstrumentor().instrument(excluded_urls=".*/404") self.perform_request(self.URL) self.perform_request(url_404) self.assert_span(num_spans=1) def test_excluded_urls_from_env(self): url = "http://localhost/env_excluded_arg/123" Entry.single_register( Entry.GET, url, status=200, ) RequestsInstrumentor().uninstrument() RequestsInstrumentor().instrument() self.perform_request(self.URL) self.perform_request(url) self.assert_span(num_spans=1) def test_name_callback_default(self): def name_callback(method, url): return 123 RequestsInstrumentor().uninstrument() RequestsInstrumentor().instrument(name_callback=name_callback) result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertEqual(span.name, "GET") def test_not_foundbasic(self): url_404 = "http://mock/status/404" Entry.single_register( Entry.GET, url_404, status=404, ) result = self.perform_request(url_404) self.assertEqual(result.status_code, 404) span = self.assert_span() self.assertEqual(span.attributes.get(HTTP_STATUS_CODE), 404) self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) def test_not_foundbasic_new_semconv(self): url_404 = "http://mock/status/404" Entry.single_register( Entry.GET, url_404, status=404, ) result = self.perform_request(url_404) self.assertEqual(result.status_code, 404) span = self.assert_span() self.assertEqual(span.attributes.get(HTTP_RESPONSE_STATUS_CODE), 404) self.assertEqual(span.attributes.get(ERROR_TYPE), "404") self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) def test_not_foundbasic_both_semconv(self): url_404 = "http://mock/status/404" Entry.single_register( Entry.GET, url_404, status=404, ) result = self.perform_request(url_404) self.assertEqual(result.status_code, 404) span = self.assert_span() self.assertEqual(span.attributes.get(HTTP_STATUS_CODE), 404) self.assertEqual(span.attributes.get(HTTP_RESPONSE_STATUS_CODE), 404) self.assertEqual(span.attributes.get(ERROR_TYPE), "404") self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) def test_uninstrument(self): RequestsInstrumentor().uninstrument() result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") self.assert_span(num_spans=0) # instrument again to avoid annoying warning message RequestsInstrumentor().instrument() def test_uninstrument_session(self): session1 = requests.Session() RequestsInstrumentor().uninstrument_session(session1) result = self.perform_request(self.URL, session1) self.assertEqual(result.text, "Hello!") self.assert_span(num_spans=0) # Test that other sessions as well as global requests is still # instrumented session2 = requests.Session() result = self.perform_request(self.URL, session2) self.assertEqual(result.text, "Hello!") self.assert_span() self.memory_exporter.clear() result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") self.assert_span() def test_suppress_instrumentation(self): with suppress_instrumentation(): result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") self.assert_span(num_spans=0) def test_suppress_http_instrumentation(self): with suppress_http_instrumentation(): result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") self.assert_span(num_spans=0) def test_not_recording(self): with mock.patch("opentelemetry.trace.INVALID_SPAN") as mock_span: RequestsInstrumentor().uninstrument() RequestsInstrumentor().instrument(tracer_provider=trace.NoOpTracerProvider()) mock_span.is_recording.return_value = False result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") self.assert_span(None, 0) self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) def test_distributed_context(self): previous_propagator = get_global_textmap() try: set_global_textmap(MockTextMapPropagator()) result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") span = self.assert_span() headers = dict(Mocket.last_request().headers) self.assertIn(MockTextMapPropagator.TRACE_ID_KEY, headers) self.assertEqual( str(span.get_span_context().trace_id), headers[MockTextMapPropagator.TRACE_ID_KEY], ) self.assertIn(MockTextMapPropagator.SPAN_ID_KEY, headers) self.assertEqual( str(span.get_span_context().span_id), headers[MockTextMapPropagator.SPAN_ID_KEY], ) finally: set_global_textmap(previous_propagator) def test_response_hook(self): RequestsInstrumentor().uninstrument() def response_hook( span, request: requests.PreparedRequest, response: requests.Response, ): span.set_attribute("http.response.body", response.content.decode("utf-8")) RequestsInstrumentor().instrument( tracer_provider=self.tracer_provider, response_hook=response_hook, ) result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertEqual( span.attributes, { HTTP_METHOD: "GET", HTTP_URL: self.URL, HTTP_STATUS_CODE: 200, "http.response.body": "Hello!", USER_AGENT_ORIGINAL: f"python-requests/{requests.__version__}", }, ) def test_custom_tracer_provider(self): resource = resources.Resource.create({}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result RequestsInstrumentor().uninstrument() RequestsInstrumentor().instrument(tracer_provider=tracer_provider) result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") span = self.assert_span(exporter=exporter) self.assertIs(span.resource, resource) @mock.patch( "requests.adapters.HTTPAdapter.send", side_effect=requests.RequestException, ) def test_requests_exception_without_response(self, *_, **__): with self.assertRaises(requests.RequestException): self.perform_request(self.URL) span = self.assert_span() self.assertEqual( span.attributes, { HTTP_METHOD: "GET", HTTP_URL: self.URL, USER_AGENT_ORIGINAL: f"python-requests/{requests.__version__}", }, ) self.assertEqual(span.status.status_code, StatusCode.ERROR) @mock.patch( "requests.adapters.HTTPAdapter.send", side_effect=requests.RequestException, ) def test_requests_exception_new_semconv(self, *_, **__): url_with_port = "http://mock:80/status/200" Entry.single_register(Entry.GET, url_with_port, status=200, body="Hello!") with self.assertRaises(requests.RequestException): self.perform_request(url_with_port) span = self.assert_span() self.assertEqual( span.attributes, { HTTP_REQUEST_METHOD: "GET", URL_FULL: url_with_port, SERVER_ADDRESS: "mock", SERVER_PORT: 80, NETWORK_PEER_PORT: 80, NETWORK_PEER_ADDRESS: "mock", ERROR_TYPE: "RequestException", USER_AGENT_ORIGINAL: f"python-requests/{requests.__version__}", }, ) self.assertEqual(span.status.status_code, StatusCode.ERROR) mocked_response = requests.Response() mocked_response.status_code = 500 mocked_response.reason = "Internal Server Error" @mock.patch( "requests.adapters.HTTPAdapter.send", side_effect=InvalidResponseObjectException, ) def test_requests_exception_without_proper_response_type(self, *_, **__): with self.assertRaises(InvalidResponseObjectException): self.perform_request(self.URL) span = self.assert_span() self.assertEqual( span.attributes, { HTTP_METHOD: "GET", HTTP_URL: self.URL, USER_AGENT_ORIGINAL: f"python-requests/{requests.__version__}", }, ) self.assertEqual(span.status.status_code, StatusCode.ERROR) mocked_response = requests.Response() mocked_response.status_code = 500 mocked_response.reason = "Internal Server Error" @mock.patch( "requests.adapters.HTTPAdapter.send", side_effect=requests.RequestException(response=mocked_response), ) def test_requests_exception_with_response(self, *_, **__): with self.assertRaises(requests.RequestException): self.perform_request(self.URL) span = self.assert_span() self.assertEqual( span.attributes, { HTTP_METHOD: "GET", HTTP_URL: self.URL, HTTP_STATUS_CODE: 500, USER_AGENT_ORIGINAL: f"python-requests/{requests.__version__}", }, ) self.assertEqual(span.status.status_code, StatusCode.ERROR) @mock.patch("requests.adapters.HTTPAdapter.send", side_effect=Exception) def test_requests_basic_exception(self, *_, **__): with self.assertRaises(Exception): self.perform_request(self.URL) span = self.assert_span() self.assertEqual(span.status.status_code, StatusCode.ERROR) @mock.patch("requests.adapters.HTTPAdapter.send", side_effect=requests.Timeout) def test_requests_timeout_exception(self, *_, **__): with self.assertRaises(Exception): self.perform_request(self.URL) span = self.assert_span() self.assertEqual(span.status.status_code, StatusCode.ERROR) def test_adapter_with_custom_response(self): response = Response() response.status_code = 210 response.reason = "hello adapter" response.raw = TransportMock() session = requests.Session() session.mount(self.URL, MyAdapter(response)) self.perform_request(self.URL, session) span = self.assert_span() self.assertEqual( span.attributes, { "http.method": "GET", "http.url": self.URL, "http.status_code": 210, USER_AGENT_ORIGINAL: f"python-requests/{requests.__version__}", }, ) class TestRequestsIntegration(RequestsIntegrationTestBase, TestBase): @staticmethod def perform_request(url: str, session: requests.Session = None): if session is None: return requests.get(url, timeout=5) return session.get(url) def test_remove_sensitive_params(self): new_url = "http://username:password@mock/status/200?AWSAccessKeyId=secret" Entry.single_register(Entry.GET, new_url, body="Hello!") self.perform_request(new_url) span = self.assert_span() self.assertEqual( span.attributes[HTTP_URL], "http://REDACTED:REDACTED@mock/status/200?AWSAccessKeyId=REDACTED", ) def test_if_headers_equals_none(self): result = requests.get(self.URL, headers=None, timeout=5) self.assertEqual(result.text, "Hello!") self.assert_span() @mock.patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST: "X-Custom-Header,X-Another-Header", }, ) def test_custom_request_headers_captured(self): """Test that specified request headers are captured as span attributes.""" RequestsInstrumentor().uninstrument() RequestsInstrumentor().instrument() headers = { "X-Custom-Header": "custom-value", "X-Another-Header": "another-value", "X-Excluded-Header": "excluded-value", } Entry.single_register(Entry.GET, self.URL, body="Hello!") result = requests.get(self.URL, headers=headers, timeout=5) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertEqual( span.attributes["http.request.header.x_custom_header"], ("custom-value",), ) self.assertEqual( span.attributes["http.request.header.x_another_header"], ("another-value",), ) self.assertNotIn("http.request.x_excluded_header", span.attributes) @mock.patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE: "X-Custom-Header,X-Another-Header", }, ) def test_custom_response_headers_captured(self): """Test that specified request headers are captured as span attributes.""" RequestsInstrumentor().uninstrument() RequestsInstrumentor().instrument() headers = { "X-Custom-Header": "custom-value", "X-Another-Header": "another-value", "X-Excluded-Header": "excluded-value", } Mocket.reset() Entry.single_register(Entry.GET, self.URL, body="Hello!", headers=headers) result = requests.get(self.URL, timeout=5) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertEqual( span.attributes["http.response.header.x_custom_header"], ("custom-value",), ) self.assertEqual( span.attributes["http.response.header.x_another_header"], ("another-value",), ) self.assertNotIn("http.response.x_excluded_header", span.attributes) @mock.patch.dict("os.environ", {}) def test_custom_headers_not_captured_when_not_configured(self): """Test that headers are not captured when env vars are not set.""" RequestsInstrumentor().uninstrument() RequestsInstrumentor().instrument() headers = {"X-Request-Header": "request-value"} Mocket.reset() Entry.single_register( Entry.GET, self.URL, body="Hello!", headers={"X-Response-Header": "response-value"}, ) result = requests.get(self.URL, headers=headers, timeout=5) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertNotIn("http.request.header.x_request_header", span.attributes) self.assertNotIn("http.response.header.x_response_header", span.attributes) @mock.patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE: "Set-Cookie,X-Secret", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST: "Authorization,X-Api-Key", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: "Authorization,X-Api-Key,Set-Cookie,X-Secret", }, ) def test_sensitive_headers_sanitized(self): """Test that sensitive header values are redacted.""" RequestsInstrumentor().uninstrument() RequestsInstrumentor().instrument() request_headers = { "Authorization": "Bearer secret-token", "X-Api-Key": "secret-key", } response_headers = { "Set-Cookie": "session=abc123", "X-Secret": "secret", } Mocket.reset() Entry.single_register( Entry.GET, self.URL, body="Hello!", headers=response_headers, ) result = requests.get(self.URL, headers=request_headers, timeout=5) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertEqual( span.attributes["http.request.header.authorization"], ("[REDACTED]",), ) self.assertEqual( span.attributes["http.request.header.x_api_key"], ("[REDACTED]",), ) self.assertEqual( span.attributes["http.response.header.set_cookie"], ("[REDACTED]",), ) self.assertEqual( span.attributes["http.response.header.x_secret"], ("[REDACTED]",), ) @mock.patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE: "X-Custom-Response-.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST: "X-Custom-Request-.*", }, ) def test_custom_headers_with_regex(self): """Test that header capture works with regex patterns.""" RequestsInstrumentor().uninstrument() RequestsInstrumentor().instrument() request_headers = { "X-Custom-Request-One": "value-one", "X-Custom-Request-Two": "value-two", "X-Other-Request-Header": "other-value", } response_headers = { "X-Custom-Response-A": "value-A", "X-Custom-Response-B": "value-B", "X-Other-Response-Header": "other-value", } Mocket.reset() Entry.single_register( Entry.GET, self.URL, body="Hello!", headers=response_headers, ) result = requests.get(self.URL, headers=request_headers, timeout=5) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertEqual( span.attributes["http.request.header.x_custom_request_one"], ("value-one",), ) self.assertEqual( span.attributes["http.request.header.x_custom_request_two"], ("value-two",), ) self.assertNotIn("http.request.header.x_other_request_header", span.attributes) self.assertEqual( span.attributes["http.response.header.x_custom_response_a"], ("value-A",), ) self.assertEqual( span.attributes["http.response.header.x_custom_response_b"], ("value-B",), ) self.assertNotIn("http.response.header.x_other_response_header", span.attributes) @mock.patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE: "x-response-header", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST: "x-request-header", }, ) def test_custom_headers_case_insensitive(self): """Test that header capture is case-insensitive.""" RequestsInstrumentor().uninstrument() RequestsInstrumentor().instrument() request_headers = {"X-ReQuESt-HeaDER": "custom-value"} response_headers = {"X-ReSPoNse-HeaDER": "custom-value"} Mocket.reset() Entry.single_register( Entry.GET, self.URL, body="Hello!", headers=response_headers, ) result = requests.get(self.URL, headers=request_headers, timeout=5) self.assertEqual(result.text, "Hello!") span = self.assert_span() self.assertEqual( span.attributes["http.request.header.x_request_header"], ("custom-value",), ) self.assertEqual( span.attributes["http.response.header.x_response_header"], ("custom-value",), ) class TestRequestsIntegrationPreparedRequest(RequestsIntegrationTestBase, TestBase): @staticmethod def perform_request(url: str, session: requests.Session = None): if session is None: session = requests.Session() request = requests.Request("GET", url) prepared_request = session.prepare_request(request) return session.send(prepared_request) class TestRequestsIntergrationMetric(TestBase): URL = "http://examplehost:8000/status/200" def setUp(self): super().setUp() test_name = "" if hasattr(self, "_testMethodName"): test_name = self._testMethodName sem_conv_mode = "default" if "new_semconv" in test_name: sem_conv_mode = "http" elif "both_semconv" in test_name: sem_conv_mode = "http/dup" self.env_patch = mock.patch.dict( "os.environ", { OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) self.env_patch.start() _OpenTelemetrySemanticConventionStability._initialized = False RequestsInstrumentor().instrument(meter_provider=self.meter_provider) self.mocketizer = Mocketizer(strict_mode=True) self.mocketizer.enter() Entry.single_register(Entry.GET, self.URL, body="Hello!") def tearDown(self): super().tearDown() self.env_patch.stop() _OpenTelemetrySemanticConventionStability._initialized = False RequestsInstrumentor().uninstrument() self.mocketizer.exit() @staticmethod def perform_request(url: str) -> requests.Response: return requests.get(url, timeout=5) def test_basic_metric_success(self): self.perform_request(self.URL) expected_attributes = { HTTP_STATUS_CODE: 200, HTTP_HOST: "examplehost", NET_PEER_PORT: 8000, NET_PEER_NAME: "examplehost", HTTP_METHOD: "GET", HTTP_FLAVOR: "1.1", HTTP_SCHEME: "http", } metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 1) for metric in metrics: self.assertEqual(metric.unit, "ms") self.assertEqual( metric.description, "measures the duration of the outbound HTTP request", ) for data_point in metric.data.data_points: self.assertEqual( data_point.explicit_bounds, HTTP_DURATION_HISTOGRAM_BUCKETS_OLD, ) self.assertDictEqual(expected_attributes, dict(data_point.attributes)) self.assertEqual(data_point.count, 1) def test_basic_metric_new_semconv(self): self.perform_request(self.URL) expected_attributes = { HTTP_RESPONSE_STATUS_CODE: 200, SERVER_ADDRESS: "examplehost", SERVER_PORT: 8000, HTTP_REQUEST_METHOD: "GET", NETWORK_PROTOCOL_VERSION: "1.1", } metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 1) for metric in metrics: self.assertEqual(metric.unit, "s") self.assertEqual(metric.description, "Duration of HTTP client requests.") for data_point in metric.data.data_points: self.assertEqual( data_point.explicit_bounds, HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) self.assertDictEqual(expected_attributes, dict(data_point.attributes)) self.assertEqual(data_point.count, 1) def test_basic_metric_both_semconv(self): self.perform_request(self.URL) expected_attributes_old = { HTTP_STATUS_CODE: 200, HTTP_HOST: "examplehost", NET_PEER_PORT: 8000, NET_PEER_NAME: "examplehost", HTTP_METHOD: "GET", HTTP_FLAVOR: "1.1", HTTP_SCHEME: "http", } expected_attributes_new = { HTTP_RESPONSE_STATUS_CODE: 200, SERVER_ADDRESS: "examplehost", SERVER_PORT: 8000, HTTP_REQUEST_METHOD: "GET", NETWORK_PROTOCOL_VERSION: "1.1", } metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 2) for metric in metrics: for data_point in metric.data.data_points: if metric.unit == "ms": self.assertDictEqual( expected_attributes_old, dict(data_point.attributes), ) else: self.assertDictEqual( expected_attributes_new, dict(data_point.attributes), ) self.assertEqual(data_point.count, 1) def test_custom_histogram_boundaries(self): RequestsInstrumentor().uninstrument() custom_boundaries = (0.0, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0) meter_provider, memory_reader = self.create_meter_provider() RequestsInstrumentor().instrument( meter_provider=meter_provider, duration_histogram_boundaries=custom_boundaries, ) self.perform_request(self.URL) metrics = memory_reader.get_metrics_data().resource_metrics[0] self.assertEqual(len(metrics.scope_metrics), 1) data_point = metrics.scope_metrics[0].metrics[0].data.data_points[0] self.assertEqual(data_point.explicit_bounds, custom_boundaries) self.assertEqual(data_point.count, 1) def test_custom_histogram_boundaries_new_semconv(self): RequestsInstrumentor().uninstrument() custom_boundaries = (0.0, 5.0, 10.0, 25.0, 50.0, 100.0) meter_provider, memory_reader = self.create_meter_provider() RequestsInstrumentor().instrument( meter_provider=meter_provider, duration_histogram_boundaries=custom_boundaries, ) self.perform_request(self.URL) metrics = memory_reader.get_metrics_data().resource_metrics[0] self.assertEqual(len(metrics.scope_metrics), 1) data_point = metrics.scope_metrics[0].metrics[0].data.data_points[0] self.assertEqual(data_point.explicit_bounds, custom_boundaries) self.assertEqual(data_point.count, 1) def test_basic_metric_non_recording_span(self): expected_attributes = { HTTP_STATUS_CODE: 200, HTTP_HOST: "examplehost", NET_PEER_PORT: 8000, NET_PEER_NAME: "examplehost", HTTP_METHOD: "GET", HTTP_FLAVOR: "1.1", HTTP_SCHEME: "http", } with mock.patch("opentelemetry.trace.INVALID_SPAN") as mock_span: RequestsInstrumentor().uninstrument() RequestsInstrumentor().instrument(tracer_provider=trace.NoOpTracerProvider()) mock_span.is_recording.return_value = False result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 1) duration_data_point = metrics[0].data.data_points[0] self.assertDictEqual(expected_attributes, dict(duration_data_point.attributes)) self.assertEqual(duration_data_point.count, 1) test_requests_ip_support.py000066400000000000000000000046361527070072300416620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requests/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import requests from opentelemetry import trace from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.test.httptest import HttpTestBase from opentelemetry.test.test_base import TestBase from opentelemetry.util.http.httplib import HttpClientInstrumentor class TestURLLib3InstrumentorWithRealSocket(HttpTestBase, TestBase): def setUp(self): super().setUp() self.assert_ip = self.server.server_address[0] self.http_host = ":".join(map(str, self.server.server_address[:2])) self.http_url_base = "http://" + self.http_host self.http_url = self.http_url_base + "/status/200" HttpClientInstrumentor().instrument() RequestsInstrumentor().instrument() def tearDown(self): super().tearDown() HttpClientInstrumentor().uninstrument() RequestsInstrumentor().uninstrument() @staticmethod def perform_request(url: str) -> requests.Response: return requests.get(url, timeout=5) def test_basic_http_success(self): response = self.perform_request(self.http_url) self.assert_success_span(response) def test_basic_http_success_using_connection_pool(self): with requests.Session() as session: response = session.get(self.http_url) self.assert_success_span(response) # Test that when re-using an existing connection, everything still works. # Especially relevant for IP capturing. response = session.get(self.http_url) self.assert_success_span(response) def assert_span(self, num_spans=1): # TODO: Move this to TestBase span_list = self.memory_exporter.get_finished_spans() self.assertEqual(num_spans, len(span_list)) if num_spans == 0: return None self.memory_exporter.clear() if num_spans == 1: return span_list[0] return span_list def assert_success_span(self, response: requests.Response): self.assertEqual("Hello!", response.text) span = self.assert_span() self.assertIs(trace.SpanKind.CLIENT, span.kind) self.assertEqual("GET", span.name) attributes = { "http.status_code": 200, "net.peer.ip": self.assert_ip, } self.assertGreaterEqual(span.attributes.items(), attributes.items()) test_user_agent_synthetic.py000066400000000000000000000150301527070072300417370ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-requests/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import mock import requests from mocket import Mocketizer from mocket.mocks.mockhttp import Entry from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.semconv._incubating.attributes.user_agent_attributes import ( USER_AGENT_ORIGINAL, USER_AGENT_SYNTHETIC_TYPE, UserAgentSyntheticTypeValues, ) from opentelemetry.test.test_base import TestBase class TestUserAgentSynthetic(TestBase): URL = "http://mock/status/200" def setUp(self): super().setUp() RequestsInstrumentor().instrument() self.mocketizer = Mocketizer(strict_mode=True) self.mocketizer.enter() Entry.single_register(Entry.GET, self.URL, body="Hello!") def tearDown(self): super().tearDown() RequestsInstrumentor().uninstrument() self.mocketizer.exit() def assert_span(self, num_spans=1): span_list = self.memory_exporter.get_finished_spans() self.assertEqual(num_spans, len(span_list)) if num_spans == 0: return None if num_spans == 1: return span_list[0] return span_list def test_user_agent_bot_googlebot(self): """Test that googlebot user agent is marked as 'bot'""" headers = {"User-Agent": "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"} requests.get(self.URL, headers=headers, timeout=5) span = self.assert_span() self.assertEqual( span.attributes.get(USER_AGENT_SYNTHETIC_TYPE), UserAgentSyntheticTypeValues.BOT.value, ) def test_user_agent_bot_bingbot(self): """Test that bingbot user agent is marked as 'bot'""" headers = {"User-Agent": "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"} requests.get(self.URL, headers=headers, timeout=5) span = self.assert_span() self.assertEqual( span.attributes.get(USER_AGENT_SYNTHETIC_TYPE), UserAgentSyntheticTypeValues.BOT.value, ) def test_user_agent_test_alwayson(self): """Test that alwayson user agent is marked as 'test'""" headers = {"User-Agent": "AlwaysOn-Monitor/1.0"} requests.get(self.URL, headers=headers, timeout=5) span = self.assert_span() self.assertEqual( span.attributes.get(USER_AGENT_SYNTHETIC_TYPE), UserAgentSyntheticTypeValues.TEST.value, ) def test_user_agent_case_insensitive(self): """Test that detection is case insensitive""" headers = {"User-Agent": "GOOGLEBOT/2.1"} requests.get(self.URL, headers=headers, timeout=5) span = self.assert_span() self.assertEqual( span.attributes.get(USER_AGENT_SYNTHETIC_TYPE), UserAgentSyntheticTypeValues.BOT.value, ) self.memory_exporter.clear() headers = {"User-Agent": "ALWAYSON-Monitor/1.0"} requests.get(self.URL, headers=headers, timeout=5) span = self.assert_span() self.assertEqual( span.attributes.get(USER_AGENT_SYNTHETIC_TYPE), UserAgentSyntheticTypeValues.TEST.value, ) def test_user_agent_normal_browser(self): """Test that normal browser user agents don't get synthetic type""" headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"} requests.get(self.URL, headers=headers, timeout=5) span = self.assert_span() self.assertNotIn(USER_AGENT_SYNTHETIC_TYPE, span.attributes) def test_no_user_agent_header(self): """Test that requests without user agent don't get synthetic type""" requests.get(self.URL, timeout=5) span = self.assert_span() self.assertNotIn(USER_AGENT_SYNTHETIC_TYPE, span.attributes) def test_empty_user_agent_header(self): """Test that empty user agent doesn't get synthetic type""" headers = {"User-Agent": ""} requests.get(self.URL, headers=headers, timeout=5) span = self.assert_span() self.assertNotIn(USER_AGENT_SYNTHETIC_TYPE, span.attributes) def test_user_agent_substring_match(self): """Test that substrings are detected correctly""" # Test googlebot in middle of string headers = {"User-Agent": "MyApp/1.0 googlebot crawler"} requests.get(self.URL, headers=headers, timeout=5) span = self.assert_span() self.assertEqual( span.attributes.get(USER_AGENT_SYNTHETIC_TYPE), UserAgentSyntheticTypeValues.BOT.value, ) self.memory_exporter.clear() # Test alwayson in middle of string headers = {"User-Agent": "TestFramework/1.0 alwayson monitoring"} requests.get(self.URL, headers=headers, timeout=5) span = self.assert_span() self.assertEqual( span.attributes.get(USER_AGENT_SYNTHETIC_TYPE), UserAgentSyntheticTypeValues.TEST.value, ) def test_user_agent_priority_alwayson_over_bot(self): """Test that alwayson takes priority if both patterns match""" headers = {"User-Agent": "alwayson-googlebot/1.0"} requests.get(self.URL, headers=headers, timeout=5) span = self.assert_span() # alwayson should be checked first and return 'test' self.assertEqual( span.attributes.get(USER_AGENT_SYNTHETIC_TYPE), UserAgentSyntheticTypeValues.TEST.value, ) def test_user_agent_bytes_like_header(self): """Test that bytes-like user agent headers are handled.""" original_prepare_headers = requests.models.PreparedRequest.prepare_headers def prepare_headers_bytes(self, headers): original_prepare_headers(self, headers) if "User-Agent" in self.headers: value = self.headers["User-Agent"] if isinstance(value, str): self.headers["User-Agent"] = value.encode("utf-8") headers = {"User-Agent": "AlwaysOn-Monitor/1.0"} with mock.patch( "requests.models.PreparedRequest.prepare_headers", new=prepare_headers_bytes, ): requests.get(self.URL, headers=headers, timeout=5) span = self.assert_span() self.assertEqual( span.attributes.get(USER_AGENT_SYNTHETIC_TYPE), UserAgentSyntheticTypeValues.TEST.value, ) self.assertEqual( span.attributes.get(USER_AGENT_ORIGINAL), "AlwaysOn-Monitor/1.0", ) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemy/000077500000000000000000000000001527070072300333055ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemy/LICENSE000066400000000000000000000261351527070072300343210ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000014531527070072300347200ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemyOpenTelemetry SQLAlchemy Instrumentation ======================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-sqlalchemy.svg :target: https://pypi.org/project/opentelemetry-instrumentation-sqlalchemy/ This library allows tracing requests made by the SQLAlchemy library. Installation ------------ :: pip install opentelemetry-instrumentation-sqlalchemy References ---------- * `SQLAlchemy Project `_ * `OpenTelemetry SQLAlchemy Tracing `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_pyproject.toml000066400000000000000000000032501527070072300361420ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemy[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-sqlalchemy" dynamic = ["version"] description = "OpenTelemetry SQLAlchemy instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "packaging >= 21.0", "wrapt >= 1.11.2", ] [project.optional-dependencies] instruments = [ "sqlalchemy >= 1.0.0, < 2.1.0", ] [project.entry-points.opentelemetry_instrumentor] sqlalchemy = "opentelemetry.instrumentation.sqlalchemy:SQLAlchemyInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-sqlalchemy" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/sqlalchemy/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/000077500000000000000000000000001527070072300340745ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300367115ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemy/srcinstrumentation/000077500000000000000000000000001527070072300421545ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetrysqlalchemy/000077500000000000000000000000001527070072300443165ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation__init__.py000066400000000000000000000271501527070072300464340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Instrument `sqlalchemy`_ to report SQL queries. There are two options for instrumenting code. The first option is to use the ``opentelemetry-instrument`` executable which will automatically instrument your SQLAlchemy engine. The second is to programmatically enable instrumentation via the following code: .. _sqlalchemy: https://pypi.org/project/sqlalchemy/ Usage ----- .. code:: python from sqlalchemy import create_engine from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor import sqlalchemy engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, ) .. code:: python # of the async variant of SQLAlchemy from sqlalchemy.ext.asyncio import create_async_engine from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor import sqlalchemy engine = create_async_engine("sqlite+aiosqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine.sync_engine ) Configuration ------------- SQLCommenter ************ You can optionally enable sqlcommenter which enriches the query with contextual information. Queries made after setting up trace integration with sqlcommenter enabled will have configurable key-value pairs appended to them, e.g. ``"select * from auth_users; /*traceparent=00-01234567-abcd-01*/"``. This supports context propagation between database client and server when database log records are enabled. For more information, see: * `Semantic Conventions - Database Spans `_ * `sqlcommenter `_ .. code:: python from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor SQLAlchemyInstrumentor().instrument(enable_commenter=True) SQLCommenter with commenter_options *********************************** The key-value pairs appended to the query can be configured using ``commenter_options``. When sqlcommenter is enabled, all available KVs/tags are calculated by default. ``commenter_options`` supports *opting out* of specific KVs. .. code:: python from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor # Opts into sqlcomment for SQLAlchemy trace integration. # Opts out of tags for db_driver, db_framework. SQLAlchemyInstrumentor().instrument( enable_commenter=True, commenter_options={ "db_driver": False, "db_framework": False, } ) Available commenter_options ########################### The following sqlcomment key-values can be opted out of through ``commenter_options``: +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | Commenter Option | Description | Example | +===========================+===========================================================+===========================================================================+ | ``db_driver`` | Database driver name. | ``db_driver='psycopg2'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``db_framework`` | Database framework name with version. | ``db_framework='sqlalchemy:1.4.0'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ | ``opentelemetry_values`` | OpenTelemetry context as traceparent at time of query. | ``traceparent='00-03afa25236b8cd948fa853d67038ac79-405ff022e8247c46-01'`` | +---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+ SQLComment in span attribute **************************** If sqlcommenter is enabled, you can opt into the inclusion of sqlcomment in the query span ``db.statement`` and/or ``db.query.text`` attribute for your needs. If ``commenter_options`` have been set, the span attribute comment will also be configured by this setting. .. code:: python from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor # Opts into sqlcomment for SQLAlchemy trace integration. # Opts into sqlcomment for `db.statement` and/or `db.query.text` span attribute. SQLAlchemyInstrumentor().instrument( enable_commenter=True, commenter_options={}, enable_attribute_commenter=True, ) Warning: Capture of sqlcomment in ``db.statement``/``db.query.text`` may have high cardinality without platform normalization. See `Semantic Conventions for database spans `_ for more information. API --- """ from collections.abc import Collection, Sequence import sqlalchemy from packaging.version import parse as parse_version from sqlalchemy.engine.base import Engine from wrapt import wrap_function_wrapper as _w from opentelemetry.instrumentation._semconv import ( _get_schema_url_for_signal_types, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, ) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.sqlalchemy.engine import ( EngineTracer, _wrap_connect, _wrap_create_async_engine, _wrap_create_engine, ) from opentelemetry.instrumentation.sqlalchemy.package import _instruments from opentelemetry.instrumentation.sqlalchemy.version import __version__ from opentelemetry.instrumentation.utils import unwrap from opentelemetry.metrics import get_meter from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.trace import get_tracer class SQLAlchemyInstrumentor(BaseInstrumentor): """An instrumentor for SQLAlchemy See `BaseInstrumentor` """ def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): """Instruments SQLAlchemy engine creation methods and the engine if passed as an argument. Args: **kwargs: Optional arguments ``engine``: a SQLAlchemy engine instance ``engines``: a list of SQLAlchemy engine instances ``tracer_provider``: a TracerProvider, defaults to global ``meter_provider``: a MeterProvider, defaults to global ``enable_commenter``: bool to enable sqlcommenter, defaults to False ``commenter_options``: dict of sqlcommenter config, defaults to {} ``enable_attribute_commenter``: bool to enable sqlcomment addition to span attribute, defaults to False. Must also set `enable_commenter`. Returns: An instrumented engine if passed in as an argument or list of instrumented engines, None otherwise. """ # Initialize semantic conventions opt-in if needed _OpenTelemetrySemanticConventionStability._initialize() # Determine schema URL based on both DATABASE and HTTP signal types # and semconv opt-in mode schema_url = _get_schema_url_for_signal_types( [ _OpenTelemetryStabilitySignalType.DATABASE, _OpenTelemetryStabilitySignalType.HTTP, ] ) tracer_provider = kwargs.get("tracer_provider") tracer = get_tracer( __name__, __version__, tracer_provider, schema_url=schema_url, ) meter_provider = kwargs.get("meter_provider") meter = get_meter( __name__, __version__, meter_provider, schema_url=schema_url, ) connections_usage = meter.create_up_down_counter( name=MetricInstruments.DB_CLIENT_CONNECTIONS_USAGE, unit="connections", description="The number of connections that are currently in state described by the state attribute.", ) enable_commenter = kwargs.get("enable_commenter", False) commenter_options = kwargs.get("commenter_options", {}) enable_attribute_commenter = kwargs.get("enable_attribute_commenter", False) _w( "sqlalchemy", "create_engine", _wrap_create_engine( tracer, connections_usage, enable_commenter, commenter_options, enable_attribute_commenter, ), ) _w( "sqlalchemy.engine", "create_engine", _wrap_create_engine( tracer, connections_usage, enable_commenter, commenter_options, enable_attribute_commenter, ), ) # sqlalchemy.engine.create is not present in earlier versions of sqlalchemy (which we support) if parse_version(sqlalchemy.__version__).release >= (1, 4): _w( "sqlalchemy.engine.create", "create_engine", _wrap_create_engine( tracer, connections_usage, enable_commenter, commenter_options, enable_attribute_commenter, ), ) _w( "sqlalchemy.engine.base", "Engine.connect", _wrap_connect(tracer), ) if parse_version(sqlalchemy.__version__).release >= (1, 4): _w( "sqlalchemy.ext.asyncio", "create_async_engine", _wrap_create_async_engine( tracer, connections_usage, enable_commenter, commenter_options, enable_attribute_commenter, ), ) if kwargs.get("engine") is not None: return EngineTracer( tracer, kwargs.get("engine"), connections_usage, kwargs.get("enable_commenter", False), kwargs.get("commenter_options", {}), kwargs.get("enable_attribute_commenter", False), ) if kwargs.get("engines") is not None and isinstance(kwargs.get("engines"), Sequence): return [ EngineTracer( tracer, engine, connections_usage, kwargs.get("enable_commenter", False), kwargs.get("commenter_options", {}), kwargs.get("enable_attribute_commenter", False), ) for engine in kwargs.get("engines") ] return None def _uninstrument(self, **kwargs): unwrap(sqlalchemy, "create_engine") unwrap(sqlalchemy.engine, "create_engine") if parse_version(sqlalchemy.__version__).release >= (1, 4): unwrap(sqlalchemy.engine.create, "create_engine") unwrap(Engine, "connect") if parse_version(sqlalchemy.__version__).release >= (1, 4): unwrap(sqlalchemy.ext.asyncio, "create_async_engine") EngineTracer.remove_all_event_listeners() engine.py000066400000000000000000000427701527070072300461470ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import re import weakref import sqlalchemy from sqlalchemy.event import ( # pylint: disable=no-name-in-module listen, remove, ) from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _set_db_name, _set_db_operation, _set_db_statement, _set_db_system, _set_db_user, _set_http_net_peer_name_client, _set_http_peer_port_client, ) from opentelemetry.instrumentation.sqlcommenter_utils import _add_sql_comment from opentelemetry.instrumentation.utils import ( _get_opentelemetry_values, is_instrumentation_enabled, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_TRANSPORT, NetTransportValues, ) from opentelemetry.trace.status import Status, StatusCode def _get_db_name_from_cursor_or_conn(vendor, conn, cursor): """Return DB name from cursor or connection when available -- else None.""" if not vendor: return None vendor = vendor.lower() db_name = None if "postgres" in vendor: info = getattr(getattr(cursor, "connection", None), "info", None) if info and hasattr(info, "dbname"): db_name = info.dbname elif "mysql" in vendor: db_name = _get_mysql_db_name(cursor) elif "mssql" in vendor or "sqlserver" in vendor: db_name = _get_mssql_db_name(cursor) if not db_name: engine = getattr(conn, "engine", None) url = getattr(engine, "url", None) db_name = getattr(url, "database", None) else: # Try connection for sqlite and others engine = getattr(conn, "engine", None) url = getattr(engine, "url", None) db_name = getattr(url, "database", None) return db_name def _get_mysql_db_name(cursor): """Extract database name from MySQL cursor.""" # mysql-connector with c-extension uses _cnx connection = getattr(cursor, "connection", None) or getattr(cursor, "_cnx", None) if not connection: return None if hasattr(connection, "database"): return connection.database if hasattr(connection, "db"): raw_db_name = connection.db return raw_db_name.decode("utf-8") if isinstance(raw_db_name, bytes) else raw_db_name return None def _get_mssql_db_name(cursor): """Extract database name from MSSQL cursor.""" connection = getattr(cursor, "connection", None) if not connection: return None if hasattr(connection, "database"): return connection.database if hasattr(connection, "db"): return connection.db info = getattr(connection, "info", None) if info and hasattr(info, "database"): return info.database return None def _normalize_vendor(vendor): """Return a canonical name for a type of database.""" if not vendor: return "db" # should this ever happen? if "sqlite" in vendor: return "sqlite" if "postgres" in vendor or vendor == "psycopg2": return "postgresql" return vendor def _wrap_create_async_engine( tracer, connections_usage, enable_commenter=False, commenter_options=None, enable_attribute_commenter=False, ): # pylint: disable=unused-argument def _wrap_create_async_engine_internal(func, module, args, kwargs): """Trace the SQLAlchemy engine, creating an `EngineTracer` object that will listen to SQLAlchemy events. """ if not is_instrumentation_enabled(): return func(*args, **kwargs) engine = func(*args, **kwargs) EngineTracer( tracer, engine.sync_engine, connections_usage, enable_commenter, commenter_options, enable_attribute_commenter, ) return engine return _wrap_create_async_engine_internal def _wrap_create_engine( tracer, connections_usage, enable_commenter=False, commenter_options=None, enable_attribute_commenter=False, ): def _wrap_create_engine_internal(func, _module, args, kwargs): """Trace the SQLAlchemy engine, creating an `EngineTracer` object that will listen to SQLAlchemy events. """ if not is_instrumentation_enabled(): return func(*args, **kwargs) engine = func(*args, **kwargs) EngineTracer( tracer, engine, connections_usage, enable_commenter, commenter_options, enable_attribute_commenter, ) return engine return _wrap_create_engine_internal def _wrap_connect(tracer): # pylint: disable=unused-argument def _wrap_connect_internal(func, module, args, kwargs): if not is_instrumentation_enabled(): return func(*args, **kwargs) # Initialize semantic conventions opt-in if needed _OpenTelemetrySemanticConventionStability._initialize() sem_conv_opt_in_mode_db = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.DATABASE, ) sem_conv_opt_in_mode_http = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) with tracer.start_as_current_span("connect", kind=trace.SpanKind.CLIENT) as span: if span.is_recording(): attrs, _ = _get_attributes_from_url( module.url, sem_conv_opt_in_mode_db, sem_conv_opt_in_mode_http, ) _set_db_system( attrs, _normalize_vendor(module.name), sem_conv_opt_in_mode_db, ) span.set_attributes(attrs) return func(*args, **kwargs) return _wrap_connect_internal class EngineTracer: _remove_event_listener_params = [] def __init__( self, tracer, engine, connections_usage, enable_commenter=False, commenter_options=None, enable_attribute_commenter=False, ): # Initialize semantic conventions opt-in if needed _OpenTelemetrySemanticConventionStability._initialize() self._sem_conv_opt_in_mode_db = ( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.DATABASE, ) ) self._sem_conv_opt_in_mode_http = ( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) ) self.tracer = tracer self.connections_usage = connections_usage self.vendor = _normalize_vendor(engine.name) self.enable_commenter = enable_commenter self.commenter_options = commenter_options if commenter_options else {} self.enable_attribute_commenter = enable_attribute_commenter self._engine_attrs = _get_attributes_from_engine(engine) self._leading_comment_remover = re.compile(r"^/\*.*?\*/") self._register_event_listener(engine, "before_cursor_execute", self._before_cur_exec, retval=True) self._register_event_listener(engine, "after_cursor_execute", _after_cur_exec) self._register_event_listener(engine, "handle_error", _handle_error) self._register_event_listener(engine, "connect", self._pool_connect) self._register_event_listener(engine, "close", self._pool_close) self._register_event_listener(engine, "checkin", self._pool_checkin) self._register_event_listener(engine, "checkout", self._pool_checkout) def _add_idle_to_connection_usage(self, value): if not is_instrumentation_enabled(): return self.connections_usage.add( value, attributes={ **self._engine_attrs, "state": "idle", }, ) def _add_used_to_connection_usage(self, value): if not is_instrumentation_enabled(): return self.connections_usage.add( value, attributes={ **self._engine_attrs, "state": "used", }, ) def _pool_connect(self, _dbapi_connection, _connection_record): self._add_idle_to_connection_usage(1) def _pool_close(self, _dbapi_connection, _connection_record): self._add_idle_to_connection_usage(-1) # Called when a connection returns to the pool. def _pool_checkin(self, _dbapi_connection, _connection_record): self._add_used_to_connection_usage(-1) self._add_idle_to_connection_usage(1) # Called when a connection is retrieved from the Pool. def _pool_checkout(self, _dbapi_connection, _connection_record, _connection_proxy): self._add_idle_to_connection_usage(-1) self._add_used_to_connection_usage(1) @classmethod def _dispose_of_event_listener(cls, obj): try: cls._remove_event_listener_params.remove(obj) except ValueError: pass @classmethod def _register_event_listener(cls, target, identifier, func, *args, **kw): listen(target, identifier, func, *args, **kw) cls._remove_event_listener_params.append((weakref.ref(target), identifier, func)) weakref.finalize( target, cls._dispose_of_event_listener, (weakref.ref(target), identifier, func), ) @classmethod def remove_all_event_listeners(cls): for ( weak_ref_target, identifier, func, ) in cls._remove_event_listener_params: # Remove an event listener only if saved weak reference points to an object # which has not been garbage collected if weak_ref_target() is not None: remove(weak_ref_target(), identifier, func) cls._remove_event_listener_params.clear() def _operation_name(self, db_name, statement): parts = [] if isinstance(statement, str): # otel spec recommends against parsing SQL queries. We are not trying to parse SQL # but simply truncating the statement to the first word. This covers probably >95% # use cases and uses the SQL statement in span name correctly as per the spec. # For some very special cases it might not record the correct statement if the SQL # dialect is too weird but in any case it shouldn't break anything. # Strip leading comments so we get the operation name. A statement that # is truthy but has no tokens left (comment-only or whitespace-only) must # not raise IndexError; skip adding an operation part in that case. tokens = self._leading_comment_remover.sub("", statement).split() if tokens: parts.append(tokens[0]) if db_name: parts.append(db_name) if not parts: return self.vendor return " ".join(parts) def _get_commenter_data(self, conn) -> dict: """Calculate sqlcomment contents from conn and configured options""" commenter_data = { "db_driver": conn.engine.driver, # Driver/framework centric information. "db_framework": f"sqlalchemy:{sqlalchemy.__version__}", } if self.commenter_options.get("opentelemetry_values", True): commenter_data.update(**_get_opentelemetry_values()) # Filter down to just the requested attributes. commenter_data = {k: v for k, v in commenter_data.items() if self.commenter_options.get(k, True)} return commenter_data def _set_db_client_span_attributes(self, span, statement, db_name, attrs) -> None: """Uses statement, db_name, and attrs to set attributes of provided Otel span""" span_attrs = dict(attrs) _set_db_statement(span_attrs, statement, self._sem_conv_opt_in_mode_db) _set_db_system(span_attrs, self.vendor, self._sem_conv_opt_in_mode_db) _set_db_operation( span_attrs, self._operation_name(db_name, statement), self._sem_conv_opt_in_mode_db, ) for key, value in span_attrs.items(): span.set_attribute(key, value) def _before_cur_exec(self, conn, cursor, statement, params, context, _executemany): if not is_instrumentation_enabled(): return statement, params attrs, found = _get_attributes_from_url( conn.engine.url, self._sem_conv_opt_in_mode_db, self._sem_conv_opt_in_mode_http, ) if not found: attrs = _get_attributes_from_cursor_or_conn( self.vendor, conn, cursor, attrs, self._sem_conv_opt_in_mode_db, self._sem_conv_opt_in_mode_http, ) # Extract db_name for operation name db_name = _get_db_name_from_cursor_or_conn(self.vendor, conn, cursor) span = self.tracer.start_span( self._operation_name(db_name, statement), kind=trace.SpanKind.CLIENT, ) with trace.use_span(span, end_on_exit=False): if span.is_recording(): if self.enable_commenter: commenter_data = self._get_commenter_data(conn) if self.enable_attribute_commenter: # just to handle type safety statement = str(statement) # sqlcomment is added to executed query and db.statement and/or db.query.text span attribute statement = _add_sql_comment(statement, **commenter_data) self._set_db_client_span_attributes(span, statement, db_name, attrs) else: # sqlcomment is only added to executed query # so db.statement and/or db.query.text is set before add_sql_comment self._set_db_client_span_attributes(span, statement, db_name, attrs) statement = _add_sql_comment(statement, **commenter_data) else: # no sqlcomment anywhere self._set_db_client_span_attributes(span, statement, db_name, attrs) context._otel_span = span return statement, params # pylint: disable=unused-argument def _after_cur_exec(conn, cursor, statement, params, context, executemany): span = getattr(context, "_otel_span", None) if span is None: return span.end() def _handle_error(context): span = getattr(context.execution_context, "_otel_span", None) if span is None: return if span.is_recording(): span.set_status( Status( StatusCode.ERROR, str(context.original_exception), ) ) span.end() def _get_attributes_from_url(url, sem_conv_opt_in_mode_db, sem_conv_opt_in_mode_http): """Set connection tags from the url. return true if successful.""" attrs = {} if url.host: _set_http_net_peer_name_client(attrs, url.host, sem_conv_opt_in_mode_http) if url.port: _set_http_peer_port_client(attrs, url.port, sem_conv_opt_in_mode_http) if url.database: _set_db_name(attrs, url.database, sem_conv_opt_in_mode_db) if url.username: _set_db_user(attrs, url.username, sem_conv_opt_in_mode_db) return attrs, bool(url.host) def _get_attributes_from_cursor_or_conn( vendor, conn, cursor, attrs, sem_conv_opt_in_mode_db, sem_conv_opt_in_mode_http, ): """Attempt to set db connection attributes by introspecting the cursor.""" if vendor == "postgresql": info = getattr(getattr(cursor, "connection", None), "info", None) if not info: return attrs db_name = _get_db_name_from_cursor_or_conn(vendor, conn, cursor) _set_db_name(attrs, db_name, sem_conv_opt_in_mode_db) is_unix_socket = info.host and info.host.startswith("/") if is_unix_socket: attrs[NET_TRANSPORT] = NetTransportValues.OTHER.value if info.port: # postgresql enforces this pattern on all socket names _set_http_net_peer_name_client( attrs, os.path.join(info.host, f".s.PGSQL.{info.port}"), sem_conv_opt_in_mode_http, ) else: attrs[NET_TRANSPORT] = NetTransportValues.IP_TCP.value _set_http_net_peer_name_client(attrs, info.host, sem_conv_opt_in_mode_http) if info.port: _set_http_peer_port_client(attrs, int(info.port), sem_conv_opt_in_mode_http) elif vendor == "sqlite": db_name = _get_db_name_from_cursor_or_conn(vendor, conn, cursor) _set_db_name(attrs, db_name, sem_conv_opt_in_mode_db) # SQLite has no network attributes return attrs def _get_connection_string(engine): drivername = engine.url.drivername or "" host = engine.url.host or "" port = engine.url.port or "" database = engine.url.database or "" return f"{drivername}://{host}:{port}/{database}" def _get_attributes_from_engine(engine): """Set metadata attributes of the database engine""" attrs = {} attrs["pool.name"] = getattr(getattr(engine, "pool", None), "logging_name", None) or _get_connection_string(engine) return attrs package.py000066400000000000000000000002671527070072300462700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("sqlalchemy >= 1.0.0, < 2.1.0",) _supports_metrics = True _semconv_status = "migration" version.py000066400000000000000000000001441527070072300463540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements-0.txt000066400000000000000000000004601527070072300376240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemyasgiref==3.8.1 cffi==1.17.0 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 SQLAlchemy==1.1.18 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-sqlalchemy test-requirements-1.txt000066400000000000000000000005051527070072300376250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemyaiosqlite==0.20.0 asgiref==3.8.1 Deprecated==1.2.14 greenlet==3.1.1 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 SQLAlchemy==1.4.51 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-sqlalchemy test-requirements-2.txt000066400000000000000000000005361527070072300376320ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemyaiosqlite==0.20.0 asgiref==3.8.1 Deprecated==1.2.14 greenlet==3.1.1 ; python_version < "3.10" greenlet==3.3.1 ; python_version >= "3.10" iniconfig==2.0.0 packaging==24.1 pluggy==1.6.0 pytest==7.4.4 SQLAlchemy==2.0.36 typing_extensions==4.12.2 wrapt==1.16.0 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-sqlalchemy opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/000077500000000000000000000000001527070072300344475ustar00rootroot00000000000000__init__.py000066400000000000000000000001141527070072300364750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 test_sqlalchemy.py000066400000000000000000001321471527070072300401530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio import logging from unittest import mock import pytest import sqlalchemy from sqlalchemy import ( create_engine, text, ) from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.sqlalchemy import ( EngineTracer, SQLAlchemyInstrumentor, ) from opentelemetry.instrumentation.sqlalchemy.engine import ( _get_db_name_from_cursor_or_conn, ) from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider, export from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_OPERATION, DB_STATEMENT, DB_SYSTEM, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_NAMESPACE, DB_OPERATION_NAME, DB_QUERY_TEXT, DB_SYSTEM_NAME, ) from opentelemetry.test.test_base import TestBase class TestSqlalchemyInstrumentation(TestBase): @pytest.fixture(autouse=True) def inject_fixtures(self, caplog): self.caplog = caplog def setUp(self): super().setUp() # Reset semconv state before each test for reproducibility # Tests using @mock.patch.dict will re-initialize again after the mock is applied _OpenTelemetrySemanticConventionStability._initialized = False def tearDown(self): super().tearDown() SQLAlchemyInstrumentor().uninstrument() def test_operation_name_comment_or_whitespace_only(self): # Regression: a comment-only or whitespace-only statement is truthy but has # no tokens after leading-comment stripping; executing it must not raise # IndexError and the resulting span must not crash on naming either. engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, ) cnx = engine.connect() try: cnx.execute(text("/* comment only */")) cnx.execute(text(" ")) except Exception as exception: # pylint: disable=broad-exception-caught self.fail(f"An unexpected exception was raised {exception}") spans = self.memory_exporter.get_finished_spans() # connect + 2 executes self.assertEqual(len(spans), 3) for span in spans[1:]: # No operation token survives comment/whitespace stripping, so the # span name falls back to just the db name. self.assertEqual(span.name, ":memory:") def test_trace_integration(self): engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, ) cnx = engine.connect() cnx.execute(text("SELECT 1 + 1;")).fetchall() cnx.execute(text("/* leading comment */ SELECT 1 + 1;")).fetchall() cnx.execute(text("/* leading comment */ SELECT 1 + 1; /* trailing comment */")).fetchall() cnx.execute(text("SELECT 1 + 1; /* trailing comment */")).fetchall() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 5) # first span - the connection to the db self.assertEqual(spans[0].name, "connect") self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT) # second span - the query itself self.assertEqual(spans[1].name, "SELECT :memory:") self.assertEqual(spans[1].kind, trace.SpanKind.CLIENT) # spans for queries with comments self.assertEqual(spans[2].name, "SELECT :memory:") self.assertEqual(spans[2].kind, trace.SpanKind.CLIENT) self.assertEqual(spans[3].name, "SELECT :memory:") self.assertEqual(spans[3].kind, trace.SpanKind.CLIENT) self.assertEqual(spans[4].name, "SELECT :memory:") self.assertEqual(spans[4].kind, trace.SpanKind.CLIENT) def test_instrument_two_engines(self): engine_1 = create_engine("sqlite:///:memory:") engine_2 = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engines=[engine_1, engine_2], tracer_provider=self.tracer_provider, ) cnx_1 = engine_1.connect() cnx_1.execute(text("SELECT 1 + 1;")).fetchall() cnx_2 = engine_2.connect() cnx_2.execute(text("SELECT 1 + 1;")).fetchall() spans = self.memory_exporter.get_finished_spans() # 2 queries + 2 engine connect self.assertEqual(len(spans), 4) def test_instrument_engine_connect(self): engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, ) engine.connect() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) @pytest.mark.skipif( not sqlalchemy.__version__.startswith("1.4"), reason="only run async tests for 1.4", ) def test_async_trace_integration(self): async def run(): from sqlalchemy.ext.asyncio import ( # pylint: disable-all # noqa: PLC0415 create_async_engine, ) engine = create_async_engine("sqlite+aiosqlite:///:memory:") SQLAlchemyInstrumentor().instrument(engine=engine.sync_engine, tracer_provider=self.tracer_provider) async with engine.connect() as cnx: await cnx.execute(text("SELECT 1 + 1;")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span - the connection to the db self.assertEqual(spans[0].name, "connect") self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT) # second span - the query self.assertEqual(spans[1].name, "SELECT :memory:") self.assertEqual(spans[1].kind, trace.SpanKind.CLIENT) self.assertEqual( spans[1].instrumentation_scope.name, "opentelemetry.instrumentation.sqlalchemy", ) asyncio.get_event_loop().run_until_complete(run()) def test_not_recording(self): mock_tracer = mock.Mock() mock_span = mock.Mock() mock_context = mock.Mock() mock_span.is_recording.return_value = False mock_context.__enter__ = mock.Mock(return_value=mock_span) mock_context.__exit__ = mock.Mock(return_value=None) mock_tracer.start_span.return_value = mock_context mock_tracer.start_as_current_span.return_value = mock_context with mock.patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, ) cnx = engine.connect() cnx.execute(text("SELECT 1 + 1;")).fetchall() self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) def test_create_engine_wrapper(self): SQLAlchemyInstrumentor().instrument() from sqlalchemy import ( # noqa: PLC0415 create_engine, # pylint: disable-all ) engine = create_engine("sqlite:///:memory:") cnx = engine.connect() cnx.execute(text("SELECT 1 + 1;")).fetchall() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span - the connection to the db self.assertEqual(spans[0].name, "connect") self.assertEqual(spans[0].attributes[DB_NAME], ":memory:") self.assertEqual(spans[0].attributes[DB_SYSTEM], "sqlite") self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT) # second span - the query self.assertEqual(spans[1].name, "SELECT :memory:") self.assertEqual(spans[1].kind, trace.SpanKind.CLIENT) self.assertEqual( spans[1].instrumentation_scope.name, "opentelemetry.instrumentation.sqlalchemy", ) def test_instrument_engine_from_config(self): SQLAlchemyInstrumentor().instrument() from sqlalchemy import ( # noqa: PLC0415 engine_from_config, # pylint: disable-all ) engine = engine_from_config({"sqlalchemy.url": "sqlite:///:memory:"}) cnx = engine.connect() cnx.execute(text("SELECT 1 + 1;")).fetchall() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) def test_create_engine_wrapper_enable_commenter(self): logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) SQLAlchemyInstrumentor().instrument( enable_commenter=True, commenter_options={"db_framework": False}, ) from sqlalchemy import ( # noqa: PLC0415 create_engine, # pylint: disable-all ) engine = create_engine("sqlite:///:memory:") cnx = engine.connect() cnx.execute(text("SELECT 1;")).fetchall() # sqlcommenter self.assertRegex( self.caplog.records[-2].getMessage(), r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span is connection to db self.assertEqual(spans[0].name, "connect") # second span is query itself query_span = spans[1] self.assertEqual( query_span.attributes[DB_STATEMENT], "SELECT 1;", ) def test_create_engine_wrapper_enable_commenter_stmt_enabled(self): logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) SQLAlchemyInstrumentor().instrument( enable_commenter=True, commenter_options={"db_framework": False}, enable_attribute_commenter=True, ) from sqlalchemy import ( # noqa: PLC0415 create_engine, # pylint: disable-all ) engine = create_engine("sqlite:///:memory:") cnx = engine.connect() cnx.execute(text("SELECT 1;")).fetchall() # sqlcommenter self.assertRegex( self.caplog.records[-2].getMessage(), r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span is connection to db self.assertEqual(spans[0].name, "connect") # second span is query itself query_span = spans[1] self.assertRegex( query_span.attributes[DB_STATEMENT], r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) def test_create_engine_wrapper_enable_commenter_otel_values_false(self): logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) SQLAlchemyInstrumentor().instrument( enable_commenter=True, commenter_options={ "db_framework": False, "opentelemetry_values": False, }, ) from sqlalchemy import ( # noqa: PLC0415 create_engine, # pylint: disable-all ) engine = create_engine("sqlite:///:memory:") cnx = engine.connect() cnx.execute(text("SELECT 1;")).fetchall() # sqlcommenter self.assertRegex( self.caplog.records[-2].getMessage(), r"SELECT 1 /\*db_driver='(.*)'\*/;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span is connection to db self.assertEqual(spans[0].name, "connect") # second span is query itself query_span = spans[1] self.assertEqual( query_span.attributes[DB_STATEMENT], "SELECT 1;", ) def test_create_engine_wrapper_enable_commenter_stmt_enabled_otel_values_false( self, ): logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) SQLAlchemyInstrumentor().instrument( enable_commenter=True, commenter_options={ "db_framework": False, "opentelemetry_values": False, }, enable_attribute_commenter=True, ) from sqlalchemy import ( # noqa: PLC0415 create_engine, # pylint: disable-all ) engine = create_engine("sqlite:///:memory:") cnx = engine.connect() cnx.execute(text("SELECT 1;")).fetchall() # sqlcommenter self.assertRegex( self.caplog.records[-2].getMessage(), r"SELECT 1 /\*db_driver='(.*)'\*/;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span is connection to db self.assertEqual(spans[0].name, "connect") # second span is query itself query_span = spans[1] self.assertRegex( query_span.attributes[DB_STATEMENT], r"SELECT 1 /\*db_driver='(.*)'\*/;", ) def test_custom_tracer_provider(self): provider = TracerProvider( resource=Resource.create( { "service.name": "test", "deployment.environment": "env", "service.version": "1234", }, ), ) provider.add_span_processor(export.SimpleSpanProcessor(self.memory_exporter)) SQLAlchemyInstrumentor().instrument(tracer_provider=provider) from sqlalchemy import ( # noqa: PLC0415 create_engine, # pylint: disable-all ) engine = create_engine("sqlite:///:memory:") cnx = engine.connect() cnx.execute(text("SELECT 1 + 1;")).fetchall() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) self.assertEqual(spans[0].resource.attributes["service.name"], "test") self.assertEqual(spans[0].resource.attributes["deployment.environment"], "env") self.assertEqual(spans[0].resource.attributes["service.version"], "1234") @pytest.mark.skipif( not sqlalchemy.__version__.startswith("1.4"), reason="only run async tests for 1.4", ) def test_create_async_engine_wrapper(self): async def run(): SQLAlchemyInstrumentor().instrument() from sqlalchemy.ext.asyncio import ( # pylint: disable-all # noqa: PLC0415 create_async_engine, ) engine = create_async_engine("sqlite+aiosqlite:///:memory:") async with engine.connect() as cnx: await cnx.execute(text("SELECT 1 + 1;")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span - the connection to the db self.assertEqual(spans[0].name, "connect") self.assertEqual(spans[0].attributes[DB_NAME], ":memory:") self.assertEqual(spans[0].attributes[DB_SYSTEM], "sqlite") self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT) # second span - the query self.assertEqual(spans[1].name, "SELECT :memory:") self.assertEqual(spans[1].kind, trace.SpanKind.CLIENT) self.assertEqual( spans[1].instrumentation_scope.name, "opentelemetry.instrumentation.sqlalchemy", ) asyncio.get_event_loop().run_until_complete(run()) @pytest.mark.skipif( not sqlalchemy.__version__.startswith("1.4"), reason="only run async tests for 1.4", ) def test_create_async_engine_wrapper_enable_commenter(self): async def run(): logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) SQLAlchemyInstrumentor().instrument( enable_commenter=True, commenter_options={ "db_framework": False, }, ) from sqlalchemy.ext.asyncio import ( # pylint: disable-all # noqa: PLC0415 create_async_engine, ) engine = create_async_engine("sqlite+aiosqlite:///:memory:") async with engine.connect() as cnx: await cnx.execute(text("SELECT 1;")) # sqlcommenter self.assertRegex( self.caplog.records[1].getMessage(), r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span is connection to db self.assertEqual(spans[0].name, "connect") # second span is query itself query_span = spans[1] self.assertEqual( query_span.attributes[DB_STATEMENT], "SELECT 1;", ) asyncio.get_event_loop().run_until_complete(run()) @pytest.mark.skipif( not sqlalchemy.__version__.startswith("1.4"), reason="only run async tests for 1.4", ) def test_create_async_engine_wrapper_enable_commenter_stmt_enabled(self): async def run(): logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) SQLAlchemyInstrumentor().instrument( enable_commenter=True, commenter_options={ "db_framework": False, }, enable_attribute_commenter=True, ) from sqlalchemy.ext.asyncio import ( # pylint: disable-all # noqa: PLC0415 create_async_engine, ) engine = create_async_engine("sqlite+aiosqlite:///:memory:") async with engine.connect() as cnx: await cnx.execute(text("SELECT 1;")) # sqlcommenter self.assertRegex( self.caplog.records[1].getMessage(), r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span is connection to db self.assertEqual(spans[0].name, "connect") # second span is query itself query_span = spans[1] self.assertRegex( query_span.attributes[DB_STATEMENT], r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) asyncio.get_event_loop().run_until_complete(run()) @pytest.mark.skipif( not sqlalchemy.__version__.startswith("1.4"), reason="only run async tests for 1.4", ) def test_create_async_engine_wrapper_enable_commenter_otel_values_false( self, ): async def run(): logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) SQLAlchemyInstrumentor().instrument( enable_commenter=True, commenter_options={ "db_framework": False, "opentelemetry_values": False, }, ) from sqlalchemy.ext.asyncio import ( # pylint: disable-all # noqa: PLC0415 create_async_engine, ) engine = create_async_engine("sqlite+aiosqlite:///:memory:") async with engine.connect() as cnx: await cnx.execute(text("SELECT 1;")) # sqlcommenter self.assertRegex( self.caplog.records[1].getMessage(), r"SELECT 1 /\*db_driver='(.*)'\*/;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span is connection to db self.assertEqual(spans[0].name, "connect") # second span is query itself query_span = spans[1] self.assertEqual( query_span.attributes[DB_STATEMENT], "SELECT 1;", ) asyncio.get_event_loop().run_until_complete(run()) @pytest.mark.skipif( not sqlalchemy.__version__.startswith("1.4"), reason="only run async tests for 1.4", ) def test_create_async_engine_wrapper_enable_commenter_stmt_enabled_otel_values_false( self, ): async def run(): logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) SQLAlchemyInstrumentor().instrument( enable_commenter=True, commenter_options={ "db_framework": False, "opentelemetry_values": False, }, enable_attribute_commenter=True, ) from sqlalchemy.ext.asyncio import ( # pylint: disable-all # noqa: PLC0415 create_async_engine, ) engine = create_async_engine("sqlite+aiosqlite:///:memory:") async with engine.connect() as cnx: await cnx.execute(text("SELECT 1;")) # sqlcommenter self.assertRegex( self.caplog.records[1].getMessage(), r"SELECT 1 /\*db_driver='(.*)'\*/;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span is connection to db self.assertEqual(spans[0].name, "connect") # second span is query itself query_span = spans[1] self.assertRegex( query_span.attributes[DB_STATEMENT], r"SELECT 1 /\*db_driver='(.*)'*/;", ) asyncio.get_event_loop().run_until_complete(run()) def test_uninstrument(self): engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, ) cnx = engine.connect() cnx.execute(text("SELECT 1 + 1;")).fetchall() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span - the connection to the db self.assertEqual(spans[0].name, "connect") self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT) # second span - the query itself self.assertEqual(spans[1].name, "SELECT :memory:") self.assertEqual(spans[1].kind, trace.SpanKind.CLIENT) self.memory_exporter.clear() SQLAlchemyInstrumentor().uninstrument() cnx.execute(text("SELECT 1 + 1;")).fetchall() engine2 = create_engine("sqlite:///:memory:") cnx2 = engine2.connect() cnx2.execute(text("SELECT 2 + 2;")).fetchall() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, ) cnx = engine.connect() cnx.execute(text("SELECT 1 + 1;")).fetchall() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) def test_uninstrument_without_engine(self): SQLAlchemyInstrumentor().instrument(tracer_provider=self.tracer_provider) from sqlalchemy import create_engine # noqa: PLC0415 engine = create_engine("sqlite:///:memory:") cnx = engine.connect() cnx.execute(text("SELECT 1 + 1;")).fetchall() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) self.memory_exporter.clear() SQLAlchemyInstrumentor().uninstrument() cnx.execute(text("SELECT 1 + 1;")).fetchall() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_no_op_tracer_provider(self): engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=trace.NoOpTracerProvider(), ) cnx = engine.connect() cnx.execute(text("SELECT 1 + 1;")).fetchall() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_no_memory_leakage_if_engine_diposed(self): SQLAlchemyInstrumentor().instrument() import gc # noqa: PLC0415 import weakref # noqa: PLC0415 from sqlalchemy import create_engine # noqa: PLC0415 from opentelemetry.instrumentation.sqlalchemy.engine import ( # noqa: PLC0415 EngineTracer, ) callback = mock.Mock() def make_shortlived_engine(): engine = create_engine("sqlite:///:memory:") # Callback will be called if engine is deallocated during garbage # collection weakref.finalize(engine, callback) with engine.connect() as conn: conn.execute(text("SELECT 1 + 1;")).fetchall() for _ in range(5): make_shortlived_engine() gc.collect() assert callback.call_count == 5 assert len(EngineTracer._remove_event_listener_params) == 0 def test_suppress_instrumentation_create_engine(self): SQLAlchemyInstrumentor().instrument() from sqlalchemy import create_engine # noqa: PLC0415 with suppress_instrumentation(): engine = create_engine("sqlite:///:memory:") self.assertTrue(not isinstance(engine, EngineTracer)) @pytest.mark.skipif( not sqlalchemy.__version__.startswith("1.4"), reason="only run async tests for 1.4", ) def test_suppress_instrumentation_create_async_engine(self): async def run(): SQLAlchemyInstrumentor().instrument() from sqlalchemy.ext.asyncio import ( # pylint: disable-all # noqa: PLC0415 create_async_engine, ) with suppress_instrumentation(): engine = create_async_engine("sqlite+aiosqlite:///:memory:") self.assertTrue(not isinstance(engine, EngineTracer)) asyncio.get_event_loop().run_until_complete(run()) def test_suppress_instrumentation_connect(self): engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, ) with suppress_instrumentation(): with engine.connect(): pass spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) def test_semconv_default_mode(self): SQLAlchemyInstrumentor().uninstrument() engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, ) cnx = engine.connect() cnx.execute(text("SELECT 1 + 1;")).fetchall() spans = self.memory_exporter.get_finished_spans() connect_span = spans[0] self.assertEqual( connect_span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.11.0", ) self.assertIn(DB_SYSTEM, connect_span.attributes) self.assertEqual(connect_span.attributes[DB_SYSTEM], "sqlite") self.assertIn(DB_NAME, connect_span.attributes) self.assertEqual(connect_span.attributes[DB_NAME], ":memory:") # Verify new conventions are NOT present self.assertNotIn(DB_SYSTEM_NAME, connect_span.attributes) self.assertNotIn(DB_NAMESPACE, connect_span.attributes) query_span = spans[1] self.assertIn(DB_STATEMENT, query_span.attributes) self.assertIn(DB_SYSTEM, query_span.attributes) self.assertEqual(query_span.attributes[DB_SYSTEM], "sqlite") self.assertIn(DB_OPERATION, query_span.attributes) self.assertEqual(query_span.attributes[DB_OPERATION], "SELECT :memory:") # Verify new conventions are NOT present self.assertNotIn(DB_QUERY_TEXT, query_span.attributes) self.assertNotIn(DB_SYSTEM_NAME, query_span.attributes) @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "http"}) def test_semconv_http_mode(self): SQLAlchemyInstrumentor().uninstrument() _OpenTelemetrySemanticConventionStability._initialized = False _OpenTelemetrySemanticConventionStability._initialize() engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, ) cnx = engine.connect() cnx.execute(text("SELECT 1 + 1;")).fetchall() spans = self.memory_exporter.get_finished_spans() connect_span = spans[0] self.assertEqual( connect_span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.21.0", ) # HTTP attributes should use new semconv self.assertNotIn(NET_PEER_NAME, connect_span.attributes) # DB attributes should still use old semconv self.assertIn(DB_SYSTEM, connect_span.attributes) self.assertEqual(connect_span.attributes[DB_SYSTEM], "sqlite") self.assertIn(DB_NAME, connect_span.attributes) self.assertEqual(connect_span.attributes[DB_NAME], ":memory:") # Verify new DB conventions are NOT present self.assertNotIn(DB_SYSTEM_NAME, connect_span.attributes) self.assertNotIn(DB_NAMESPACE, connect_span.attributes) query_span = spans[1] # DB attributes should still use old semconv self.assertIn(DB_STATEMENT, query_span.attributes) self.assertIn(DB_SYSTEM, query_span.attributes) self.assertEqual(query_span.attributes[DB_SYSTEM], "sqlite") # Verify new DB conventions are NOT present self.assertNotIn(DB_QUERY_TEXT, query_span.attributes) self.assertNotIn(DB_SYSTEM_NAME, query_span.attributes) @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "database"}) def test_semconv_database_mode(self): SQLAlchemyInstrumentor().uninstrument() _OpenTelemetrySemanticConventionStability._initialized = False _OpenTelemetrySemanticConventionStability._initialize() engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, ) cnx = engine.connect() cnx.execute(text("SELECT 1 + 1;")).fetchall() spans = self.memory_exporter.get_finished_spans() connect_span = spans[0] self.assertEqual( connect_span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.25.0", ) self.assertIn(DB_SYSTEM_NAME, connect_span.attributes) self.assertEqual(connect_span.attributes[DB_SYSTEM_NAME], "sqlite") self.assertIn(DB_NAMESPACE, connect_span.attributes) self.assertEqual(connect_span.attributes[DB_NAMESPACE], ":memory:") # Verify old conventions are NOT present self.assertNotIn(DB_SYSTEM, connect_span.attributes) self.assertNotIn(DB_NAME, connect_span.attributes) query_span = spans[1] self.assertIn(DB_QUERY_TEXT, query_span.attributes) self.assertIn(DB_SYSTEM_NAME, query_span.attributes) self.assertEqual(query_span.attributes[DB_SYSTEM_NAME], "sqlite") self.assertIn(DB_OPERATION_NAME, query_span.attributes) self.assertEqual(query_span.attributes[DB_OPERATION_NAME], "SELECT :memory:") # Verify old conventions are NOT present self.assertNotIn(DB_STATEMENT, query_span.attributes) self.assertNotIn(DB_SYSTEM, query_span.attributes) @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "http,database"}) def test_semconv_http_and_database_mode(self): SQLAlchemyInstrumentor().uninstrument() _OpenTelemetrySemanticConventionStability._initialized = False _OpenTelemetrySemanticConventionStability._initialize() engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, ) cnx = engine.connect() cnx.execute(text("SELECT 1 + 1;")).fetchall() spans = self.memory_exporter.get_finished_spans() connect_span = spans[0] self.assertEqual( connect_span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.25.0", ) # New DB conventions should be present self.assertIn(DB_SYSTEM_NAME, connect_span.attributes) self.assertEqual(connect_span.attributes[DB_SYSTEM_NAME], "sqlite") self.assertIn(DB_NAMESPACE, connect_span.attributes) self.assertEqual(connect_span.attributes[DB_NAMESPACE], ":memory:") # Old DB conventions should NOT be present self.assertNotIn(DB_SYSTEM, connect_span.attributes) self.assertNotIn(DB_NAME, connect_span.attributes) query_span = spans[1] # New DB conventions should be present self.assertIn(DB_QUERY_TEXT, query_span.attributes) self.assertIn(DB_SYSTEM_NAME, query_span.attributes) self.assertEqual(query_span.attributes[DB_SYSTEM_NAME], "sqlite") # Old DB conventions should NOT be present self.assertNotIn(DB_STATEMENT, query_span.attributes) self.assertNotIn(DB_SYSTEM, query_span.attributes) @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "http/dup"}) def test_semconv_http_dup_mode(self): SQLAlchemyInstrumentor().uninstrument() _OpenTelemetrySemanticConventionStability._initialized = False _OpenTelemetrySemanticConventionStability._initialize() engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, ) cnx = engine.connect() cnx.execute(text("SELECT 1 + 1;")).fetchall() spans = self.memory_exporter.get_finished_spans() connect_span = spans[0] self.assertEqual( connect_span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.21.0", ) # DB attributes should use old semconv only self.assertIn(DB_SYSTEM, connect_span.attributes) self.assertEqual(connect_span.attributes[DB_SYSTEM], "sqlite") self.assertIn(DB_NAME, connect_span.attributes) self.assertEqual(connect_span.attributes[DB_NAME], ":memory:") # Verify new DB conventions are NOT present self.assertNotIn(DB_SYSTEM_NAME, connect_span.attributes) self.assertNotIn(DB_NAMESPACE, connect_span.attributes) query_span = spans[1] # DB attributes should use old semconv self.assertIn(DB_STATEMENT, query_span.attributes) self.assertIn(DB_SYSTEM, query_span.attributes) self.assertEqual(query_span.attributes[DB_SYSTEM], "sqlite") # Verify new DB conventions are NOT present self.assertNotIn(DB_QUERY_TEXT, query_span.attributes) self.assertNotIn(DB_SYSTEM_NAME, query_span.attributes) @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "database/dup"}) def test_semconv_database_dup_mode(self): SQLAlchemyInstrumentor().uninstrument() engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, ) cnx = engine.connect() cnx.execute(text("SELECT 1 + 1;")).fetchall() spans = self.memory_exporter.get_finished_spans() connect_span = spans[0] self.assertEqual( connect_span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.25.0", ) # Old conventions self.assertIn(DB_SYSTEM, connect_span.attributes) self.assertEqual(connect_span.attributes[DB_SYSTEM], "sqlite") self.assertIn(DB_NAME, connect_span.attributes) self.assertEqual(connect_span.attributes[DB_NAME], ":memory:") # New conventions self.assertIn(DB_SYSTEM_NAME, connect_span.attributes) self.assertEqual(connect_span.attributes[DB_SYSTEM_NAME], "sqlite") self.assertIn(DB_NAMESPACE, connect_span.attributes) self.assertEqual(connect_span.attributes[DB_NAMESPACE], ":memory:") query_span = spans[1] # Old conventions self.assertIn(DB_STATEMENT, query_span.attributes) self.assertIn(DB_SYSTEM, query_span.attributes) self.assertEqual(query_span.attributes[DB_SYSTEM], "sqlite") # New conventions self.assertIn(DB_QUERY_TEXT, query_span.attributes) self.assertIn(DB_SYSTEM_NAME, query_span.attributes) self.assertEqual(query_span.attributes[DB_SYSTEM_NAME], "sqlite") @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "http/dup,database/dup"}) def test_semconv_http_and_database_dup_mode(self): SQLAlchemyInstrumentor().uninstrument() _OpenTelemetrySemanticConventionStability._initialized = False _OpenTelemetrySemanticConventionStability._initialize() engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, ) cnx = engine.connect() cnx.execute(text("SELECT 1 + 1;")).fetchall() spans = self.memory_exporter.get_finished_spans() connect_span = spans[0] self.assertEqual( connect_span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.25.0", ) # Both old and new DB conventions self.assertIn(DB_SYSTEM, connect_span.attributes) self.assertEqual(connect_span.attributes[DB_SYSTEM], "sqlite") self.assertIn(DB_NAME, connect_span.attributes) self.assertEqual(connect_span.attributes[DB_NAME], ":memory:") self.assertIn(DB_SYSTEM_NAME, connect_span.attributes) self.assertEqual(connect_span.attributes[DB_SYSTEM_NAME], "sqlite") self.assertIn(DB_NAMESPACE, connect_span.attributes) self.assertEqual(connect_span.attributes[DB_NAMESPACE], ":memory:") query_span = spans[1] # Both old and new DB conventions self.assertIn(DB_STATEMENT, query_span.attributes) self.assertIn(DB_SYSTEM, query_span.attributes) self.assertEqual(query_span.attributes[DB_SYSTEM], "sqlite") self.assertIn(DB_QUERY_TEXT, query_span.attributes) self.assertIn(DB_SYSTEM_NAME, query_span.attributes) self.assertEqual(query_span.attributes[DB_SYSTEM_NAME], "sqlite") def test_suppress_instrumentation_cursor_and_metric(self): engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, enable_commenter=True, ) with suppress_instrumentation(): with engine.connect() as conn: conn.execute(text("SELECT 1 + 1;")).fetchall() spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) metric_list = self.get_sorted_metrics() self.assertEqual(len(metric_list), 0) def test_get_db_name_from_cursor_or_conn_postgresql_success(self): mock_cursor = mock.Mock() mock_connection = mock.Mock() mock_info = mock.Mock() mock_info.dbname = "test_database" mock_connection.info = mock_info mock_cursor.connection = mock_connection result = _get_db_name_from_cursor_or_conn("postgresql", mock_connection, mock_cursor) self.assertEqual(result, "test_database") def test_get_db_name_from_cursor_or_conn_postgresql_no_connection(self): mock_cursor = mock.Mock() mock_cursor.connection = None mock_connection = mock.Mock() result = _get_db_name_from_cursor_or_conn("postgresql", mock_connection, mock_cursor) self.assertIsNone(result) def test_get_db_name_from_cursor_or_conn_postgresql_no_dbname(self): mock_cursor = mock.Mock() mock_connection = mock.Mock() mock_info = mock.Mock() mock_info.dbname = None mock_connection.info = mock_info mock_cursor.connection = mock_connection result = _get_db_name_from_cursor_or_conn("postgresql", mock_connection, mock_cursor) self.assertIsNone(result) def test_get_db_name_from_cursor_or_conn_unknown_vendor_with_url(self): mock_cursor = mock.Mock() mock_connection = mock.Mock() mock_connection.engine.url.database = "foo" mock_cursor.connection = mock_connection result = _get_db_name_from_cursor_or_conn("unknown_db", mock_connection, mock_cursor) self.assertEqual(result, "foo") def test_get_db_name_from_cursor_or_conn_unknown_vendor_without_url(self): mock_cursor = mock.Mock() mock_connection = mock.Mock() mock_connection.engine.url.database = None mock_cursor.connection = mock_connection result = _get_db_name_from_cursor_or_conn("unknown_db", mock_connection, mock_cursor) self.assertIsNone(result) def test_get_db_name_from_cursor_or_conn_mysql_with_database_attr(self): mock_cursor = mock.Mock() mock_connection = mock.Mock() mock_connection.database = "mysql_test_db" mock_cursor.connection = mock_connection result = _get_db_name_from_cursor_or_conn("mysql", mock_connection, mock_cursor) self.assertEqual(result, "mysql_test_db") def test_get_db_name_from_cursor_or_conn_mysql_with_db_attr_string(self): mock_cursor = mock.Mock() mock_connection = mock.Mock() mock_connection.db = "mysql_test_db" del mock_connection.database # Remove database attribute mock_cursor.connection = mock_connection result = _get_db_name_from_cursor_or_conn("mysql", mock_connection, mock_cursor) self.assertEqual(result, "mysql_test_db") def test_get_db_name_from_cursor_or_conn_mysql_with_db_attr_bytes(self): mock_cursor = mock.Mock() mock_connection = mock.Mock() mock_connection.db = b"mysql_test_db" del mock_connection.database # Remove database attribute mock_cursor.connection = mock_connection result = _get_db_name_from_cursor_or_conn("mysql", mock_connection, mock_cursor) self.assertEqual(result, "mysql_test_db") def test_get_db_name_from_cursor_or_conn_mysql_with_cnx_attr(self): mock_cursor = mock.Mock() mock_connection = mock.Mock() mock_cnx = mock.Mock() mock_cnx.database = "mysql_cnx_db" mock_cursor.connection = None mock_cursor._cnx = mock_cnx result = _get_db_name_from_cursor_or_conn("mysql", mock_connection, mock_cursor) self.assertEqual(result, "mysql_cnx_db") def test_get_db_name_from_cursor_or_conn_mssql_with_database_attr(self): mock_cursor = mock.Mock() mock_connection = mock.Mock() mock_connection.database = "mssql_test_db" mock_cursor.connection = mock_connection result = _get_db_name_from_cursor_or_conn("mssql", mock_connection, mock_cursor) self.assertEqual(result, "mssql_test_db") def test_get_db_name_from_cursor_or_conn_mysql_variant_names(self): mock_cursor = mock.Mock() mock_connection = mock.Mock() mock_connection.database = "test_db" mock_cursor.connection = mock_connection for vendor in ["mysql", "pymysql", "mysqlclient", "mysql+pymysql"]: result = _get_db_name_from_cursor_or_conn(vendor, mock_connection, mock_cursor) self.assertEqual(result, "test_db", f"Failed for vendor: {vendor}") def test_get_db_name_from_cursor_or_conn_mssql_variant_names(self): mock_cursor = mock.Mock() mock_connection = mock.Mock() mock_connection.database = "test_db" mock_cursor.connection = mock_connection for vendor in ["mssql", "mssql+pyodbc", "sqlserver"]: result = _get_db_name_from_cursor_or_conn(vendor, mock_connection, mock_cursor) self.assertEqual(result, "test_db", f"Failed for vendor: {vendor}") def test_get_db_name_from_cursor_or_conn_postgresql_variant_names(self): mock_cursor = mock.Mock() mock_connection = mock.Mock() mock_info = mock.Mock() mock_info.dbname = "test_db" mock_connection.info = mock_info mock_cursor.connection = mock_connection for vendor in ["postgresql", "postgres", "postgresql+psycopg2"]: result = _get_db_name_from_cursor_or_conn(vendor, mock_connection, mock_cursor) self.assertEqual(result, "test_db", f"Failed for vendor: {vendor}") test_sqlalchemy_metrics.py000066400000000000000000000103331527070072300416710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import sqlalchemy from sqlalchemy.pool import QueuePool from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from opentelemetry.test.test_base import TestBase SCOPE = "opentelemetry.instrumentation.sqlalchemy" class TestSqlalchemyMetricsInstrumentation(TestBase): def setUp(self): super().setUp() SQLAlchemyInstrumentor().instrument( tracer_provider=self.tracer_provider, ) def tearDown(self): super().tearDown() SQLAlchemyInstrumentor().uninstrument() def assert_pool_idle_used_expected(self, pool_name, idle, used): metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 1) self.assert_metric_expected( metrics[0], [ self.create_number_data_point( value=idle, attributes={"pool.name": pool_name, "state": "idle"}, ), self.create_number_data_point( value=used, attributes={"pool.name": pool_name, "state": "used"}, ), ], ) def test_metrics_one_connection(self): pool_name = "pool_test_name" engine = sqlalchemy.create_engine( "sqlite:///:memory:", pool_size=5, poolclass=QueuePool, pool_logging_name=pool_name, ) self.assertEqual(len(self.get_sorted_metrics(SCOPE)), 0) with engine.connect(): self.assert_pool_idle_used_expected(pool_name=pool_name, idle=0, used=1) # After the connection is closed self.assert_pool_idle_used_expected(pool_name=pool_name, idle=1, used=0) def test_metrics_without_pool_name(self): pool_name = "pool_test_name" engine = sqlalchemy.create_engine( "sqlite:///:memory:", pool_size=5, poolclass=QueuePool, pool_logging_name=pool_name, ) self.assertEqual(len(self.get_sorted_metrics(SCOPE)), 0) with engine.connect(): self.assert_pool_idle_used_expected(pool_name=pool_name, idle=0, used=1) # After the connection is closed self.assert_pool_idle_used_expected(pool_name=pool_name, idle=1, used=0) def test_metrics_two_connections(self): pool_name = "pool_test_name" engine = sqlalchemy.create_engine( "sqlite:///:memory:", pool_size=5, poolclass=QueuePool, pool_logging_name=pool_name, ) self.assertEqual(len(self.get_sorted_metrics(SCOPE)), 0) with engine.connect(): with engine.connect(): self.assert_pool_idle_used_expected(pool_name, idle=0, used=2) # After the first connection is closed self.assert_pool_idle_used_expected(pool_name, idle=1, used=1) # After the two connections are closed self.assert_pool_idle_used_expected(pool_name, idle=2, used=0) def test_metrics_connections(self): pool_name = "pool_test_name" engine = sqlalchemy.create_engine( "sqlite:///:memory:", pool_size=5, poolclass=QueuePool, pool_logging_name=pool_name, ) self.assertEqual(len(self.get_sorted_metrics(SCOPE)), 0) with engine.connect(): with engine.connect(): self.assert_pool_idle_used_expected(pool_name=pool_name, idle=0, used=2) # After the first connection is closed self.assert_pool_idle_used_expected(pool_name=pool_name, idle=1, used=1) # Resume from idle to used with engine.connect(): self.assert_pool_idle_used_expected(pool_name=pool_name, idle=0, used=2) # After the two connections are closed self.assert_pool_idle_used_expected(pool_name=pool_name, idle=2, used=0) def test_metric_uninstrument(self): SQLAlchemyInstrumentor().uninstrument() engine = sqlalchemy.create_engine( "sqlite:///:memory:", poolclass=QueuePool, ) engine.connect() self.assertEqual(len(self.get_sorted_metrics(SCOPE)), 0) test_sqlcommenter.py000066400000000000000000000434361527070072300405240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import re from unittest import mock import pytest from sqlalchemy import ( create_engine, text, ) from opentelemetry import context from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_STATEMENT, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_QUERY_TEXT, ) from opentelemetry.test.test_base import TestBase class TestSqlalchemyInstrumentationWithSQLCommenter(TestBase): @pytest.fixture(autouse=True) def inject_fixtures(self, caplog): self.caplog = caplog def setUp(self): super().setUp() _OpenTelemetrySemanticConventionStability._initialized = False def tearDown(self): super().tearDown() SQLAlchemyInstrumentor().uninstrument() def test_sqlcommenter_disabled(self): logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) engine = create_engine("sqlite:///:memory:", echo=True) SQLAlchemyInstrumentor().instrument(engine=engine, tracer_provider=self.tracer_provider) cnx = engine.connect() cnx.execute(text("SELECT 1;")).fetchall() self.assertEqual(self.caplog.records[-2].getMessage(), "SELECT 1;") def test_sqlcommenter_enabled(self): engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, enable_commenter=True, commenter_options={"db_framework": False}, ) cnx = engine.connect() cnx.execute(text("SELECT 1;")).fetchall() self.assertRegex( self.caplog.records[-2].getMessage(), r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) def test_sqlcommenter_default_stmt_enabled_no_comments_anywhere(self): engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, # enable_commenter not set enable_attribute_commenter=True, ) cnx = engine.connect() cnx.execute(text("SELECT 1;")).fetchall() query_log = self.caplog.records[-2].getMessage() self.assertEqual( query_log, "SELECT 1;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span is connection to db self.assertEqual(spans[0].name, "connect") # second span is query itself query_span = spans[1] self.assertEqual( query_span.attributes[DB_STATEMENT], "SELECT 1;", ) def test_sqlcommenter_disabled_stmt_enabled_no_comments_anywhere(self): engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, enable_commenter=False, enable_attribute_commenter=True, ) cnx = engine.connect() cnx.execute(text("SELECT 1;")).fetchall() query_log = self.caplog.records[-2].getMessage() self.assertEqual( query_log, "SELECT 1;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span is connection to db self.assertEqual(spans[0].name, "connect") # second span is query itself query_span = spans[1] self.assertEqual( query_span.attributes[DB_STATEMENT], "SELECT 1;", ) def test_sqlcommenter_enabled_stmt_disabled_default( self, ): engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, enable_commenter=True, commenter_options={"db_framework": False}, # enable_attribute_commenter not set ) cnx = engine.connect() cnx.execute(text("SELECT 1;")).fetchall() query_log = self.caplog.records[-2].getMessage() self.assertRegex( query_log, r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span is connection to db self.assertEqual(spans[0].name, "connect") # second span is query itself query_span = spans[1] self.assertEqual( query_span.attributes[DB_STATEMENT], "SELECT 1;", ) def test_sqlcommenter_enabled_stmt_enabled_matches_db_statement_attribute( self, ): engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, enable_commenter=True, commenter_options={"db_framework": False}, enable_attribute_commenter=True, ) cnx = engine.connect() cnx.execute(text("SELECT 1;")).fetchall() query_log = self.caplog.records[-2].getMessage() self.assertRegex( query_log, r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span is connection to db self.assertEqual(spans[0].name, "connect") # second span is query itself query_span = spans[1] self.assertRegex( query_span.attributes[DB_STATEMENT], r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) cnx_span_id = re.search(r"[a-zA-Z0-9_]{16}", query_log).group() db_statement_span_id = re.search( r"[a-zA-Z0-9_]{16}", query_span.attributes[DB_STATEMENT], ).group() self.assertEqual(cnx_span_id, db_statement_span_id) def test_sqlcommenter_enabled_otel_values_false(self): engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, enable_commenter=True, commenter_options={ "db_framework": False, "opentelemetry_values": False, }, ) cnx = engine.connect() cnx.execute(text("SELECT 1;")).fetchall() self.assertRegex( self.caplog.records[-2].getMessage(), r"SELECT 1 /\*db_driver='(.*)'\*/;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span is connection to db self.assertEqual(spans[0].name, "connect") # second span is query itself query_span = spans[1] self.assertEqual( query_span.attributes[DB_STATEMENT], r"SELECT 1;", ) def test_sqlcommenter_enabled_stmt_enabled_otel_values_false(self): engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, enable_commenter=True, commenter_options={ "db_framework": False, "opentelemetry_values": False, }, enable_attribute_commenter=True, ) cnx = engine.connect() cnx.execute(text("SELECT 1;")).fetchall() self.assertRegex( self.caplog.records[-2].getMessage(), r"SELECT 1 /\*db_driver='(.*)'\*/;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span is connection to db self.assertEqual(spans[0].name, "connect") # second span is query itself query_span = spans[1] self.assertRegex( query_span.attributes[DB_STATEMENT], r"SELECT 1 /\*db_driver='(.*)'*/;", ) def test_sqlcommenter_flask_integration(self): engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, enable_commenter=True, commenter_options={"db_framework": False}, ) cnx = engine.connect() current_context = context.get_current() sqlcommenter_context = context.set_value("SQLCOMMENTER_ORM_TAGS_AND_VALUES", {"flask": 1}, current_context) context.attach(sqlcommenter_context) cnx.execute(text("SELECT 1;")).fetchall() self.assertRegex( self.caplog.records[-2].getMessage(), r"SELECT 1 /\*db_driver='(.*)',flask=1,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span is connection to db self.assertEqual(spans[0].name, "connect") # second span is query itself query_span = spans[1] self.assertEqual( query_span.attributes[DB_STATEMENT], "SELECT 1;", ) def test_sqlcommenter_stmt_enabled_flask_integration(self): engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, enable_commenter=True, commenter_options={"db_framework": False}, enable_attribute_commenter=True, ) cnx = engine.connect() current_context = context.get_current() sqlcommenter_context = context.set_value("SQLCOMMENTER_ORM_TAGS_AND_VALUES", {"flask": 1}, current_context) context.attach(sqlcommenter_context) cnx.execute(text("SELECT 1;")).fetchall() self.assertRegex( self.caplog.records[-2].getMessage(), r"SELECT 1 /\*db_driver='(.*)',flask=1,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span is connection to db self.assertEqual(spans[0].name, "connect") # second span is query itself query_span = spans[1] self.assertRegex( query_span.attributes[DB_STATEMENT], r"SELECT 1 /\*db_driver='(.*)',flask=1,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) def test_sqlcommenter_enabled_create_engine_after_instrumentation(self): SQLAlchemyInstrumentor().instrument( tracer_provider=self.tracer_provider, enable_commenter=True, ) from sqlalchemy import ( # noqa: PLC0415 create_engine, # pylint: disable-all ) engine = create_engine("sqlite:///:memory:") cnx = engine.connect() cnx.execute(text("SELECT 1;")).fetchall() self.assertRegex( self.caplog.records[-2].getMessage(), r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span is connection to db self.assertEqual(spans[0].name, "connect") # second span is query itself query_span = spans[1] self.assertEqual( query_span.attributes[DB_STATEMENT], "SELECT 1;", ) def test_sqlcommenter_enabled_stmt_enabled_create_engine_after_instrumentation( self, ): SQLAlchemyInstrumentor().instrument( tracer_provider=self.tracer_provider, enable_commenter=True, enable_attribute_commenter=True, ) from sqlalchemy import ( # noqa: PLC0415 create_engine, # pylint: disable-all ) engine = create_engine("sqlite:///:memory:") cnx = engine.connect() cnx.execute(text("SELECT 1;")).fetchall() self.assertRegex( self.caplog.records[-2].getMessage(), r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # first span is connection to db self.assertEqual(spans[0].name, "connect") # second span is query itself query_span = spans[1] self.assertRegex( query_span.attributes[DB_STATEMENT], r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "database"}) def test_sqlcommenter_enabled_database_mode(self): _OpenTelemetrySemanticConventionStability._initialized = False _OpenTelemetrySemanticConventionStability._initialize() logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, enable_commenter=True, commenter_options={"db_framework": False}, ) cnx = engine.connect() cnx.execute(text("SELECT 1;")).fetchall() # Query log should have sqlcommenter self.assertRegex( self.caplog.records[-2].getMessage(), r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) query_span = spans[1] # Should use new semconv attribute (db.query.text not db.statement) self.assertNotIn(DB_STATEMENT, query_span.attributes) self.assertIn(DB_QUERY_TEXT, query_span.attributes) self.assertEqual(query_span.attributes[DB_QUERY_TEXT], "SELECT 1;") @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "database"}) def test_sqlcommenter_enabled_stmt_enabled_database_mode(self): _OpenTelemetrySemanticConventionStability._initialized = False _OpenTelemetrySemanticConventionStability._initialize() logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, enable_commenter=True, commenter_options={"db_framework": False}, enable_attribute_commenter=True, ) cnx = engine.connect() cnx.execute(text("SELECT 1;")).fetchall() query_log = self.caplog.records[-2].getMessage() self.assertRegex( query_log, r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) query_span = spans[1] # Should use new semconv attribute with comment self.assertNotIn(DB_STATEMENT, query_span.attributes) self.assertIn(DB_QUERY_TEXT, query_span.attributes) self.assertRegex( query_span.attributes[DB_QUERY_TEXT], r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) @mock.patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "database/dup"}) def test_sqlcommenter_enabled_database_dup_mode(self): _OpenTelemetrySemanticConventionStability._initialized = False _OpenTelemetrySemanticConventionStability._initialize() logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) engine = create_engine("sqlite:///:memory:") SQLAlchemyInstrumentor().instrument( engine=engine, tracer_provider=self.tracer_provider, enable_commenter=True, commenter_options={"db_framework": False}, enable_attribute_commenter=True, ) cnx = engine.connect() cnx.execute(text("SELECT 1;")).fetchall() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) query_span = spans[1] # Should have both old and new semconv attributes with comment self.assertIn(DB_STATEMENT, query_span.attributes) self.assertRegex( query_span.attributes[DB_STATEMENT], r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) self.assertIn(DB_QUERY_TEXT, query_span.attributes) self.assertRegex( query_span.attributes[DB_QUERY_TEXT], r"SELECT 1 /\*db_driver='(.*)',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) def test_sqlcommenter_disabled_create_engine_after_instrumentation(self): SQLAlchemyInstrumentor().instrument( tracer_provider=self.tracer_provider, enable_commenter=False, ) from sqlalchemy import ( # noqa: PLC0415 create_engine, # pylint: disable-all ) engine = create_engine("sqlite:///:memory:") cnx = engine.connect() cnx.execute(text("SELECT 1;")).fetchall() self.assertEqual(self.caplog.records[-2].getMessage(), "SELECT 1;") opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlite3/000077500000000000000000000000001527070072300325275ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlite3/LICENSE000066400000000000000000000261351527070072300335430ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlite3/README.rst000066400000000000000000000012401527070072300342130ustar00rootroot00000000000000OpenTelemetry SQLite3 Instrumentation ===================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-sqlite3.svg :target: https://pypi.org/project/opentelemetry-instrumentation-sqlite3/ Installation ------------ :: pip install opentelemetry-instrumentation-sqlite3 References ---------- * `OpenTelemetry SQLite3 Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031051527070072300353630ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlite3[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-sqlite3" dynamic = ["version"] description = "OpenTelemetry SQLite3 instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-instrumentation-dbapi == 0.66b2", ] [project.optional-dependencies] instruments = [] [project.entry-points.opentelemetry_instrumentor] sqlite3 = "opentelemetry.instrumentation.sqlite3:SQLite3Instrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-sqlite3" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/sqlite3/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlite3/src/000077500000000000000000000000001527070072300333165ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300361335ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlite3/srcinstrumentation/000077500000000000000000000000001527070072300413765ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetrysqlite3/000077500000000000000000000000001527070072300427625ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation__init__.py000066400000000000000000000101461527070072300450750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ SQLite instrumentation supporting `sqlite3`_, it can be enabled by using ``SQLite3Instrumentor``. At this time, cursor objects must be explicitly initialized as shown below to support tracing. .. _sqlite3: https://docs.python.org/3/library/sqlite3.html Usage ----- .. code:: python import sqlite3 from opentelemetry.instrumentation.sqlite3 import SQLite3Instrumentor # Call instrument() to wrap all database connections SQLite3Instrumentor().instrument() cnx = sqlite3.connect(':memory:') cursor = cnx.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)") cursor.execute("INSERT INTO test (testField) VALUES (123)") cursor.close() cnx.close() .. code:: python import sqlite3 from opentelemetry.instrumentation.sqlite3 import SQLite3Instrumentor # Alternatively, use instrument_connection for an individual connection conn = sqlite3.connect(":memory:") instrumented_connection = SQLite3Instrumentor().instrument_connection(conn) cursor = instrumented_connection.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)") cursor.execute("INSERT INTO test (testField) VALUES (123)") cursor.execute("SELECT * FROM test") cursor.close() instrumented_connection.close() API --- """ from __future__ import annotations import sqlite3 from collections.abc import Collection from sqlite3 import dbapi2 from typing import Any, TypeVar from opentelemetry.instrumentation import dbapi from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.sqlite3.package import _instruments from opentelemetry.instrumentation.sqlite3.version import __version__ from opentelemetry.trace import TracerProvider # No useful attributes of sqlite3 connection object _CONNECTION_ATTRIBUTES = {} _DATABASE_SYSTEM = "sqlite" SQLite3Connection = TypeVar( # pylint: disable=invalid-name "SQLite3Connection", bound=sqlite3.Connection | None ) class SQLite3Instrumentor(BaseInstrumentor): _TO_WRAP = [sqlite3, dbapi2] def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs: Any) -> None: """Integrate with SQLite3 Python library. https://docs.python.org/3/library/sqlite3.html """ tracer_provider = kwargs.get("tracer_provider") for module in self._TO_WRAP: dbapi.wrap_connect( __name__, module, "connect", _DATABASE_SYSTEM, _CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, ) def _uninstrument(self, **kwargs: Any) -> None: """ "Disable SQLite3 instrumentation""" for module in self._TO_WRAP: dbapi.unwrap_connect(module, "connect") @staticmethod def instrument_connection( connection: SQLite3Connection, tracer_provider: TracerProvider | None = None, ) -> SQLite3Connection: """Enable instrumentation in a SQLite connection. Args: connection: The connection to instrument. tracer_provider: The optional tracer provider to use. If omitted the current globally configured one is used. Returns: An instrumented SQLite connection that supports telemetry for tracing database operations. """ return dbapi.instrument_connection( __name__, connection, _DATABASE_SYSTEM, _CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, ) @staticmethod def uninstrument_connection( connection: SQLite3Connection, ) -> SQLite3Connection: """Disable instrumentation in a SQLite connection. Args: connection: The connection to uninstrument. Returns: An uninstrumented connection. """ return dbapi.uninstrument_connection(connection) package.py000066400000000000000000000002671527070072300447340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations _instruments: tuple[str, ...] = tuple() _semconv_status = "migration" py.typed000066400000000000000000000000001527070072300444470ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3version.py000066400000000000000000000001741527070072300450230ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" _instruments = tuple() test-requirements.txt000066400000000000000000000005041527070072300367100ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlite3asgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-dbapi -e instrumentation/opentelemetry-instrumentation-sqlite3 opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlite3/tests/000077500000000000000000000000001527070072300336715ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300357110ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlite3/teststest_sqlite3.py000066400000000000000000000303101527070072300366040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlite3/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import contextlib import sqlite3 from sqlite3 import dbapi2 from unittest import mock from opentelemetry import trace as trace_api from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.sqlite3 import SQLite3Instrumentor from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_STATEMENT, DB_SYSTEM, DB_USER, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_NAMESPACE, DB_QUERY_TEXT, DB_SYSTEM_NAME, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.test.test_base import TestBase @contextlib.contextmanager def use_semconv_opt_in(sem_conv_mode): env_patch = mock.patch.dict( "os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode}, ) _OpenTelemetrySemanticConventionStability._initialized = False env_patch.start() try: yield finally: env_patch.stop() _OpenTelemetrySemanticConventionStability._initialized = False class TestSQLite3(TestBase): def setUp(self): super().setUp() SQLite3Instrumentor().instrument(tracer_provider=self.tracer_provider) self._tracer = self.tracer_provider.get_tracer(__name__) self._connection = sqlite3.connect(":memory:") self._cursor = self._connection.cursor() self._connection2 = dbapi2.connect(":memory:") self._cursor2 = self._connection2.cursor() self._connection3 = SQLite3Instrumentor.instrument_connection(dbapi2.connect(":memory:")) self._cursor3 = self._connection3.cursor() def tearDown(self): super().tearDown() if self._cursor: self._cursor.close() if self._connection: self._connection.close() if self._cursor2: self._cursor2.close() if self._connection2: self._connection2.close() if self._cursor3: self._cursor3.close() if self._connection3: self._connection3.close() SQLite3Instrumentor().uninstrument() def validate_spans(self, span_name): spans = self.memory_exporter.get_finished_spans() self.memory_exporter.clear() self.assertEqual(len(spans), 2) root_span = None child_span = None for span in spans: if span.name == "rootSpan": root_span = span else: child_span = span self.assertIsInstance(span.start_time, int) self.assertIsInstance(span.end_time, int) self.assertIsNotNone(root_span) self.assertIsNotNone(child_span) self.assertEqual(root_span.name, "rootSpan") self.assertEqual(child_span.name, span_name) self.assertIsNotNone(child_span.parent) self.assertIs(child_span.parent, root_span.get_span_context()) self.assertIs(child_span.kind, trace_api.SpanKind.CLIENT) def _create_tables(self): stmt = "CREATE TABLE IF NOT EXISTS test (id integer)" self._cursor.execute(stmt) self._cursor2.execute(stmt) self._cursor3.execute(stmt) self.memory_exporter.clear() def test_execute(self): """Should create a child span for execute method""" stmt = "CREATE TABLE IF NOT EXISTS test (id integer)" with self._tracer.start_as_current_span("rootSpan"): self._cursor.execute(stmt) self.validate_spans("CREATE") with self._tracer.start_as_current_span("rootSpan"): self._cursor2.execute(stmt) self.validate_spans("CREATE") with self._tracer.start_as_current_span("rootSpan"): self._cursor3.execute(stmt) self.validate_spans("CREATE") def test_executemany(self): """Should create a child span for executemany""" self._create_tables() # real spans for executemany stmt = "INSERT INTO test (id) VALUES (?)" data = [("1",), ("2",), ("3",)] with self._tracer.start_as_current_span("rootSpan"): self._cursor.executemany(stmt, data) self.validate_spans("INSERT") with self._tracer.start_as_current_span("rootSpan"): self._cursor2.executemany(stmt, data) self.validate_spans("INSERT") with self._tracer.start_as_current_span("rootSpan"): self._cursor3.executemany(stmt, data) self.validate_spans("INSERT") def test_callproc(self): """Should create a child span for callproc""" with ( self._tracer.start_as_current_span("rootSpan"), self.assertRaises(Exception), ): self._cursor.callproc("test", ()) self.validate_spans("test") class TestSQLite3Integration(TestBase): def tearDown(self): super().tearDown() SQLite3Instrumentor().uninstrument() def _connect(self): """Create an in-memory connection with cleanup registered.""" cnx = sqlite3.connect(":memory:") self.addCleanup(cnx.close) return cnx def test_uninstrument(self): """Should stop generating spans after uninstrument.""" SQLite3Instrumentor().instrument(tracer_provider=self.tracer_provider) cnx = self._connect() cursor = cnx.cursor() self.addCleanup(cursor.close) cursor.execute("CREATE TABLE IF NOT EXISTS test (id integer)") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) SQLite3Instrumentor().uninstrument() self.memory_exporter.clear() cnx2 = self._connect() cursor2 = cnx2.cursor() self.addCleanup(cursor2.close) cursor2.execute("CREATE TABLE IF NOT EXISTS test (id integer)") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) def test_uninstrument_connection_with_instrument(self): """Should stop generating spans for uninstrumented connection.""" SQLite3Instrumentor().instrument(tracer_provider=self.tracer_provider) cnx = self._connect() query = "CREATE TABLE IF NOT EXISTS test (id integer)" cursor = cnx.cursor() self.addCleanup(cursor.close) cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) self.memory_exporter.clear() cnx = SQLite3Instrumentor.uninstrument_connection(cnx) cursor = cnx.cursor() self.addCleanup(cursor.close) cursor.execute(query) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) def test_no_op_tracer_provider(self): """Should produce no spans with NoOpTracerProvider.""" SQLite3Instrumentor().instrument(tracer_provider=trace_api.NoOpTracerProvider()) cnx = self._connect() cursor = cnx.cursor() self.addCleanup(cursor.close) cursor.execute("CREATE TABLE IF NOT EXISTS test (id integer)") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) def test_suppress_instrumentation(self): """Should produce no spans when suppressed.""" SQLite3Instrumentor().instrument(tracer_provider=self.tracer_provider) cnx = self._connect() with suppress_instrumentation(): cursor = cnx.cursor() self.addCleanup(cursor.close) cursor.execute("CREATE TABLE IF NOT EXISTS test (id integer)") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) def test_span_failed(self): """Should set error status on span when query fails.""" SQLite3Instrumentor().instrument(tracer_provider=self.tracer_provider) cnx = self._connect() cursor = cnx.cursor() self.addCleanup(cursor.close) with self.assertRaises(sqlite3.OperationalError): cursor.execute("SELECT * FROM nonexistent_table") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual( span.attributes[DB_STATEMENT], "SELECT * FROM nonexistent_table", ) self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) self.assertEqual( span.status.description, "OperationalError: no such table: nonexistent_table", ) def test_semconv_default(self): """No opt-in emits only legacy attributes.""" with use_semconv_opt_in(""): SQLite3Instrumentor().instrument(tracer_provider=self.tracer_provider) cnx = self._connect() cursor = cnx.cursor() self.addCleanup(cursor.close) cursor.execute("SELECT 1") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_SYSTEM], "sqlite") self.assertEqual(span.attributes[DB_STATEMENT], "SELECT 1") self.assertNotIn(DB_SYSTEM_NAME, span.attributes) self.assertNotIn(DB_QUERY_TEXT, span.attributes) # sqlite3 does not capture connection attributes, so neither # legacy nor stable database/peer attributes are emitted. self.assertNotIn(DB_NAME, span.attributes) self.assertNotIn(DB_NAMESPACE, span.attributes) self.assertNotIn(DB_USER, span.attributes) self.assertNotIn(NET_PEER_NAME, span.attributes) self.assertNotIn(NET_PEER_PORT, span.attributes) self.assertNotIn(SERVER_ADDRESS, span.attributes) self.assertNotIn(SERVER_PORT, span.attributes) def test_semconv_stable(self): """database,http opt-in emits only stable attributes.""" with use_semconv_opt_in("database,http"): SQLite3Instrumentor().instrument(tracer_provider=self.tracer_provider) cnx = self._connect() cursor = cnx.cursor() self.addCleanup(cursor.close) cursor.execute("SELECT 1") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_SYSTEM_NAME], "sqlite") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT 1") self.assertNotIn(DB_SYSTEM, span.attributes) self.assertNotIn(DB_STATEMENT, span.attributes) # sqlite3 does not capture connection attributes, so neither # legacy nor stable database/peer attributes are emitted. self.assertNotIn(DB_NAME, span.attributes) self.assertNotIn(DB_NAMESPACE, span.attributes) self.assertNotIn(DB_USER, span.attributes) self.assertNotIn(NET_PEER_NAME, span.attributes) self.assertNotIn(NET_PEER_PORT, span.attributes) self.assertNotIn(SERVER_ADDRESS, span.attributes) self.assertNotIn(SERVER_PORT, span.attributes) def test_semconv_dup(self): """database/dup,http/dup opt-in emits both legacy and stable attributes.""" with use_semconv_opt_in("database/dup,http/dup"): SQLite3Instrumentor().instrument(tracer_provider=self.tracer_provider) cnx = self._connect() cursor = cnx.cursor() self.addCleanup(cursor.close) cursor.execute("SELECT 1") spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.attributes[DB_SYSTEM], "sqlite") self.assertEqual(span.attributes[DB_SYSTEM_NAME], "sqlite") self.assertEqual(span.attributes[DB_STATEMENT], "SELECT 1") self.assertEqual(span.attributes[DB_QUERY_TEXT], "SELECT 1") testdb000066400000000000000000000200001527070072300350120ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-sqlite3/testsSQLite format 3@ .WH ÌÌ2ItabletesttestCREATE TABLE test (id integer) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-starlette/000077500000000000000000000000001527070072300331525ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-starlette/LICENSE000066400000000000000000000261351527070072300341660ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000014301527070072300345600ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-starletteOpenTelemetry Starlette Instrumentation ======================================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-starlette.svg :target: https://pypi.org/project/opentelemetry-instrumentation-starlette/ This library provides automatic and manual instrumentation of Starlette web frameworks, instrumenting http requests served by applications utilizing the framework. auto-instrumentation using the opentelemetry-instrumentation package is also supported. Installation ------------ :: pip install opentelemetry-instrumentation-starlette References ---------- * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000032671527070072300360170ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-starlette[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-starlette" dynamic = ["version"] description = "OpenTelemetry Starlette Instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-instrumentation-asgi == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-util-http == 0.66b2", ] [project.optional-dependencies] instruments = ["starlette >= 0.13"] [project.entry-points.opentelemetry_instrumentor] starlette = "opentelemetry.instrumentation.starlette:StarletteInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-starlette" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/starlette/version.py" [tool.hatch.build.targets.sdist] include = ["/src", "/tests"] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-starlette/src/000077500000000000000000000000001527070072300337415ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300365565ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-starlette/srcinstrumentation/000077500000000000000000000000001527070072300420215ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetrystarlette/000077500000000000000000000000001527070072300440305ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation__init__.py000066400000000000000000000356161527070072300461540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Usage ----- .. code-block:: python from opentelemetry.instrumentation.starlette import StarletteInstrumentor from starlette import applications from starlette.responses import PlainTextResponse from starlette.routing import Route def home(request): return PlainTextResponse("hi") app = applications.Starlette(routes=[Route("/foobar", home)]) StarletteInstrumentor.instrument_app(app) Configuration ------------- Exclude lists ************* To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_STARLETTE_EXCLUDED_URLS`` (or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the URLs. For example, :: export OTEL_PYTHON_STARLETTE_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. Request/Response hooks ********************** This instrumentation supports request and response hooks. These are functions that get called right after a span is created for a request and right before the span is finished for the response. - The server request hook is passed a server span and ASGI scope object for every incoming request. - The client request hook is called with the internal span, and ASGI scope and event when the method ``receive`` is called. - The client response hook is called with the internal span, and ASGI scope and event when the method ``send`` is called. For example, .. code-block:: python from opentelemetry.instrumentation.starlette import StarletteInstrumentor from opentelemetry.trace import Span from typing import Any def server_request_hook(span: Span, scope: dict[str, Any]): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_request_hook", "some-value") def client_request_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_client_request_hook", "some-value") def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_response_hook", "some-value") StarletteInstrumentor().instrument( server_request_hook=server_request_hook, client_request_hook=client_request_hook, client_response_hook=client_response_hook, ) Capture HTTP request and response headers ***************************************** You can configure the agent to capture specified HTTP headers as span attributes, according to the `semantic conventions `_. Request headers *************** To capture HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. Request header names in Starlette are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" Would match all request headers that start with ``Accept`` and ``X-``. Additionally, the special keyword ``all`` can be used to capture all request headers. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="all" The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a list containing the header values. For example: ``http.request.header.custom_request_header = ["", ""]`` Response headers **************** To capture HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. Response header names in Starlette are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" Would match all response headers that start with ``Content`` and ``X-``. Additionally, the special keyword ``all`` can be used to capture all response headers. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="all" The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a list containing the header values. For example: ``http.response.header.custom_response_header = ["", ""]`` Sanitizing headers ****************** In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be matched in a case-insensitive manner. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. Note: The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. API --- """ from __future__ import annotations from collections.abc import Collection from typing import TYPE_CHECKING, Any, cast from weakref import WeakSet from starlette import applications from starlette.routing import Match from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.instrumentation.asgi.types import ( ClientRequestHook, ClientResponseHook, ServerRequestHook, ) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.starlette.package import _instruments from opentelemetry.instrumentation.starlette.version import __version__ from opentelemetry.metrics import MeterProvider, get_meter from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_ROUTE, ) from opentelemetry.trace import TracerProvider, get_tracer from opentelemetry.util.http import get_excluded_urls if TYPE_CHECKING: from typing import TypedDict, Unpack class InstrumentKwargs(TypedDict, total=False): tracer_provider: TracerProvider meter_provider: MeterProvider server_request_hook: ServerRequestHook client_request_hook: ClientRequestHook client_response_hook: ClientResponseHook _excluded_urls = get_excluded_urls("STARLETTE") class StarletteInstrumentor(BaseInstrumentor): """An instrumentor for Starlette. See `BaseInstrumentor`. """ _original_starlette = None @staticmethod def instrument_app( app: applications.Starlette, server_request_hook: ServerRequestHook = None, client_request_hook: ClientRequestHook = None, client_response_hook: ClientResponseHook = None, meter_provider: MeterProvider | None = None, tracer_provider: TracerProvider | None = None, ): """Instrument an uninstrumented Starlette application. Args: app: The starlette ASGI application callable to forward requests to. server_request_hook: Optional callback which is called with the server span and ASGI scope object for every incoming request. client_request_hook: Optional callback which is called with the internal span, and ASGI scope and event which are sent as dictionaries for when the method receive is called. client_response_hook: Optional callback which is called with the internal span, and ASGI scope and event which are sent as dictionaries for when the method send is called. meter_provider: The optional meter provider to use. If omitted the current globally configured one is used. tracer_provider: The optional tracer provider to use. If omitted the current globally configured one is used. """ tracer = get_tracer( __name__, __version__, tracer_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) meter = get_meter( __name__, __version__, meter_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) if not getattr(app, "_is_instrumented_by_opentelemetry", False): app.add_middleware( OpenTelemetryMiddleware, excluded_urls=_excluded_urls, default_span_details=_get_default_span_details, server_request_hook=server_request_hook, client_request_hook=client_request_hook, client_response_hook=client_response_hook, # Pass in tracer/meter to get __name__and __version__ of starlette instrumentation tracer=tracer, meter=meter, ) app._is_instrumented_by_opentelemetry = True # adding apps to set for uninstrumenting _InstrumentedStarlette._instrumented_starlette_apps.add(app) @staticmethod def uninstrument_app(app: applications.Starlette): app.user_middleware = [x for x in app.user_middleware if x.cls is not OpenTelemetryMiddleware] app.middleware_stack = app.build_middleware_stack() app._is_instrumented_by_opentelemetry = False def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs: Unpack[InstrumentKwargs]): self._original_starlette = applications.Starlette _InstrumentedStarlette._tracer_provider = kwargs.get("tracer_provider") _InstrumentedStarlette._server_request_hook = kwargs.get("server_request_hook") _InstrumentedStarlette._client_request_hook = kwargs.get("client_request_hook") _InstrumentedStarlette._client_response_hook = kwargs.get("client_response_hook") _InstrumentedStarlette._meter_provider = kwargs.get("meter_provider") applications.Starlette = _InstrumentedStarlette def _uninstrument(self, **kwargs: Any): """uninstrumenting all created apps by user""" for instance in _InstrumentedStarlette._instrumented_starlette_apps: self.uninstrument_app(instance) _InstrumentedStarlette._instrumented_starlette_apps.clear() applications.Starlette = self._original_starlette class _InstrumentedStarlette(applications.Starlette): _tracer_provider: TracerProvider | None = None _meter_provider: MeterProvider | None = None _server_request_hook: ServerRequestHook = None _client_request_hook: ClientRequestHook = None _client_response_hook: ClientResponseHook = None _instrumented_starlette_apps: WeakSet[applications.Starlette] = WeakSet() def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) tracer = get_tracer( __name__, __version__, _InstrumentedStarlette._tracer_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) meter = get_meter( __name__, __version__, _InstrumentedStarlette._meter_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) self.add_middleware( OpenTelemetryMiddleware, excluded_urls=_excluded_urls, default_span_details=_get_default_span_details, server_request_hook=_InstrumentedStarlette._server_request_hook, client_request_hook=_InstrumentedStarlette._client_request_hook, client_response_hook=_InstrumentedStarlette._client_response_hook, # Pass in tracer/meter to get __name__and __version__ of starlette instrumentation tracer=tracer, meter=meter, ) self._is_instrumented_by_opentelemetry = True # adding apps to set for uninstrumenting _InstrumentedStarlette._instrumented_starlette_apps.add(self) def _get_route_details(scope: dict[str, Any]) -> str | None: """ Function to retrieve Starlette route from ASGI scope. TODO: there is currently no way to retrieve http.route from a starlette application from scope. See: https://github.com/encode/starlette/pull/804 Args: scope: The ASGI scope that contains the Starlette application in the "app" key. Returns: The path to the route if found, otherwise None. """ app = cast(applications.Starlette, scope["app"]) route: str | None = None for starlette_route in app.routes: match, _ = starlette_route.matches(scope) if match == Match.FULL: try: route = starlette_route.path except AttributeError: # routes added via host routing won't have a path attribute route = scope.get("path") break if match == Match.PARTIAL: route = starlette_route.path return route def _get_default_span_details( scope: dict[str, Any], ) -> tuple[str, dict[str, Any]]: """Callback to retrieve span name and attributes from ASGI scope. Args: scope: The ASGI scope that contains the Starlette application in the "app" key. Returns: A tuple of span name and attributes. """ route = _get_route_details(scope) method: str = scope.get("method", "") attributes: dict[str, Any] = {} if route: attributes[HTTP_ROUTE] = route if method and route: # http span_name = f"{method} {route}" elif route: # websocket span_name = route else: # fallback span_name = method return span_name, attributes package.py000066400000000000000000000002161527070072300457740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("starlette >= 0.13",) _supports_metrics = True py.typed000066400000000000000000000000001527070072300455150ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starletteversion.py000066400000000000000000000001441527070072300460660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.in000066400000000000000000000005041527070072300371220ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-starlette# Used to generate test-requirements.{oldest,latest}.txt with uv pip compile pytest~=7.4 requests~=2.33.1 urllib3~=2.7.0 httpx~=0.28 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-asgi -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-starlette[instruments] test-requirements.latest.txt000066400000000000000000000055541527070072300406400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-starlette# This file was autogenerated by uv via the following command: # uv pip compile --python 3.10 --universal --no-emit-package opentelemetry-api --no-emit-package opentelemetry-semantic-conventions -c dev-requirements.txt instrumentation/opentelemetry-instrumentation-starlette/test-requirements.in -o instrumentation/opentelemetry-instrumentation-starlette/test-requirements.latest.txt -e instrumentation/opentelemetry-instrumentation-asgi # via # -r instrumentation/opentelemetry-instrumentation-starlette/test-requirements.in # opentelemetry-instrumentation-starlette -e instrumentation/opentelemetry-instrumentation-starlette # via -r instrumentation/opentelemetry-instrumentation-starlette/test-requirements.in -e opentelemetry-instrumentation # via # -r instrumentation/opentelemetry-instrumentation-starlette/test-requirements.in # opentelemetry-instrumentation-asgi # opentelemetry-instrumentation-starlette -e util/opentelemetry-util-http # via # -r instrumentation/opentelemetry-instrumentation-starlette/test-requirements.in # opentelemetry-instrumentation-asgi # opentelemetry-instrumentation-starlette anyio==4.8.0 # via # httpx # starlette asgiref==3.8.1 # via opentelemetry-instrumentation-asgi certifi==2025.1.31 # via # httpcore # httpx # requests charset-normalizer==3.4.1 # via requests colorama==0.4.6 ; sys_platform == 'win32' # via pytest exceptiongroup==1.2.2 ; python_full_version < '3.11' # via # anyio # pytest h11==0.14.0 # via httpcore httpcore==1.0.7 # via httpx httpx==0.28.1 # via -r instrumentation/opentelemetry-instrumentation-starlette/test-requirements.in idna==3.10 # via # anyio # httpx # requests importlib-metadata==8.6.1 # via opentelemetry-api iniconfig==2.0.0 # via pytest packaging==24.2 # via # opentelemetry-instrumentation # pytest pluggy==1.6.0 # via pytest pytest==7.4.4 # via # -c dev-requirements.txt # -r instrumentation/opentelemetry-instrumentation-starlette/test-requirements.in requests==2.33.1 # via # -c dev-requirements.txt # -r instrumentation/opentelemetry-instrumentation-starlette/test-requirements.in sniffio==1.3.1 # via anyio starlette==0.46.2 # via opentelemetry-instrumentation-starlette tomli==2.2.1 ; python_full_version < '3.11' # via pytest typing-extensions==4.12.2 # via # anyio # asgiref # opentelemetry-api # opentelemetry-semantic-conventions urllib3==2.7.0 # via # -r instrumentation/opentelemetry-instrumentation-starlette/test-requirements.in # requests wrapt==1.14.1 # via opentelemetry-instrumentation zipp==3.21.0 # via importlib-metadata # The following packages were excluded from the output: # opentelemetry-api # opentelemetry-semantic-conventions test-requirements.oldest.txt000066400000000000000000000055551527070072300406370ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-starlette# This file was autogenerated by uv via the following command: # uv pip compile --python 3.10 --universal --resolution lowest-direct --no-emit-package opentelemetry-api --no-emit-package opentelemetry-semantic-conventions -c dev-requirements.txt instrumentation/opentelemetry-instrumentation-starlette/test-requirements.in -o instrumentation/opentelemetry-instrumentation-starlette/test-requirements.oldest.txt -e instrumentation/opentelemetry-instrumentation-asgi # via # -r instrumentation/opentelemetry-instrumentation-starlette/test-requirements.in # opentelemetry-instrumentation-starlette -e instrumentation/opentelemetry-instrumentation-starlette # via -r instrumentation/opentelemetry-instrumentation-starlette/test-requirements.in -e opentelemetry-instrumentation # via # -r instrumentation/opentelemetry-instrumentation-starlette/test-requirements.in # opentelemetry-instrumentation-asgi # opentelemetry-instrumentation-starlette -e util/opentelemetry-util-http # via # -r instrumentation/opentelemetry-instrumentation-starlette/test-requirements.in # opentelemetry-instrumentation-asgi # opentelemetry-instrumentation-starlette anyio==4.8.0 # via httpx asgiref==3.8.1 # via opentelemetry-instrumentation-asgi certifi==2025.1.31 # via # httpcore # httpx # requests charset-normalizer==3.4.1 # via requests colorama==0.4.6 ; sys_platform == 'win32' # via pytest exceptiongroup==1.2.2 ; python_full_version < '3.11' # via # anyio # pytest h11==0.14.0 # via httpcore httpcore==1.0.7 # via httpx httpx==0.28.0 # via -r instrumentation/opentelemetry-instrumentation-starlette/test-requirements.in idna==3.10 # via # anyio # httpx # requests importlib-metadata==8.6.1 # via opentelemetry-api iniconfig==2.0.0 # via pytest packaging==24.0 # via # opentelemetry-instrumentation # pytest pluggy==1.6.0 # via pytest pytest==7.4.4 # via # -c dev-requirements.txt # -r instrumentation/opentelemetry-instrumentation-starlette/test-requirements.in requests==2.33.1 # via # -c dev-requirements.txt # -r instrumentation/opentelemetry-instrumentation-starlette/test-requirements.in sniffio==1.3.1 # via anyio starlette==0.13.8 # via opentelemetry-instrumentation-starlette tomli==2.2.1 ; python_full_version < '3.11' # via pytest typing-extensions==4.12.2 # via # anyio # asgiref # opentelemetry-api # opentelemetry-semantic-conventions urllib3==2.7.0 # via # -r instrumentation/opentelemetry-instrumentation-starlette/test-requirements.in # requests wrapt==1.16.0 # via opentelemetry-instrumentation zipp==3.21.0 # via importlib-metadata # The following packages were excluded from the output: # opentelemetry-api # opentelemetry-semantic-conventions opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-starlette/tests/000077500000000000000000000000001527070072300343145ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300363340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-starlette/teststest_starlette_instrumentation.py000066400000000000000000001061131527070072300432020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-starlette/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from timeit import default_timer from unittest.mock import patch from starlette import applications from starlette.responses import PlainTextResponse from starlette.routing import Host, Mount, Route from starlette.testclient import TestClient from starlette.websockets import WebSocket import opentelemetry.instrumentation.starlette as otel_starlette from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, NumberDataPoint, ) from opentelemetry.sdk.resources import Resource from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_FLAVOR, HTTP_ROUTE, HTTP_TARGET, HTTP_URL, ) from opentelemetry.test.globals_test import reset_trace_globals from opentelemetry.test.test_base import TestBase from opentelemetry.trace import ( NoOpTracerProvider, SpanKind, get_tracer, set_tracer_provider, ) from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, _active_requests_count_attrs, _duration_attrs, get_excluded_urls, ) _expected_metric_names = [ "http.server.active_requests", "http.server.duration", "http.server.response.size", "http.server.request.size", ] _recommended_attrs = { "http.server.active_requests": _active_requests_count_attrs, "http.server.duration": _duration_attrs, "http.server.response.size": _duration_attrs, "http.server.request.size": _duration_attrs, } SCOPE = "opentelemetry.instrumentation.starlette" class TestStarletteManualInstrumentation(TestBase): def _create_app(self): app = self._create_starlette_app() self._instrumentor.instrument_app( app=app, server_request_hook=getattr(self, "server_request_hook", None), client_request_hook=getattr(self, "client_request_hook", None), client_response_hook=getattr(self, "client_response_hook", None), ) return app def setUp(self): super().setUp() self.env_patch = patch.dict( "os.environ", {"OTEL_PYTHON_STARLETTE_EXCLUDED_URLS": "/exclude/123,healthzz"}, ) self.env_patch.start() self.exclude_patch = patch( "opentelemetry.instrumentation.starlette._excluded_urls", get_excluded_urls("STARLETTE"), ) self.exclude_patch.start() self._instrumentor = otel_starlette.StarletteInstrumentor() self._app = self._create_app() self._client = TestClient(self._app) def tearDown(self): super().tearDown() self.env_patch.stop() self.exclude_patch.stop() def test_basic_starlette_call(self): self._client.get("/foobar") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) for span in spans: self.assertIn("GET /foobar", span.name) self.assertEqual( span.instrumentation_scope.name, "opentelemetry.instrumentation.starlette", ) def test_sub_app_starlette_call(self): """ This test is to ensure that a span in case of a sub app targeted contains the correct server url """ self._client.get("/sub/home") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) for span in spans: # As we are only looking to the "outer" app, we would see only the "GET /sub" spans self.assertIn("GET /sub", span.name) # We now want to specifically test all spans including the # - HTTP_TARGET # - HTTP_URL # attributes to be populated with the expected values spans_with_http_attributes = [ span for span in spans if (HTTP_URL in span.attributes or HTTP_TARGET in span.attributes) ] # expect only one span to have the attributes self.assertEqual(1, len(spans_with_http_attributes)) for span in spans_with_http_attributes: self.assertEqual("/sub/home", span.attributes[HTTP_TARGET]) self.assertEqual( "http://testserver/sub/home", span.attributes[HTTP_URL], ) def test_host_starlette_call(self): client = TestClient(self._app, base_url="http://testserver2") client.get("/home") spans = self.memory_exporter.get_finished_spans() spans_with_http_attributes = [ span for span in spans if (HTTP_URL in span.attributes or HTTP_TARGET in span.attributes) ] for span in spans_with_http_attributes: self.assertEqual("/home", span.attributes[HTTP_TARGET]) self.assertEqual( "http://testserver2/home", span.attributes[HTTP_URL], ) def test_starlette_route_attribute_added(self): """Ensure that starlette routes are used as the span name.""" self._client.get("/user/123") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) for span in spans: self.assertIn("GET /user/{username}", span.name) self.assertEqual(spans[-1].attributes[HTTP_ROUTE], "/user/{username}") # ensure that at least one attribute that is populated by # the asgi instrumentation is successfully feeding though. self.assertEqual(spans[-1].attributes[HTTP_FLAVOR], "1.1") def test_starlette_excluded_urls(self): """Ensure that given starlette routes are excluded.""" self._client.get("/healthzz") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_starlette_metrics(self): self._client.get("/foobar") self._client.get("/foobar") self._client.get("/foobar") number_data_point_seen = False histogram_data_point_seen = False metrics = self.get_sorted_metrics(SCOPE) self.assertTrue(len(metrics) == 3) for metric in metrics: self.assertIn(metric.name, _expected_metric_names) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 3) histogram_data_point_seen = True if isinstance(point, NumberDataPoint): number_data_point_seen = True for attr in point.attributes: self.assertIn(attr, _recommended_attrs[metric.name]) self.assertTrue(number_data_point_seen and histogram_data_point_seen) def test_basic_post_request_metric_success(self): start = default_timer() expected_duration_attributes = { "http.flavor": "1.1", "http.host": "testserver", "http.method": "POST", "http.scheme": "http", "http.server_name": "testserver", "http.status_code": 405, "net.host.port": 80, } expected_requests_count_attributes = { "http.flavor": "1.1", "http.host": "testserver", "http.method": "POST", "http.scheme": "http", "http.server_name": "testserver", } response = self._client.post( "/foobar", json={"foo": "bar"}, ) duration = max(round((default_timer() - start) * 1000), 0) response_size = int(response.headers.get("content-length")) request_size = int(response.request.headers.get("content-length")) metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) self.assertDictEqual(dict(point.attributes), expected_duration_attributes) if metric.name == "http.server.duration": self.assertAlmostEqual(duration, point.sum, delta=350) elif metric.name == "http.server.response.size": self.assertEqual(response_size, point.sum) elif metric.name == "http.server.request.size": self.assertEqual(request_size, point.sum) if isinstance(point, NumberDataPoint): self.assertDictEqual( expected_requests_count_attributes, dict(point.attributes), ) self.assertEqual(point.value, 0) def test_metric_for_uninstrment_app_method(self): self._client.get("/foobar") # uninstrumenting the existing client app self._instrumentor.uninstrument_app(self._app) self._client.get("/foobar") self._client.get("/foobar") metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) if isinstance(point, NumberDataPoint): self.assertEqual(point.value, 0) def test_metric_uninstrument_inherited_by_base(self): # instrumenting class and creating app to send request self._instrumentor.instrument() app = self._create_starlette_app() client = TestClient(app) client.get("/foobar") # calling uninstrument and checking for telemetry data self._instrumentor.uninstrument() client.get("/foobar") client.get("/foobar") client.get("/foobar") metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) if isinstance(point, NumberDataPoint): self.assertEqual(point.value, 0) @staticmethod def _create_starlette_app(): def home(_): return PlainTextResponse("hi") def health(_): return PlainTextResponse("ok") def sub_home(_): return PlainTextResponse("sub hi") sub_app = applications.Starlette(routes=[Route("/home", sub_home)]) app = applications.Starlette( routes=[ Route("/foobar", home), Route("/user/{username}", home), Route("/healthzz", health), Mount("/sub", app=sub_app), Host("testserver2", sub_app), ], ) return app class TestStarletteManualInstrumentationHooks(TestStarletteManualInstrumentation): _server_request_hook = None _client_request_hook = None _client_response_hook = None def server_request_hook(self, span, scope): if self._server_request_hook is not None: self._server_request_hook(span, scope) def client_request_hook(self, receive_span, scope, message): if self._client_request_hook is not None: self._client_request_hook(receive_span, scope, message) def client_response_hook(self, send_span, scope, message): if self._client_response_hook is not None: self._client_response_hook(send_span, scope, message) def test_hooks(self): def server_request_hook(span, scope): span.update_name("name from server hook") def client_request_hook(receive_span, scope, message): receive_span.update_name("name from client hook") receive_span.set_attribute("attr-from-request-hook", "set") def client_response_hook(send_span, scope, message): send_span.update_name("name from response hook") send_span.set_attribute("attr-from-response-hook", "value") self._server_request_hook = server_request_hook self._client_request_hook = client_request_hook self._client_response_hook = client_response_hook self._client.get("/foobar") spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 3) # 1 server span and 2 response spans (response start and body) server_span = spans[2] self.assertEqual(server_span.name, "name from server hook") response_spans = spans[:2] for span in response_spans: self.assertEqual(span.name, "name from response hook") self.assertSpanHasAttributes(span, {"attr-from-response-hook": "value"}) class TestAutoInstrumentation(TestStarletteManualInstrumentation): """Test the auto-instrumented variant Extending the manual instrumentation as most test cases apply to both. """ def _create_app(self): # instrumentation is handled by the instrument call resource = Resource.create({"key1": "value1", "key2": "value2"}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result self.memory_exporter = exporter self._instrumentor.instrument(tracer_provider=tracer_provider) return self._create_starlette_app() def tearDown(self): self._instrumentor.uninstrument() super().tearDown() def test_request(self): self._client.get("/foobar") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) for span in spans: self.assertEqual(span.resource.attributes["key1"], "value1") self.assertEqual(span.resource.attributes["key2"], "value2") def test_uninstrument(self): self._client.get("/foobar") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) self.memory_exporter.clear() self._instrumentor.uninstrument() self._client.get("/foobar") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_no_op_tracer_provider(self): self._client.get("/foobar") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) self.memory_exporter.clear() self._instrumentor.uninstrument() tracer_provider = NoOpTracerProvider() self._instrumentor.instrument(tracer_provider=tracer_provider) self._client.get("/foobar") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) def test_manual_instrument_is_noop(self): app = self._create_starlette_app() self._instrumentor.instrument_app(app) self.assertEqual(len(app.user_middleware), 1) client = TestClient(app) client.get("/foobar") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) def test_sub_app_starlette_call(self): """ !!! Attention: we need to override this testcase for the auto-instrumented variant The reason is, that with auto instrumentation, the sub app is instrumented as well and therefore we would see the spans for the sub app as well This test is to ensure that a span in case of a sub app targeted contains the correct server url """ self._client.get("/sub/home") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 6) for span in spans: # As we are only looking to the "outer" app, we would see only the "GET /sub" spans # -> the outer app is not aware of the sub_apps internal routes sub_in = "GET /sub" in span.name # The sub app spans are named GET /home as from the sub app perspective the request targets /home # -> the sub app is technically not aware of the /sub prefix home_in = "GET /home" in span.name # We expect the spans to be either from the outer app or the sub app self.assertTrue( sub_in or home_in, f"Span {span.name} does not have /sub or /home in its name", ) # We now want to specifically test all spans including the # - HTTP_TARGET # - HTTP_URL # attributes to be populated with the expected values spans_with_http_attributes = [ span for span in spans if (HTTP_URL in span.attributes or HTTP_TARGET in span.attributes) ] # We now expect spans with attributes from both the app and its sub app self.assertEqual(2, len(spans_with_http_attributes)) # Due to a potential bug in starlettes handling of sub app mounts, we can # check only the server kind spans for the correct attributes # The internal one generated by the sub app is not yet producing the correct attributes server_span = next( (span for span in spans_with_http_attributes if span.kind == SpanKind.SERVER), None, ) self.assertIsNotNone(server_span) # As soon as the bug is fixed for starlette, we can iterate over spans_with_http_attributes here # to verify the correctness of the attributes for the internal span as well self.assertEqual("/sub/home", server_span.attributes[HTTP_TARGET]) self.assertEqual( "http://testserver/sub/home", server_span.attributes[HTTP_URL], ) class TestAutoInstrumentationHooks(TestStarletteManualInstrumentationHooks): """ Test the auto-instrumented variant for request and response hooks """ def _create_app(self): # instrumentation is handled by the instrument call self._instrumentor.instrument( server_request_hook=getattr(self, "server_request_hook", None), client_request_hook=getattr(self, "client_request_hook", None), client_response_hook=getattr(self, "client_response_hook", None), ) return self._create_starlette_app() def tearDown(self): self._instrumentor.uninstrument() super().tearDown() def test_sub_app_starlette_call(self): """ !!! Attention: we need to override this testcase for the auto-instrumented variant The reason is, that with auto instrumentation, the sub app is instrumented as well and therefore we would see the spans for the sub app as well This test is to ensure that a span in case of a sub app targeted contains the correct server url """ self._client.get("/sub/home") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 6) for span in spans: # As we are only looking to the "outer" app, we would see only the "GET /sub" spans # -> the outer app is not aware of the sub_apps internal routes sub_in = "GET /sub" in span.name # The sub app spans are named GET /home as from the sub app perspective the request targets /home # -> the sub app is technically not aware of the /sub prefix home_in = "GET /home" in span.name # We expect the spans to be either from the outer app or the sub app self.assertTrue( sub_in or home_in, f"Span {span.name} does not have /sub or /home in its name", ) # We now want to specifically test all spans including the # - HTTP_TARGET # - HTTP_URL # attributes to be populated with the expected values spans_with_http_attributes = [ span for span in spans if (HTTP_URL in span.attributes or HTTP_TARGET in span.attributes) ] # We now expect spans with attributes from both the app and its sub app self.assertEqual(2, len(spans_with_http_attributes)) # Due to a potential bug in starlettes handling of sub app mounts, we can # check only the server kind spans for the correct attributes # The internal one generated by the sub app is not yet producing the correct attributes server_span = next( (span for span in spans_with_http_attributes if span.kind == SpanKind.SERVER), None, ) self.assertIsNotNone(server_span) # As soon as the bug is fixed for starlette, we can iterate over spans_with_http_attributes here # to verify the correctness of the attributes for the internal span as well self.assertEqual("/sub/home", server_span.attributes[HTTP_TARGET]) self.assertEqual( "http://testserver/sub/home", server_span.attributes[HTTP_URL], ) class TestAutoInstrumentationLogic(unittest.TestCase): def test_instrumentation(self): """Verify that instrumentation methods are instrumenting and removing as expected. """ instrumentor = otel_starlette.StarletteInstrumentor() original = applications.Starlette instrumentor.instrument() try: instrumented = applications.Starlette self.assertIsNot(original, instrumented) finally: instrumentor.uninstrument() should_be_original = applications.Starlette self.assertIs(original, should_be_original) class TestConditonalServerSpanCreation(TestStarletteManualInstrumentation): def test_mark_span_internal_in_presence_of_another_span(self): tracer = get_tracer(__name__) with tracer.start_as_current_span("test", kind=SpanKind.SERVER) as parent_span: self._client.get("/foobar") spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) starlette_span = spans[2] self.assertEqual(SpanKind.INTERNAL, starlette_span.kind) self.assertEqual(SpanKind.SERVER, parent_span.kind) self.assertEqual(parent_span.context.span_id, starlette_span.parent.span_id) class TestBaseWithCustomHeaders(TestBase): def create_app(self): app = self.create_starlette_app() self._instrumentor.instrument_app(app=app) return app def setUp(self): super().setUp() self._instrumentor = otel_starlette.StarletteInstrumentor() self._app = self.create_app() self._client = TestClient(self._app) def tearDown(self) -> None: super().tearDown() with self.disable_logging(): self._instrumentor.uninstrument() @staticmethod def create_starlette_app(): app = applications.Starlette() @app.route("/foobar") def _foobar(request): return PlainTextResponse( content="hi", headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", "my-custom-regex-header-1": "my-custom-regex-value-1,my-custom-regex-value-2", "My-Custom-Regex-Header-2": "my-custom-regex-value-3,my-custom-regex-value-4", "my-secret-header": "my-secret-value", }, ) @app.websocket_route("/foobar_web") async def _foobar_web(websocket: WebSocket) -> None: message = await websocket.receive() if message.get("type") == "websocket.connect": await websocket.send( { "type": "websocket.accept", "headers": [ (b"custom-test-header-1", b"test-header-value-1"), (b"custom-test-header-2", b"test-header-value-2"), ( b"my-custom-regex-header-1", b"my-custom-regex-value-1,my-custom-regex-value-2", ), ( b"My-Custom-Regex-Header-2", b"my-custom-regex-value-3,my-custom-regex-value-4", ), (b"my-secret-header", b"my-secret-value"), ], } ) await websocket.send_json({"message": "hello world"}) await websocket.close() if message.get("type") == "websocket.disconnect": pass return app class TestHTTPAppWithCustomHeaders(TestBaseWithCustomHeaders): def setUp(self): self.test_env_patch = patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", }, ) self.test_env_patch.start() super().setUp() def tearDown(self): self.test_env_patch.stop() super().tearDown() def test_custom_request_headers_in_span_attributes(self): expected = { "http.request.header.custom_test_header_1": ("test-header-value-1",), "http.request.header.custom_test_header_2": ("test-header-value-2",), "http.request.header.regex_test_header_1": ("Regex Test Value 1",), "http.request.header.regex_test_header_2": ("RegexTestValue2,RegexTestValue3",), "http.request.header.my_secret_header": ("[REDACTED]",), } resp = self._client.get( "/foobar", headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", "Regex-Test-Header-1": "Regex Test Value 1", "regex-test-header-2": "RegexTestValue2,RegexTestValue3", "My-Secret-Header": "My Secret Value", }, ) self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) server_span = [span for span in span_list if span.kind == SpanKind.SERVER][0] self.assertSpanHasAttributes(server_span, expected) @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", }, ) def test_custom_request_headers_not_in_span_attributes(self): not_expected = { "http.request.header.custom_test_header_3": ("test-header-value-3",), } resp = self._client.get( "/foobar", headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", "Regex-Test-Header-1": "Regex Test Value 1", "regex-test-header-2": "RegexTestValue2,RegexTestValue3", "My-Secret-Header": "My Secret Value", }, ) self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) server_span = [span for span in span_list if span.kind == SpanKind.SERVER][0] for key in not_expected: self.assertNotIn(key, server_span.attributes) def test_custom_response_headers_in_span_attributes(self): expected = { "http.response.header.custom_test_header_1": ("test-header-value-1",), "http.response.header.custom_test_header_2": ("test-header-value-2",), "http.response.header.my_custom_regex_header_1": ("my-custom-regex-value-1,my-custom-regex-value-2",), "http.response.header.my_custom_regex_header_2": ("my-custom-regex-value-3,my-custom-regex-value-4",), "http.response.header.my_secret_header": ("[REDACTED]",), } resp = self._client.get("/foobar") self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) server_span = [span for span in span_list if span.kind == SpanKind.SERVER][0] self.assertSpanHasAttributes(server_span, expected) def test_custom_response_headers_not_in_span_attributes(self): not_expected = { "http.response.header.custom_test_header_3": ("test-header-value-3",), } resp = self._client.get("/foobar") self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) server_span = [span for span in span_list if span.kind == SpanKind.SERVER][0] for key in not_expected: self.assertNotIn(key, server_span.attributes) class TestWebSocketAppWithCustomHeaders(TestBaseWithCustomHeaders): def setUp(self): self.test_env_patch = patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", }, ) self.test_env_patch.start() super().setUp() def tearDown(self): self.test_env_patch.stop() super().tearDown() def test_custom_request_headers_in_span_attributes(self): expected = { "http.request.header.custom_test_header_1": ("test-header-value-1",), "http.request.header.custom_test_header_2": ("test-header-value-2",), "http.request.header.regex_test_header_1": ("Regex Test Value 1",), "http.request.header.regex_test_header_2": ("RegexTestValue2,RegexTestValue3",), "http.request.header.my_secret_header": ("[REDACTED]",), } with self._client.websocket_connect( "/foobar_web", headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", "Regex-Test-Header-1": "Regex Test Value 1", "regex-test-header-2": "RegexTestValue2,RegexTestValue3", "My-Secret-Header": "My Secret Value", }, ) as websocket: data = websocket.receive_json() self.assertEqual(data, {"message": "hello world"}) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 5) server_span = [span for span in span_list if span.kind == SpanKind.SERVER][0] self.assertSpanHasAttributes(server_span, expected) def test_custom_request_headers_not_in_span_attributes(self): not_expected = { "http.request.header.custom_test_header_3": ("test-header-value-3",), } with self._client.websocket_connect( "/foobar_web", headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", "Regex-Test-Header-1": "Regex Test Value 1", "regex-test-header-2": "RegexTestValue2,RegexTestValue3", "My-Secret-Header": "My Secret Value", }, ) as websocket: data = websocket.receive_json() self.assertEqual(data, {"message": "hello world"}) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 5) server_span = [span for span in span_list if span.kind == SpanKind.SERVER][0] for key, _ in not_expected.items(): self.assertNotIn(key, server_span.attributes) def test_custom_response_headers_in_span_attributes(self): expected = { "http.response.header.custom_test_header_1": ("test-header-value-1",), "http.response.header.custom_test_header_2": ("test-header-value-2",), "http.response.header.my_custom_regex_header_1": ("my-custom-regex-value-1,my-custom-regex-value-2",), "http.response.header.my_custom_regex_header_2": ("my-custom-regex-value-3,my-custom-regex-value-4",), "http.response.header.my_secret_header": ("[REDACTED]",), } with self._client.websocket_connect("/foobar_web") as websocket: data = websocket.receive_json() self.assertEqual(data, {"message": "hello world"}) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 5) server_span = [span for span in span_list if span.kind == SpanKind.SERVER][0] self.assertSpanHasAttributes(server_span, expected) def test_custom_response_headers_not_in_span_attributes(self): not_expected = { "http.response.header.custom_test_header_3": ("test-header-value-3",), } with self._client.websocket_connect("/foobar_web") as websocket: data = websocket.receive_json() self.assertEqual(data, {"message": "hello world"}) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 5) server_span = [span for span in span_list if span.kind == SpanKind.SERVER][0] for key, _ in not_expected.items(): self.assertNotIn(key, server_span.attributes) class TestNonRecordingSpanWithCustomHeaders(TestBaseWithCustomHeaders): def setUp(self): super().setUp() self.test_env_patch = patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", }, ) self.test_env_patch.start() reset_trace_globals() set_tracer_provider(tracer_provider=NoOpTracerProvider()) self._app = self.create_app() self._client = TestClient(self._app) def tearDown(self): self.test_env_patch.stop() super().tearDown() def test_custom_header_not_present_in_non_recording_span(self): resp = self._client.get( "/foobar", headers={ "custom-test-header-1": "test-header-value-1", }, ) self.assertEqual(200, resp.status_code) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-structlog/000077500000000000000000000000001527070072300331715ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-structlog/LICENSE000066400000000000000000000261351527070072300342050ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000012031527070072300345750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-structlogOpenTelemetry structlog integration =================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-structlog.svg :target: https://pypi.org/project/opentelemetry-instrumentation-structlog/ Installation ------------ :: pip install opentelemetry-instrumentation-structlog References ---------- * `OpenTelemetry Python Contrib documentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031521527070072300360270ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-structlog[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-structlog" dynamic = ["version"] description = "OpenTelemetry structlog instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", ] [project.optional-dependencies] instruments = [ "structlog >= 21.1", ] [project.entry-points.opentelemetry_instrumentor] structlog = "opentelemetry.instrumentation.structlog:StructlogInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-structlog" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/structlog/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-structlog/src/000077500000000000000000000000001527070072300337605ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300365755ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-structlog/srcinstrumentation/000077500000000000000000000000001527070072300420405ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-structlog/src/opentelemetrystructlog/000077500000000000000000000000001527070072300440665ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-structlog/src/opentelemetry/instrumentation__init__.py000066400000000000000000000374771527070072300462210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-structlog/src/opentelemetry/instrumentation/structlog# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The OpenTelemetry structlog integration provides a processor that emits structlog events as OpenTelemetry logs. .. code-block:: python import structlog from opentelemetry.instrumentation.structlog import StructlogInstrumentor StructlogInstrumentor().instrument() logger = structlog.get_logger() logger.info("user logged in", user_id=42) This will emit the structlog event as an OpenTelemetry LogRecord, preserving all context including trace context, custom attributes, and exception information. """ import sys import traceback from collections.abc import Callable, Collection from datetime import datetime from time import time_ns from typing import Any, Optional import structlog from opentelemetry._logs import ( LogRecord, NoOpLogger, SeverityNumber, get_logger, get_logger_provider, ) from opentelemetry.context import get_current from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.log_utils import std_to_otel from opentelemetry.instrumentation.structlog.package import _instruments from opentelemetry.semconv.attributes import ( exception_attributes, ) # Reserved keys that structlog uses internally and should not be passed as attributes _STRUCTLOG_RESERVED_KEYS = frozenset( { "event", "level", "timestamp", "exc_info", "exception", "_record", "_logger", "_name", } ) # Map structlog level names to standard library log level numbers _STRUCTLOG_LEVEL_TO_LEVELNO = { "debug": 10, "info": 20, "warning": 30, "warn": 30, "error": 40, "critical": 50, "fatal": 50, } # Map structlog level names to OTel canonical severity text where they differ _STRUCTLOG_TO_OTEL_SEVERITY_TEXT = { "warning": "WARN", "critical": "FATAL", "fatal": "FATAL", } def _parse_structlog_timestamp(value: Any) -> int | None: """ Convert a structlog timestamp value to nanoseconds since epoch, or None. structlog's TimeStamper emits either a float (UNIX seconds, the default) or a string (ISO 8601 when fmt="iso", or a strftime pattern otherwise). We handle float and timezone-aware ISO 8601; anything else returns None so the SDK can fill in the observed time. """ if value is None: return None if isinstance(value, (int, float)): return int(value * 1e9) if isinstance(value, str): timestamp = value if timestamp.endswith("Z"): timestamp = timestamp[:-1] + "+00:00" try: dt = datetime.fromisoformat(timestamp) if dt.tzinfo is None: return None return int(dt.timestamp() * 1e9) except ValueError: return None return None class StructlogProcessor: """ A structlog processor that translates structlog events into OpenTelemetry LogRecords. This processor should be added to the structlog processor chain to emit logs to OpenTelemetry. It translates structlog's event dictionary format into the OpenTelemetry Logs data model. Args: logger_provider: The LoggerProvider to use. If None, uses the global provider. """ def __init__(self, logger_provider=None): """Initialize the processor with an optional logger provider.""" self._logger_provider = logger_provider or get_logger_provider() def __call__(self, logger, method_name: str, event_dict: dict) -> dict: """ Process a structlog event and emit it as an OpenTelemetry log. This method implements the structlog processor interface. It receives the event dictionary, translates it to an OTel LogRecord, and emits it. Args: logger: The wrapped structlog logger. method_name: The logger method name. event_dict: The structlog event dictionary. Returns: The unmodified event_dict (passthrough for other processors). """ logger_name = getattr(logger, "name", __name__) otel_logger = get_logger(logger_name, logger_provider=self._logger_provider) # Skip emission if we have a no-op logger if not isinstance(otel_logger, NoOpLogger): log_record = self._translate(event_dict, method_name) otel_logger.emit(log_record) return event_dict @staticmethod def _get_attributes(event_dict: dict) -> dict[str, Any]: """ Extract attributes from the structlog event dictionary. Filters out reserved keys and extracts exception information into the appropriate semantic convention attributes. Args: event_dict: The structlog event dictionary. Returns: Dictionary of attributes to attach to the LogRecord. """ # Start with all non-reserved keys attributes = {k: v for k, v in event_dict.items() if k not in _STRUCTLOG_RESERVED_KEYS} # Handle exception information exc_info = event_dict.get("exc_info") # Match True explicitly because exception tuples and exception instances # are also truthy and are handled separately below. if exc_info is True: # exc_info=True means "get current exception" exc_info = sys.exc_info() elif isinstance(exc_info, BaseException): # exc_info can also be passed as an exception instance directly exc_info = (type(exc_info), exc_info, exc_info.__traceback__) if isinstance(exc_info, tuple) and len(exc_info) == 3: exctype, value, tb = exc_info if exctype is not None: attributes[exception_attributes.EXCEPTION_TYPE] = exctype.__name__ if value is not None and value.args: attributes[exception_attributes.EXCEPTION_MESSAGE] = str(value.args[0]) if tb is not None: attributes[exception_attributes.EXCEPTION_STACKTRACE] = "".join(traceback.format_exception(*exc_info)) # Handle pre-rendered exception string (from structlog's ExceptionRenderer) exception_str = event_dict.get("exception") if isinstance(exception_str, str): # If we don't already have a stacktrace from exc_info, use this if exception_attributes.EXCEPTION_STACKTRACE not in attributes: attributes[exception_attributes.EXCEPTION_STACKTRACE] = exception_str return attributes def _translate(self, event_dict: dict, method_name: str | None = None) -> LogRecord: """ Translate a structlog event dictionary into an OpenTelemetry LogRecord. Args: event_dict: The structlog event dictionary. Returns: An OpenTelemetry LogRecord. """ # observed_timestamp is when the SDK received the event (always now). # timestamp is when the event occurred; use the structlog "timestamp" # field if present and parseable (UNIX float or ISO 8601 string), # otherwise leave as None and let the SDK fill it in. observed_timestamp = time_ns() timestamp = _parse_structlog_timestamp(event_dict.get("timestamp")) # Get the log level and map to OTel severity. structlog passes the # logger method name to processors, so use it as a fallback when no # prior processor added a level to the event dict. level_str = event_dict.get("level") if not isinstance(level_str, str) or not level_str: level_str = method_name level_name = level_str.lower() if isinstance(level_str, str) else None levelno = _STRUCTLOG_LEVEL_TO_LEVELNO.get(level_name) if level_name is not None else None if levelno is None: severity_number = SeverityNumber.UNSPECIFIED severity_text = level_str.upper() if isinstance(level_str, str) else None else: severity_number = std_to_otel(levelno) # Normalize severity text to OTel canonical names where structlog # level names differ: "warning" -> "WARN", "critical"/"fatal" -> "FATAL" severity_text = _STRUCTLOG_TO_OTEL_SEVERITY_TEXT.get(level_name, level_str.upper()) # Get the message body body = event_dict.get("event") # Get attributes (filters reserved keys and extracts exception info) attributes = self._get_attributes(event_dict) # Get the current OTel context (includes trace context and baggage) context = get_current() return LogRecord( timestamp=timestamp, observed_timestamp=observed_timestamp, context=context, severity_text=severity_text, severity_number=severity_number, body=body, attributes=attributes, ) def flush(self) -> None: """ Flush the logger provider. """ if hasattr(self._logger_provider, "force_flush") and callable(self._logger_provider.force_flush): self._logger_provider.force_flush() class StructlogInstrumentor(BaseInstrumentor): """ An instrumentor for the structlog logging library. This instrumentor adds a StructlogProcessor to the structlog processor chain, enabling automatic emission of structlog events as OpenTelemetry logs. Example: >>> from opentelemetry.instrumentation.structlog import ( ... StructlogInstrumentor, ... ) >>> import structlog >>> StructlogInstrumentor().instrument() >>> logger = structlog.get_logger() >>> logger.info("hello", user="alice") """ _processor: Optional["StructlogProcessor"] = None _original_configure: Callable[..., None] = structlog.configure _original_configure_once: Callable[..., None] = structlog.configure_once _original_reset_defaults: Callable[..., None] = structlog.reset_defaults _is_configured_by_app: bool = False def instrumentation_dependencies(self) -> Collection[str]: """Return the required instrumentation dependencies.""" return _instruments def _instrument(self, **kwargs): """ Add the StructlogProcessor to structlog's processor chain. The processor is inserted before the last processor in the current chain. This assumes the last processor is a renderer (e.g. ConsoleRenderer, JSONRenderer). The processor must run before rendering so it receives the raw event dict rather than a formatted string. If your chain does not end with a renderer, or has post-processing steps after the renderer, configure the chain manually instead of relying on auto-instrumentation: structlog.configure(processors=[ structlog.stdlib.add_log_level, StructlogProcessor(logger_provider=provider), structlog.dev.ConsoleRenderer(), ]) Args: logger_provider: Optional LoggerProvider to use. """ # Create the OTel processor logger_provider = kwargs.get("logger_provider") processor = StructlogProcessor(logger_provider=logger_provider) # Store original configuration functions for later restoration. StructlogInstrumentor._original_configure = structlog.configure StructlogInstrumentor._original_configure_once = structlog.configure_once StructlogInstrumentor._original_reset_defaults = structlog.reset_defaults StructlogInstrumentor._is_configured_by_app = structlog.is_configured() # Get current structlog configuration config = structlog.get_config() current_processors = list(config.get("processors", [])) # Insert before the last processor, assumed to be the renderer. if current_processors: insert_position = len(current_processors) - 1 else: insert_position = 0 current_processors.insert(insert_position, processor) # Reconfigure structlog with the new processor chain StructlogInstrumentor._original_configure(processors=current_processors) # Store reference for uninstrumentation StructlogInstrumentor._processor = processor def ensure_processor(processors: Any) -> list[Any]: processors = list(processors) if not any(isinstance(p, StructlogProcessor) for p in processors): insert_position = max(len(processors) - 1, 0) processors.insert(insert_position, StructlogInstrumentor._processor) return processors def ensure_processor_in_args( args: tuple[Any, ...], kwargs: dict[str, Any] ) -> tuple[tuple[Any, ...], dict[str, Any]]: if args and "processors" not in kwargs: processors = args[0] if processors is not None: args = (ensure_processor(processors), *args[1:]) elif kwargs.get("processors") is not None: kwargs["processors"] = ensure_processor(kwargs["processors"]) return args, kwargs def patched_configure(*args: Any, **kwargs: Any) -> None: # If the user is supplying a processors list, ensure our processor # is included before passing it to the original configure. StructlogInstrumentor._is_configured_by_app = True args, kwargs = ensure_processor_in_args(args, kwargs) return StructlogInstrumentor._original_configure(*args, **kwargs) def patched_configure_once(*args: Any, **kwargs: Any) -> None: # structlog.configure_once() treats the instrumentor's internal # configure() call as prior configuration. If the app had not # configured structlog yet, allow its first configure_once() call # and inject the OTel processor into that chain. if StructlogInstrumentor._is_configured_by_app: return StructlogInstrumentor._original_configure_once(*args, **kwargs) StructlogInstrumentor._is_configured_by_app = True args, kwargs = ensure_processor_in_args(args, kwargs) return StructlogInstrumentor._original_configure(*args, **kwargs) def patched_reset_defaults(*args: Any, **kwargs: Any) -> None: StructlogInstrumentor._original_reset_defaults(*args, **kwargs) StructlogInstrumentor._is_configured_by_app = False structlog.configure = patched_configure structlog.configure_once = patched_configure_once structlog.reset_defaults = patched_reset_defaults def _uninstrument(self, **kwargs): """ Remove the StructlogProcessor from structlog's processor chain. """ # Get current structlog configuration config = structlog.get_config() current_processors = list(config.get("processors", [])) # Remove all StructlogProcessor instances new_processors = [p for p in current_processors if not isinstance(p, StructlogProcessor)] configured_by_app = StructlogInstrumentor._is_configured_by_app # Restore the original structlog.configure before reconfiguring so # the patched version does not re-insert the processor. structlog.configure = StructlogInstrumentor._original_configure structlog.configure_once = StructlogInstrumentor._original_configure_once structlog.reset_defaults = StructlogInstrumentor._original_reset_defaults if configured_by_app: # Reconfigure structlog without the processor while preserving the # app's configured state. structlog.configure(processors=new_processors) else: # Instrumentation was the only configure() call. Restore structlog's # unconfigured state so a later configure_once() can still run. structlog.reset_defaults() # Clear reference StructlogInstrumentor._processor = None StructlogInstrumentor._is_configured_by_app = False package.py000066400000000000000000000001641527070072300460340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-structlog/src/opentelemetry/instrumentation/structlog# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("structlog >= 21.1",) version.py000066400000000000000000000001441527070072300461240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-structlog/src/opentelemetry/instrumentation/structlog# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.in000066400000000000000000000003471527070072300371460ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-structlog# Used to generate test-requirements.{oldest,latest}.txt with uv pip compile pytest~=7.4 packaging>=21.0 wrapt>=1.16.0,<3.0.0 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-structlog[instruments] test-requirements.latest.txt000066400000000000000000000033361527070072300406530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-structlog# This file was autogenerated by uv via the following command: # uv pip compile --python 3.10 --universal --no-emit-package opentelemetry-api --no-emit-package opentelemetry-semantic-conventions -c dev-requirements.txt instrumentation/opentelemetry-instrumentation-structlog/test-requirements.in -o instrumentation/opentelemetry-instrumentation-structlog/test-requirements.latest.txt -e instrumentation/opentelemetry-instrumentation-structlog # via -r instrumentation/opentelemetry-instrumentation-structlog/test-requirements.in -e opentelemetry-instrumentation # via # -r instrumentation/opentelemetry-instrumentation-structlog/test-requirements.in # opentelemetry-instrumentation-structlog colorama==0.4.6 ; sys_platform == 'win32' # via pytest exceptiongroup==1.3.1 ; python_full_version < '3.11' # via pytest iniconfig==2.3.0 # via pytest packaging==26.2 # via # -r instrumentation/opentelemetry-instrumentation-structlog/test-requirements.in # opentelemetry-instrumentation # pytest pluggy==1.6.0 # via pytest pytest==7.4.4 # via # -c dev-requirements.txt # -r instrumentation/opentelemetry-instrumentation-structlog/test-requirements.in structlog==26.1.0 # via opentelemetry-instrumentation-structlog tomli==2.4.1 ; python_full_version < '3.11' # via pytest typing-extensions==4.15.0 # via # exceptiongroup # opentelemetry-api # opentelemetry-semantic-conventions # structlog wrapt==2.2.1 # via # -r instrumentation/opentelemetry-instrumentation-structlog/test-requirements.in # opentelemetry-instrumentation # The following packages were excluded from the output: # opentelemetry-api # opentelemetry-semantic-conventions test-requirements.oldest.txt000066400000000000000000000034151527070072300406470ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-structlog# This file was autogenerated by uv via the following command: # uv pip compile --python 3.10 --universal --resolution lowest-direct --no-emit-package opentelemetry-api --no-emit-package opentelemetry-semantic-conventions -c dev-requirements.txt instrumentation/opentelemetry-instrumentation-structlog/test-requirements.in -o instrumentation/opentelemetry-instrumentation-structlog/test-requirements.oldest.txt -e instrumentation/opentelemetry-instrumentation-structlog # via -r instrumentation/opentelemetry-instrumentation-structlog/test-requirements.in -e opentelemetry-instrumentation # via # -r instrumentation/opentelemetry-instrumentation-structlog/test-requirements.in # opentelemetry-instrumentation-structlog colorama==0.4.6 ; sys_platform == 'win32' # via pytest exceptiongroup==1.3.1 ; python_full_version < '3.11' # via pytest iniconfig==2.3.0 # via pytest packaging==21.0 # via # -r instrumentation/opentelemetry-instrumentation-structlog/test-requirements.in # opentelemetry-instrumentation # pytest pluggy==1.6.0 # via pytest pyparsing==3.3.2 # via packaging pytest==7.4.4 # via # -c dev-requirements.txt # -r instrumentation/opentelemetry-instrumentation-structlog/test-requirements.in structlog==21.1.0 # via opentelemetry-instrumentation-structlog tomli==2.4.1 ; python_full_version < '3.11' # via pytest typing-extensions==4.15.0 # via # exceptiongroup # opentelemetry-api # opentelemetry-semantic-conventions wrapt==1.16.0 # via # -r instrumentation/opentelemetry-instrumentation-structlog/test-requirements.in # opentelemetry-instrumentation # The following packages were excluded from the output: # opentelemetry-api # opentelemetry-semantic-conventions opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-structlog/tests/000077500000000000000000000000001527070072300343335ustar00rootroot00000000000000test_structlog.py000066400000000000000000000634711527070072300377260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-structlog/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import sys import time import warnings from datetime import datetime, timezone from unittest.mock import Mock import structlog from opentelemetry._logs import SeverityNumber from opentelemetry.instrumentation.structlog import ( StructlogInstrumentor, StructlogProcessor, ) from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import ( InMemoryLogRecordExporter, SimpleLogRecordProcessor, ) from opentelemetry.semconv.attributes import exception_attributes from opentelemetry.test.test_base import TestBase from opentelemetry.trace import TraceFlags class TestStructlogProcessor(TestBase): """Tests for the StructlogProcessor class.""" def setUp(self): super().setUp() # Set up in-memory log exporter self.exporter = InMemoryLogRecordExporter() self.logger_provider = LoggerProvider() self.logger_provider.add_log_record_processor(SimpleLogRecordProcessor(self.exporter)) # Configure structlog with OTel processor self.processor = StructlogProcessor(logger_provider=self.logger_provider) structlog.configure( processors=[ structlog.stdlib.add_log_level, self.processor, structlog.dev.ConsoleRenderer(), ] ) self.logger = structlog.get_logger() self.tracer = self.tracer_provider.get_tracer(__name__) def tearDown(self): super().tearDown() # Reset structlog configuration structlog.reset_defaults() self.exporter.clear() def test_basic_log_emission(self): """Test that basic logs are emitted correctly.""" self.logger.info("test message", user="alice", count=42) logs = self.exporter.get_finished_logs() self.assertEqual(len(logs), 1) log = logs[0] self.assertEqual(log.log_record.body, "test message") self.assertEqual(log.log_record.severity_text, "INFO") self.assertEqual(log.log_record.severity_number, SeverityNumber.INFO) self.assertIn("user", log.log_record.attributes) self.assertEqual(log.log_record.attributes["user"], "alice") self.assertIn("count", log.log_record.attributes) self.assertEqual(log.log_record.attributes["count"], 42) def test_debug_level(self): """Test debug level mapping.""" self.logger.debug("debug message") logs = self.exporter.get_finished_logs() self.assertEqual(len(logs), 1) log = logs[0] self.assertEqual(log.log_record.severity_text, "DEBUG") self.assertEqual(log.log_record.severity_number, SeverityNumber.DEBUG) def test_info_level(self): """Test info level mapping.""" self.logger.info("info message") logs = self.exporter.get_finished_logs() self.assertEqual(len(logs), 1) log = logs[0] self.assertEqual(log.log_record.severity_text, "INFO") self.assertEqual(log.log_record.severity_number, SeverityNumber.INFO) def test_warning_level(self): """Test warning level mapping and normalization to WARN.""" self.logger.warning("warning message") logs = self.exporter.get_finished_logs() self.assertEqual(len(logs), 1) log = logs[0] # Should be normalized to "WARN" per OTel spec self.assertEqual(log.log_record.severity_text, "WARN") self.assertEqual(log.log_record.severity_number, SeverityNumber.WARN) def test_error_level(self): """Test error level mapping.""" self.logger.error("error message") logs = self.exporter.get_finished_logs() self.assertEqual(len(logs), 1) log = logs[0] self.assertEqual(log.log_record.severity_text, "ERROR") self.assertEqual(log.log_record.severity_number, SeverityNumber.ERROR) def test_error_level_without_add_log_level(self): """Test level mapping from structlog method name.""" structlog.configure( processors=[ self.processor, structlog.dev.ConsoleRenderer(), ] ) self.logger.error("error message") logs = self.exporter.get_finished_logs() self.assertEqual(len(logs), 1) log = logs[0] self.assertEqual(log.log_record.severity_text, "ERROR") self.assertEqual(log.log_record.severity_number, SeverityNumber.ERROR) def test_unknown_level(self): """Test unknown level mapping.""" log_record = self.processor._translate({"event": "message", "level": "notice"}) self.assertEqual(log_record.severity_text, "NOTICE") self.assertEqual(log_record.severity_number, SeverityNumber.UNSPECIFIED) def test_missing_level_without_method_name(self): """Test missing level mapping without structlog method fallback.""" log_record = self.processor._translate({"event": "message"}) self.assertIsNone(log_record.severity_text) self.assertEqual(log_record.severity_number, SeverityNumber.UNSPECIFIED) def test_critical_level(self): """Test critical level mapping.""" self.logger.critical("critical message") logs = self.exporter.get_finished_logs() self.assertEqual(len(logs), 1) log = logs[0] self.assertEqual(log.log_record.severity_text, "FATAL") self.assertEqual(log.log_record.severity_number, SeverityNumber.FATAL) def test_exception_from_exc_info_tuple(self): """Test exception extraction from exc_info tuple.""" try: raise ValueError("test error") except ValueError: exc_info = sys.exc_info() self.logger.error("error occurred", exc_info=exc_info) logs = self.exporter.get_finished_logs() self.assertEqual(len(logs), 1) log = logs[0] attrs = log.log_record.attributes self.assertIn(exception_attributes.EXCEPTION_TYPE, attrs) self.assertEqual(attrs[exception_attributes.EXCEPTION_TYPE], "ValueError") self.assertIn(exception_attributes.EXCEPTION_MESSAGE, attrs) self.assertEqual(attrs[exception_attributes.EXCEPTION_MESSAGE], "test error") self.assertIn(exception_attributes.EXCEPTION_STACKTRACE, attrs) stacktrace = attrs[exception_attributes.EXCEPTION_STACKTRACE] self.assertIn("ValueError", stacktrace) self.assertIn("test error", stacktrace) def test_exception_from_exc_info_true(self): """Test exception extraction when exc_info=True.""" try: raise RuntimeError("runtime error") except RuntimeError: self.logger.error("error occurred", exc_info=True) logs = self.exporter.get_finished_logs() self.assertEqual(len(logs), 1) log = logs[0] attrs = log.log_record.attributes self.assertIn(exception_attributes.EXCEPTION_TYPE, attrs) self.assertEqual(attrs[exception_attributes.EXCEPTION_TYPE], "RuntimeError") self.assertIn(exception_attributes.EXCEPTION_MESSAGE, attrs) self.assertEqual(attrs[exception_attributes.EXCEPTION_MESSAGE], "runtime error") self.assertIn(exception_attributes.EXCEPTION_STACKTRACE, attrs) def test_exception_from_exception_instance(self): """Test exception extraction when exc_info is an Exception instance.""" try: raise KeyError("missing key") except KeyError as exc: self.logger.error("error occurred", exc_info=exc) logs = self.exporter.get_finished_logs() self.assertEqual(len(logs), 1) log = logs[0] attrs = log.log_record.attributes self.assertIn(exception_attributes.EXCEPTION_TYPE, attrs) self.assertEqual(attrs[exception_attributes.EXCEPTION_TYPE], "KeyError") self.assertIn(exception_attributes.EXCEPTION_MESSAGE, attrs) self.assertIn(exception_attributes.EXCEPTION_STACKTRACE, attrs) def test_exception_from_string(self): """Test exception from pre-rendered string (e.g., from ExceptionRenderer).""" exception_string = "Traceback (most recent call last):\n File test.py\nValueError: test" self.logger.error("error occurred", exception=exception_string) logs = self.exporter.get_finished_logs() self.assertEqual(len(logs), 1) log = logs[0] attrs = log.log_record.attributes self.assertIn(exception_attributes.EXCEPTION_STACKTRACE, attrs) self.assertEqual(attrs[exception_attributes.EXCEPTION_STACKTRACE], exception_string) def test_trace_context_with_active_span(self): """Test that trace context is captured with an active span.""" with self.tracer.start_as_current_span("test-span") as span: self.logger.info("message in span") logs = self.exporter.get_finished_logs() self.assertEqual(len(logs), 1) log = logs[0] # Context should be set self.assertIsNotNone(log.log_record.trace_id) self.assertIsNotNone(log.log_record.span_id) # Should match the current span span_context = span.get_span_context() self.assertEqual(log.log_record.trace_id, span_context.trace_id) self.assertEqual(log.log_record.span_id, span_context.span_id) self.assertEqual( log.log_record.trace_flags, TraceFlags(span_context.trace_flags), ) def test_without_active_span(self): """Test that logging works without an active span.""" self.logger.info("message without span") logs = self.exporter.get_finished_logs() self.assertEqual(len(logs), 1) log = logs[0] # Should still have a log record, just no trace context self.assertEqual(log.log_record.body, "message without span") def test_reserved_keys_filtered(self): """Test that structlog reserved keys are filtered from attributes.""" # These keys should be filtered out self.logger.info( "test", user="alice", _record="should be filtered", _logger="should be filtered", _name="should be filtered", ) logs = self.exporter.get_finished_logs() self.assertEqual(len(logs), 1) log = logs[0] attrs = log.log_record.attributes # User attribute should be present self.assertIn("user", attrs) self.assertEqual(attrs["user"], "alice") # Reserved keys should be filtered self.assertNotIn("_record", attrs) self.assertNotIn("_logger", attrs) self.assertNotIn("_name", attrs) self.assertNotIn("event", attrs) self.assertNotIn("level", attrs) self.assertNotIn("timestamp", attrs) def test_custom_attributes_pass_through(self): """Test that custom attributes are passed through correctly.""" self.logger.info( "test", user_id=123, request_id="abc-def", ip_address="192.168.1.1", duration=1.5, success=True, ) logs = self.exporter.get_finished_logs() self.assertEqual(len(logs), 1) log = logs[0] attrs = log.log_record.attributes self.assertEqual(attrs["user_id"], 123) self.assertEqual(attrs["request_id"], "abc-def") self.assertEqual(attrs["ip_address"], "192.168.1.1") self.assertEqual(attrs["duration"], 1.5) self.assertEqual(attrs["success"], True) @staticmethod def test_flush(): """Test that flush calls force_flush on the provider.""" # Create a mock provider to verify flush is called mock_provider = Mock() mock_provider.force_flush = Mock() processor = StructlogProcessor(logger_provider=mock_provider) processor.flush() # Give the thread a moment to execute time.sleep(0.1) # Verify force_flush was called mock_provider.force_flush.assert_called_once() class TestStructlogProcessorTimestamps(TestBase): """Tests for StructlogProcessor timestamp handling.""" def test_timestamp_from_unix_float(self): """Test UNIX float timestamp parsing.""" log_record = StructlogProcessor()._translate( { "event": "test", "level": "info", "timestamp": 1713806404.5, } ) self.assertEqual(log_record.timestamp, 1713806404500000000) def test_timestamp_from_utc_z_iso_string(self): """Test UTC ISO timestamp parsing with Z suffix.""" log_record = StructlogProcessor()._translate( { "event": "test", "level": "info", "timestamp": "2024-04-22T17:20:04Z", } ) expected = int(datetime(2024, 4, 22, 17, 20, 4, tzinfo=timezone.utc).timestamp() * 1e9) self.assertEqual(log_record.timestamp, expected) def test_timestamp_from_offset_iso_string(self): """Test offset-aware ISO timestamp parsing.""" log_record = StructlogProcessor()._translate( { "event": "test", "level": "info", "timestamp": "2024-04-22T10:20:04-07:00", } ) expected = int(datetime(2024, 4, 22, 17, 20, 4, tzinfo=timezone.utc).timestamp() * 1e9) self.assertEqual(log_record.timestamp, expected) def test_timestamp_from_naive_iso_string(self): """Test naive ISO timestamps are ignored.""" log_record = StructlogProcessor()._translate( { "event": "test", "level": "info", "timestamp": "2024-04-22T17:20:04", } ) self.assertIsNone(log_record.timestamp) class TestStructlogInstrumentor(TestBase): """Tests for the StructlogInstrumentor class.""" def setUp(self): super().setUp() # Store original structlog config self.original_config = structlog.get_config() # Set up in-memory log exporter self.exporter = InMemoryLogRecordExporter() self.logger_provider = LoggerProvider() self.logger_provider.add_log_record_processor(SimpleLogRecordProcessor(self.exporter)) def tearDown(self): super().tearDown() # Uninstrument if needed if StructlogInstrumentor()._is_instrumented_by_opentelemetry: StructlogInstrumentor().uninstrument() # Reset structlog structlog.reset_defaults() self.exporter.clear() def test_instrument_adds_processor(self): """Test that instrument() adds the OTel processor to the chain.""" # Configure structlog with a simple processor chain structlog.configure( processors=[ structlog.dev.ConsoleRenderer(), ] ) # Get initial processor count initial_processors = structlog.get_config()["processors"] initial_count = len(initial_processors) # Instrument StructlogInstrumentor().instrument(logger_provider=self.logger_provider) # Check that processor was added new_processors = structlog.get_config()["processors"] self.assertEqual(len(new_processors), initial_count + 1) # Check that a StructlogProcessor is in the chain has_otel_processor = any(isinstance(p, StructlogProcessor) for p in new_processors) self.assertTrue(has_otel_processor) def test_instrument_without_prior_structlog_configuration_emits_logs(self): """Test instrument() emits logs before the app configures structlog.""" structlog.reset_defaults() StructlogInstrumentor().instrument(logger_provider=self.logger_provider) structlog.get_logger().info("zero config message") logs = self.exporter.get_finished_logs() self.assertEqual(len(logs), 1) self.assertEqual(logs[0].log_record.body, "zero config message") def test_uninstrument_removes_processor(self): """Test that uninstrument() removes the OTel processor.""" # Configure structlog structlog.configure( processors=[ structlog.dev.ConsoleRenderer(), ] ) # Instrument StructlogInstrumentor().instrument(logger_provider=self.logger_provider) # Verify processor was added config_after_instrument = structlog.get_config()["processors"] has_otel = any(isinstance(p, StructlogProcessor) for p in config_after_instrument) self.assertTrue(has_otel) # Uninstrument StructlogInstrumentor().uninstrument() # Verify processor was removed config_after_uninstrument = structlog.get_config()["processors"] has_otel = any(isinstance(p, StructlogProcessor) for p in config_after_uninstrument) self.assertFalse(has_otel) def test_double_instrument_prevented(self): """Test that double instrumentation is prevented by BaseInstrumentor.""" structlog.configure( processors=[ structlog.dev.ConsoleRenderer(), ] ) # First instrumentation StructlogInstrumentor().instrument(logger_provider=self.logger_provider) config_after_first = structlog.get_config()["processors"] count_after_first = len(config_after_first) # Second instrumentation (should be no-op) StructlogInstrumentor().instrument(logger_provider=self.logger_provider) config_after_second = structlog.get_config()["processors"] count_after_second = len(config_after_second) # Should have same number of processors self.assertEqual(count_after_first, count_after_second) def test_custom_logger_provider(self): """Test that custom logger_provider is passed to the processor.""" custom_provider = LoggerProvider() custom_exporter = InMemoryLogRecordExporter() custom_provider.add_log_record_processor(SimpleLogRecordProcessor(custom_exporter)) structlog.configure( processors=[ structlog.dev.ConsoleRenderer(), ] ) # Instrument with custom provider StructlogInstrumentor().instrument(logger_provider=custom_provider) # Log something logger = structlog.get_logger() logger.info("test message") # Verify it went to the custom exporter logs = custom_exporter.get_finished_logs() self.assertEqual(len(logs), 1) self.assertEqual(logs[0].log_record.body, "test message") def test_configure_after_instrument_preserves_processor(self): """Test that calling structlog.configure() after instrumentation preserves the processor.""" StructlogInstrumentor().instrument(logger_provider=self.logger_provider) # Simulate user code calling structlog.configure after instrumentation structlog.configure( processors=[ structlog.dev.ConsoleRenderer(), ] ) processors = structlog.get_config()["processors"] has_otel_processor = any(isinstance(p, StructlogProcessor) for p in processors) self.assertTrue(has_otel_processor) def test_configure_after_instrument_accepts_positional_processors(self): """Test that patched configure accepts positional processors.""" StructlogInstrumentor().instrument(logger_provider=self.logger_provider) structlog.configure([structlog.dev.ConsoleRenderer()]) processors = structlog.get_config()["processors"] self.assertIsInstance(processors[0], StructlogProcessor) def test_configure_after_instrument_accepts_none_processors(self): """Test that patched configure accepts processors=None.""" structlog.configure( processors=[ structlog.dev.ConsoleRenderer(), ] ) StructlogInstrumentor().instrument(logger_provider=self.logger_provider) processors_before = structlog.get_config()["processors"] structlog.configure(processors=None) processors = structlog.get_config()["processors"] self.assertEqual(processors, processors_before) self.assertTrue(any(isinstance(p, StructlogProcessor) for p in processors)) def test_configure_once_after_instrument_allows_app_configuration(self): """Test configure_once works after instrumentation configures structlog.""" StructlogInstrumentor().instrument(logger_provider=self.logger_provider) with warnings.catch_warnings(record=True) as captured_warnings: warnings.simplefilter("always") structlog.configure_once( processors=[ structlog.stdlib.add_log_level, structlog.processors.JSONRenderer(), ] ) self.assertEqual(captured_warnings, []) processors = structlog.get_config()["processors"] self.assertTrue(any(isinstance(p, StructlogProcessor) for p in processors)) self.assertTrue(any(isinstance(p, structlog.processors.JSONRenderer) for p in processors)) def test_configure_once_after_instrument_warns_on_repeat(self): """Test configure_once still warns after app configuration.""" StructlogInstrumentor().instrument(logger_provider=self.logger_provider) structlog.configure_once( processors=[ structlog.processors.JSONRenderer(), ] ) with warnings.catch_warnings(record=True) as captured_warnings: warnings.simplefilter("always") structlog.configure_once( processors=[ structlog.dev.ConsoleRenderer(), ] ) self.assertEqual(len(captured_warnings), 1) self.assertEqual(captured_warnings[0].category, RuntimeWarning) self.assertEqual( str(captured_warnings[0].message), "Repeated configuration attempted.", ) processors = structlog.get_config()["processors"] self.assertFalse(any(isinstance(p, structlog.dev.ConsoleRenderer) for p in processors)) def test_configure_once_after_preconfigured_instrument_warns(self): """Test preconfigured structlog keeps configure_once warning behavior.""" structlog.configure( processors=[ structlog.dev.ConsoleRenderer(), ] ) StructlogInstrumentor().instrument(logger_provider=self.logger_provider) with warnings.catch_warnings(record=True) as captured_warnings: warnings.simplefilter("always") structlog.configure_once( processors=[ structlog.processors.JSONRenderer(), ] ) self.assertEqual(len(captured_warnings), 1) self.assertEqual(captured_warnings[0].category, RuntimeWarning) self.assertEqual( str(captured_warnings[0].message), "Repeated configuration attempted.", ) processors = structlog.get_config()["processors"] self.assertTrue(any(isinstance(p, StructlogProcessor) for p in processors)) self.assertTrue(any(isinstance(p, structlog.dev.ConsoleRenderer) for p in processors)) self.assertFalse(any(isinstance(p, structlog.processors.JSONRenderer) for p in processors)) structlog.reset_defaults() with warnings.catch_warnings(record=True) as captured_warnings: warnings.simplefilter("always") structlog.configure_once( processors=[ structlog.processors.JSONRenderer(), ] ) self.assertEqual(captured_warnings, []) processors = structlog.get_config()["processors"] self.assertTrue(any(isinstance(p, StructlogProcessor) for p in processors)) self.assertTrue(any(isinstance(p, structlog.processors.JSONRenderer) for p in processors)) def test_uninstrument_restores_configure(self): """Test that uninstrument() restores structlog configuration APIs.""" original_configure = structlog.configure original_configure_once = structlog.configure_once original_reset_defaults = structlog.reset_defaults StructlogInstrumentor().instrument(logger_provider=self.logger_provider) self.assertIsNot(structlog.configure, original_configure) self.assertIsNot(structlog.configure_once, original_configure_once) self.assertIsNot(structlog.reset_defaults, original_reset_defaults) StructlogInstrumentor().uninstrument() self.assertIs(structlog.configure, original_configure) self.assertIs(structlog.configure_once, original_configure_once) self.assertIs(structlog.reset_defaults, original_reset_defaults) def test_uninstrument_restores_unconfigured_state(self): """Test uninstrument restores configure_once for unconfigured apps.""" self.assertFalse(structlog.is_configured()) StructlogInstrumentor().instrument(logger_provider=self.logger_provider) self.assertTrue(structlog.is_configured()) StructlogInstrumentor().uninstrument() self.assertFalse(structlog.is_configured()) with warnings.catch_warnings(record=True) as captured_warnings: warnings.simplefilter("always") structlog.configure_once( processors=[ structlog.processors.JSONRenderer(), ] ) self.assertEqual(captured_warnings, []) processors = structlog.get_config()["processors"] self.assertTrue(any(isinstance(p, structlog.processors.JSONRenderer) for p in processors)) self.assertFalse(any(isinstance(p, StructlogProcessor) for p in processors)) def test_instrumentation_dependencies(self): """Test that instrumentation_dependencies returns the correct value.""" instrumentor = StructlogInstrumentor() deps = instrumentor.instrumentation_dependencies() self.assertIn("structlog", " ".join(deps)) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-system-metrics/000077500000000000000000000000001527070072300341335ustar00rootroot00000000000000LICENSE000066400000000000000000000261351527070072300350700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-system-metrics Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000012351527070072300355440ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-system-metricsOpenTelemetry System Metrics Instrumentation ============================================ |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-system-metrics.svg :target: https://pypi.org/project/opentelemetry-instrumentation-system-metrics/ Instrumentation to collect system performance metrics. Installation ------------ :: pip install opentelemetry-instrumentation-system-metrics References ---------- * `OpenTelemetry System Metrics Instrumentation `_ * `OpenTelemetry Project `_ pyproject.toml000066400000000000000000000032401527070072300367670ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-system-metrics[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-system-metrics" dynamic = ["version"] description = "OpenTelemetry System Metrics Instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-api ~= 1.11", "psutil >= 5.9.0, < 8", ] [project.optional-dependencies] instruments = [ "psutil >= 5", ] [project.entry-points.opentelemetry_instrumentor] system_metrics = "opentelemetry.instrumentation.system_metrics:SystemMetricsInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-system-metrics" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/system_metrics/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] src/000077500000000000000000000000001527070072300346435ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-system-metricsopentelemetry/000077500000000000000000000000001527070072300375375ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-system-metrics/srcinstrumentation/000077500000000000000000000000001527070072300430025ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetrysystem_metrics/000077500000000000000000000000001527070072300460545ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation__init__.py000066400000000000000000001215701527070072300501730ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines """ Instrument to report system (CPU, memory, network) and process (CPU, memory, garbage collection) metrics. By default, the following metrics are configured: .. code:: python { "system.cpu.time": ["idle", "user", "system", "irq"], "system.cpu.utilization": ["idle", "user", "system", "irq"], "system.memory.usage": ["used", "free", "cached"], "system.memory.utilization": ["used", "free", "cached"], "system.swap.usage": ["used", "free"], "system.swap.utilization": ["used", "free"], "system.disk.io": ["read", "write"], "system.disk.operations": ["read", "write"], "system.disk.time": ["read", "write"], "system.network.dropped.packets": ["transmit", "receive"], "system.network.packets": ["transmit", "receive"], "system.network.errors": ["transmit", "receive"], "system.network.io": ["transmit", "receive"], "system.network.connections": ["family", "type"], "system.thread_count": None "process.context_switches": ["involuntary", "voluntary"], "process.cpu.time": ["user", "system"], "process.cpu.utilization": None, "process.memory.usage": None, "process.memory.virtual": None, "process.disk.io": ["read", "write"], "process.open_file_descriptor.count": None, "process.thread.count": None, "process.runtime.memory": ["rss", "vms"], "process.runtime.cpu.time": ["user", "system"], "process.runtime.gc_count": None, "cpython.gc.collections": None, "cpython.gc.collected_objects": None, "cpython.gc.uncollectable_objects": None, "process.runtime.thread_count": None, "process.runtime.cpu.utilization": None, "process.runtime.context_switches": ["involuntary", "voluntary"], } Usage ----- .. code:: python from opentelemetry.metrics import set_meter_provider from opentelemetry.instrumentation.system_metrics import SystemMetricsInstrumentor from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader exporter = ConsoleMetricExporter() set_meter_provider(MeterProvider([PeriodicExportingMetricReader(exporter)])) SystemMetricsInstrumentor().instrument() # metrics are collected asynchronously input("...") # to configure custom metrics configuration = { "system.memory.usage": ["used", "free", "cached"], "system.cpu.time": ["idle", "user", "system", "irq"], "system.network.io": ["transmit", "receive"], "process.memory.usage": None, "process.memory.virtual": None, "process.cpu.time": ["user", "system"], "process.context_switches": ["involuntary", "voluntary"], } SystemMetricsInstrumentor(config=configuration).instrument() Out-of-spec `process.runtime` prefixed metrics are deprecated and will be removed in future versions, users are encouraged to move to the `process` metrics. API --- """ from __future__ import annotations import fnmatch import gc import logging import os import sys import threading from collections.abc import Collection, Iterable from platform import python_implementation from typing import Any import psutil from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.system_metrics.environment_variables import ( OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS, ) from opentelemetry.instrumentation.system_metrics.package import _instruments from opentelemetry.instrumentation.system_metrics.version import __version__ from opentelemetry.metrics import CallbackOptions, Observation, get_meter from opentelemetry.semconv._incubating.attributes.cpython_attributes import ( CPYTHON_GC_GENERATION, ) from opentelemetry.semconv._incubating.metrics.process_metrics import ( create_process_cpu_utilization, ) _logger = logging.getLogger(__name__) _DEFAULT_CONFIG: dict[str, list[str] | None] = { "system.cpu.time": ["idle", "user", "system", "irq"], "system.cpu.utilization": ["idle", "user", "system", "irq"], "system.memory.usage": ["used", "free", "cached"], "system.memory.utilization": ["used", "free", "cached"], "system.swap.usage": ["used", "free"], "system.swap.utilization": ["used", "free"], "system.disk.io": ["read", "write"], "system.disk.operations": ["read", "write"], "system.disk.time": ["read", "write"], "system.network.dropped.packets": ["transmit", "receive"], "system.network.packets": ["transmit", "receive"], "system.network.errors": ["transmit", "receive"], "system.network.io": ["transmit", "receive"], "system.network.connections": ["family", "type"], "system.thread_count": None, "process.context_switches": ["involuntary", "voluntary"], "process.cpu.time": ["user", "system"], "process.cpu.utilization": ["user", "system"], "process.memory.usage": None, "process.memory.virtual": None, "process.open_file_descriptor.count": None, "process.thread.count": None, "process.disk.io": ["read", "write"], "process.runtime.memory": ["rss", "vms"], "process.runtime.cpu.time": ["user", "system"], "process.runtime.gc_count": None, "cpython.gc.collections": None, "cpython.gc.collected_objects": None, "cpython.gc.uncollectable_objects": None, "process.runtime.thread_count": None, "process.runtime.cpu.utilization": None, "process.runtime.context_switches": ["involuntary", "voluntary"], } if sys.platform == "darwin": # see https://github.com/giampaolo/psutil/issues/1219 _DEFAULT_CONFIG.pop("system.network.connections") def _build_default_config() -> dict[str, list[str] | None]: excluded_metrics: list[str] = [ pat.strip() for pat in os.environ.get(OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS, "").split(",") if pat.strip() ] if excluded_metrics: return { key: value for key, value in _DEFAULT_CONFIG.items() if not any(fnmatch.fnmatch(key, pat) for pat in excluded_metrics) } return _DEFAULT_CONFIG class SystemMetricsInstrumentor(BaseInstrumentor): def __init__( self, labels: dict[str, str] | None = None, config: dict[str, list[str] | None] | None = None, ): super().__init__() self._config = _build_default_config() if config is None else config self._labels = {} if labels is None else labels self._meter = None self._python_implementation = python_implementation().lower() self._proc = psutil.Process(os.getpid()) self._system_cpu_time_labels = self._labels.copy() self._system_cpu_utilization_labels = self._labels.copy() self._system_memory_usage_labels = self._labels.copy() self._system_memory_utilization_labels = self._labels.copy() self._system_swap_usage_labels = self._labels.copy() self._system_swap_utilization_labels = self._labels.copy() self._system_disk_io_labels = self._labels.copy() self._system_disk_operations_labels = self._labels.copy() self._system_disk_time_labels = self._labels.copy() self._system_disk_merged_labels = self._labels.copy() self._system_network_dropped_packets_labels = self._labels.copy() self._system_network_packets_labels = self._labels.copy() self._system_network_errors_labels = self._labels.copy() self._system_network_io_labels = self._labels.copy() self._system_network_connections_labels = self._labels.copy() self._system_thread_count_labels = self._labels.copy() self._context_switches_labels = self._labels.copy() self._cpu_time_labels = self._labels.copy() self._cpu_utilization_labels = self._labels.copy() self._memory_usage_labels = self._labels.copy() self._memory_virtual_labels = self._labels.copy() self._process_disk_io_labels = self._labels.copy() self._open_file_descriptor_count_labels = self._labels.copy() self._thread_count_labels = self._labels.copy() self._runtime_memory_labels = self._labels.copy() self._runtime_cpu_time_labels = self._labels.copy() self._runtime_gc_count_labels = self._labels.copy() self._runtime_gc_collections_labels = self._labels.copy() self._runtime_gc_collected_objects_labels = self._labels.copy() self._runtime_gc_uncollectable_objects_labels = self._labels.copy() self._runtime_thread_count_labels = self._labels.copy() self._runtime_cpu_utilization_labels = self._labels.copy() self._runtime_context_switches_labels = self._labels.copy() def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs: Any): # pylint: disable=too-many-branches,too-many-statements meter_provider = kwargs.get("meter_provider") self._meter = get_meter( __name__, __version__, meter_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) # system metrics if "system.cpu.time" in self._config: self._meter.create_observable_counter( name="system.cpu.time", callbacks=[self._get_system_cpu_time], description="System CPU time", unit="s", ) # FIXME: double check this is divided by cpu core if "system.cpu.utilization" in self._config: self._meter.create_observable_gauge( name="system.cpu.utilization", callbacks=[self._get_system_cpu_utilization], description="System CPU utilization", unit="1", ) if "system.memory.usage" in self._config: self._meter.create_observable_gauge( name="system.memory.usage", callbacks=[self._get_system_memory_usage], description="System memory usage", unit="By", ) if "system.memory.utilization" in self._config: self._meter.create_observable_gauge( name="system.memory.utilization", callbacks=[self._get_system_memory_utilization], description="System memory utilization", unit="1", ) # FIXME: system.swap is gone in favour of system.paging if "system.swap.usage" in self._config: self._meter.create_observable_gauge( name="system.swap.usage", callbacks=[self._get_system_swap_usage], description="System swap usage", unit="pages", ) if "system.swap.utilization" in self._config: self._meter.create_observable_gauge( name="system.swap.utilization", callbacks=[self._get_system_swap_utilization], description="System swap utilization", unit="1", ) # TODO Add _get_system_swap_page_faults # self._meter.create_observable_counter( # name="system.swap.page_faults", # callbacks=[self._get_system_swap_page_faults], # description="System swap page faults", # unit="faults", # value_type=int, # ) # TODO Add _get_system_swap_page_operations # self._meter.create_observable_counter( # name="system.swap.page_operations", # callbacks=self._get_system_swap_page_operations, # description="System swap page operations", # unit="operations", # value_type=int, # ) if "system.disk.io" in self._config: self._meter.create_observable_counter( name="system.disk.io", callbacks=[self._get_system_disk_io], description="System disk IO", unit="By", ) if "system.disk.operations" in self._config: self._meter.create_observable_counter( name="system.disk.operations", callbacks=[self._get_system_disk_operations], description="System disk operations", unit="operations", ) # FIXME: this has been replaced by system.disk.operation.time if "system.disk.time" in self._config: self._meter.create_observable_counter( name="system.disk.time", callbacks=[self._get_system_disk_time], description="System disk time", unit="s", ) # TODO Add _get_system_filesystem_usage # self.accumulator.register_valueobserver( # callback=self._get_system_filesystem_usage, # name="system.filesystem.usage", # description="System filesystem usage", # unit="By", # value_type=int, # ) # TODO Add _get_system_filesystem_utilization # self._meter.create_observable_gauge( # callback=self._get_system_filesystem_utilization, # name="system.filesystem.utilization", # description="System filesystem utilization", # unit="1", # value_type=float, # ) # TODO Filesystem information can be obtained with os.statvfs in Unix-like # OSs, how to do the same in Windows? # FIXME: this is now just system.network.dropped if "system.network.dropped.packets" in self._config: self._meter.create_observable_counter( name="system.network.dropped_packets", callbacks=[self._get_system_network_dropped_packets], description="System network dropped_packets", unit="packets", ) if "system.network.packets" in self._config: self._meter.create_observable_counter( name="system.network.packets", callbacks=[self._get_system_network_packets], description="System network packets", unit="packets", ) if "system.network.errors" in self._config: self._meter.create_observable_counter( name="system.network.errors", callbacks=[self._get_system_network_errors], description="System network errors", unit="errors", ) if "system.network.io" in self._config: self._meter.create_observable_counter( name="system.network.io", callbacks=[self._get_system_network_io], description="System network io", unit="By", ) if "system.network.connections" in self._config: self._meter.create_observable_up_down_counter( name="system.network.connections", callbacks=[self._get_system_network_connections], description="System network connections", unit="connections", ) # FIXME: this is gone if "system.thread_count" in self._config: self._meter.create_observable_gauge( name="system.thread_count", callbacks=[self._get_system_thread_count], description="System active threads count", ) # process metrics if "process.cpu.time" in self._config: self._meter.create_observable_counter( name="process.cpu.time", callbacks=[self._get_cpu_time], description="Total CPU seconds broken down by different states.", unit="s", ) if "process.cpu.utilization" in self._config: create_process_cpu_utilization(self._meter, callbacks=[self._get_cpu_utilization]) if "process.context_switches" in self._config and self._can_read_context_switches(): self._meter.create_observable_counter( name="process.context_switches", callbacks=[self._get_context_switches], description="Number of times the process has been context switched.", ) if "process.memory.usage" in self._config: self._meter.create_observable_up_down_counter( name="process.memory.usage", callbacks=[self._get_memory_usage], description="The amount of physical memory in use.", unit="By", ) if "process.memory.virtual" in self._config: self._meter.create_observable_up_down_counter( name="process.memory.virtual", callbacks=[self._get_memory_virtual], description="The amount of committed virtual memory.", unit="By", ) if sys.platform != "win32" and "process.open_file_descriptor.count" in self._config: self._meter.create_observable_up_down_counter( name="process.open_file_descriptor.count", callbacks=[self._get_open_file_descriptors], description="Number of file descriptors in use by the process.", ) if "process.thread.count" in self._config: self._meter.create_observable_up_down_counter( name="process.thread.count", callbacks=[self._get_thread_count], description="Process threads count.", ) if "process.disk.io" in self._config: self._meter.create_observable_counter( name="process.disk.io", callbacks=[self._get_process_disk_io], description="Disk bytes transferred for the process.", unit="By", ) # FIXME: process.runtime keys are deprecated and will be removed in subsequent releases. # When removing them, remember to clean also the callbacks and labels if "process.runtime.memory" in self._config: self._meter.create_observable_up_down_counter( name=f"process.runtime.{self._python_implementation}.memory", callbacks=[self._get_runtime_memory], description=f"Runtime {self._python_implementation} memory", unit="By", ) if "process.runtime.cpu.time" in self._config: self._meter.create_observable_counter( name=f"process.runtime.{self._python_implementation}.cpu_time", callbacks=[self._get_runtime_cpu_time], description=f"Runtime {self._python_implementation} CPU time", unit="s", ) if "process.runtime.gc_count" in self._config: if self._python_implementation == "pypy": _logger.warning( "The process.runtime.gc_count metric won't be collected because the interpreter is PyPy" ) else: self._meter.create_observable_counter( name=f"process.runtime.{self._python_implementation}.gc_count", callbacks=[self._get_runtime_gc_count], description=f"Runtime {self._python_implementation} GC count", unit="By", ) if "cpython.gc.collections" in self._config: if self._python_implementation == "pypy": _logger.warning("The cpython.gc.collections metric won't be collected because the interpreter is PyPy") else: self._meter.create_observable_counter( name="cpython.gc.collections", callbacks=[self._get_runtime_gc_collections], description="The number of times a generation was collected since interpreter start.", unit="{collection}", ) if "cpython.gc.collected_objects" in self._config: if self._python_implementation == "pypy": _logger.warning( "The cpython.gc.collected_objects metric won't be collected because the interpreter is PyPy" ) else: self._meter.create_observable_counter( name="cpython.gc.collected_objects", callbacks=[self._get_runtime_gc_collected_objects], description="The total number of objects collected since interpreter start.", unit="{object}", ) if "cpython.gc.uncollectable_objects" in self._config: if self._python_implementation == "pypy": _logger.warning( "The cpython.gc.uncollectable_objects metric won't be collected because the interpreter is PyPy" ) else: self._meter.create_observable_counter( name="cpython.gc.uncollectable_objects", callbacks=[self._get_runtime_gc_uncollectable_objects], description="The total number of uncollectable objects found since interpreter start.", unit="{object}", ) if "process.runtime.thread_count" in self._config: self._meter.create_observable_up_down_counter( name=f"process.runtime.{self._python_implementation}.thread_count", callbacks=[self._get_runtime_thread_count], description="Runtime active threads count", ) if "process.runtime.cpu.utilization" in self._config: self._meter.create_observable_gauge( name=f"process.runtime.{self._python_implementation}.cpu.utilization", callbacks=[self._get_runtime_cpu_utilization], description="Runtime CPU utilization", unit="1", ) if "process.runtime.context_switches" in self._config and self._can_read_context_switches(): self._meter.create_observable_counter( name=f"process.runtime.{self._python_implementation}.context_switches", callbacks=[self._get_runtime_context_switches], description="Runtime context switches", unit="switches", ) def _uninstrument(self, **kwargs: Any): pass def _can_read_context_switches(self) -> bool: """On Google Cloud Run psutil is not able to read context switches, catch it before creating the observable instrument""" try: self._proc.num_ctx_switches() return True except NotImplementedError: return False def _get_open_file_descriptors(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for Number of file descriptors in use by the process""" yield Observation( self._proc.num_fds(), self._open_file_descriptor_count_labels.copy(), ) def _get_system_cpu_time(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for system CPU time""" for cpu, times in enumerate(psutil.cpu_times(percpu=True)): for metric in self._config["system.cpu.time"]: if hasattr(times, metric): self._system_cpu_time_labels["state"] = metric self._system_cpu_time_labels["cpu"] = cpu + 1 yield Observation( getattr(times, metric), self._system_cpu_time_labels.copy(), ) def _get_system_cpu_utilization(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for system CPU utilization""" for cpu, times_percent in enumerate(psutil.cpu_times_percent(percpu=True)): for metric in self._config["system.cpu.utilization"]: if hasattr(times_percent, metric): self._system_cpu_utilization_labels["state"] = metric self._system_cpu_utilization_labels["cpu"] = cpu + 1 yield Observation( getattr(times_percent, metric) / 100, self._system_cpu_utilization_labels.copy(), ) def _get_system_memory_usage(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for memory usage""" virtual_memory = psutil.virtual_memory() for metric in self._config["system.memory.usage"]: self._system_memory_usage_labels["state"] = metric if hasattr(virtual_memory, metric): yield Observation( getattr(virtual_memory, metric), self._system_memory_usage_labels.copy(), ) def _get_system_memory_utilization(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for memory utilization""" system_memory = psutil.virtual_memory() for metric in self._config["system.memory.utilization"]: self._system_memory_utilization_labels["state"] = metric if hasattr(system_memory, metric): yield Observation( getattr(system_memory, metric) / system_memory.total, self._system_memory_utilization_labels.copy(), ) def _get_system_swap_usage(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for swap usage""" system_swap = psutil.swap_memory() for metric in self._config["system.swap.usage"]: self._system_swap_usage_labels["state"] = metric if hasattr(system_swap, metric): yield Observation( getattr(system_swap, metric), self._system_swap_usage_labels.copy(), ) def _get_system_swap_utilization(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for swap utilization""" system_swap = psutil.swap_memory() for metric in self._config["system.swap.utilization"]: if hasattr(system_swap, metric): self._system_swap_utilization_labels["state"] = metric yield Observation( (getattr(system_swap, metric) / system_swap.total if system_swap.total else 0), self._system_swap_utilization_labels.copy(), ) def _get_system_disk_io(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for disk IO""" for device, counters in psutil.disk_io_counters(perdisk=True).items(): for metric in self._config["system.disk.io"]: if hasattr(counters, f"{metric}_bytes"): self._system_disk_io_labels["device"] = device self._system_disk_io_labels["direction"] = metric yield Observation( getattr(counters, f"{metric}_bytes"), self._system_disk_io_labels.copy(), ) def _get_system_disk_operations(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for disk operations""" for device, counters in psutil.disk_io_counters(perdisk=True).items(): for metric in self._config["system.disk.operations"]: if hasattr(counters, f"{metric}_count"): self._system_disk_operations_labels["device"] = device self._system_disk_operations_labels["direction"] = metric yield Observation( getattr(counters, f"{metric}_count"), self._system_disk_operations_labels.copy(), ) def _get_system_disk_time(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for disk time""" for device, counters in psutil.disk_io_counters(perdisk=True).items(): for metric in self._config["system.disk.time"]: if hasattr(counters, f"{metric}_time"): self._system_disk_time_labels["device"] = device self._system_disk_time_labels["direction"] = metric yield Observation( getattr(counters, f"{metric}_time") / 1000, self._system_disk_time_labels.copy(), ) def _get_system_disk_merged(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for disk merged operations""" # FIXME The units in the spec is 1, it seems like it should be # operations or the value type should be Double for device, counters in psutil.disk_io_counters(perdisk=True).items(): for metric in self._config["system.disk.time"]: if hasattr(counters, f"{metric}_merged_count"): self._system_disk_merged_labels["device"] = device self._system_disk_merged_labels["direction"] = metric yield Observation( getattr(counters, f"{metric}_merged_count"), self._system_disk_merged_labels.copy(), ) def _get_system_network_dropped_packets(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for network dropped packets""" for device, counters in psutil.net_io_counters(pernic=True).items(): for metric in self._config["system.network.dropped.packets"]: in_out = {"receive": "in", "transmit": "out"}[metric] if hasattr(counters, f"drop{in_out}"): self._system_network_dropped_packets_labels["device"] = device self._system_network_dropped_packets_labels["direction"] = metric yield Observation( getattr(counters, f"drop{in_out}"), self._system_network_dropped_packets_labels.copy(), ) def _get_system_network_packets(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for network packets""" for device, counters in psutil.net_io_counters(pernic=True).items(): for metric in self._config["system.network.packets"]: recv_sent = {"receive": "recv", "transmit": "sent"}[metric] if hasattr(counters, f"packets_{recv_sent}"): self._system_network_packets_labels["device"] = device self._system_network_packets_labels["direction"] = metric yield Observation( getattr(counters, f"packets_{recv_sent}"), self._system_network_packets_labels.copy(), ) def _get_system_network_errors(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for network errors""" for device, counters in psutil.net_io_counters(pernic=True).items(): for metric in self._config["system.network.errors"]: in_out = {"receive": "in", "transmit": "out"}[metric] if hasattr(counters, f"err{in_out}"): self._system_network_errors_labels["device"] = device self._system_network_errors_labels["direction"] = metric yield Observation( getattr(counters, f"err{in_out}"), self._system_network_errors_labels.copy(), ) def _get_system_network_io(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for network IO""" for device, counters in psutil.net_io_counters(pernic=True).items(): for metric in self._config["system.network.io"]: recv_sent = {"receive": "recv", "transmit": "sent"}[metric] if hasattr(counters, f"bytes_{recv_sent}"): self._system_network_io_labels["device"] = device self._system_network_io_labels["direction"] = metric yield Observation( getattr(counters, f"bytes_{recv_sent}"), self._system_network_io_labels.copy(), ) def _get_system_network_connections(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for network connections""" # TODO How to find the device identifier for a particular # connection? connection_counters = {} for net_connection in psutil.net_connections(): for metric in self._config["system.network.connections"]: self._system_network_connections_labels["protocol"] = { 1: "tcp", 2: "udp", }[net_connection.type.value] self._system_network_connections_labels["state"] = net_connection.status self._system_network_connections_labels[metric] = getattr(net_connection, metric) connection_counters_key = tuple(sorted(self._system_network_connections_labels.items())) if connection_counters_key in connection_counters: connection_counters[connection_counters_key]["counter"] += 1 else: connection_counters[connection_counters_key] = { "counter": 1, "labels": self._system_network_connections_labels.copy(), } for connection_counter in connection_counters.values(): yield Observation( connection_counter["counter"], connection_counter["labels"], ) def _get_system_thread_count(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for active thread count""" yield Observation(threading.active_count(), self._system_thread_count_labels) # process callbacks def _get_context_switches(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for context switches""" ctx_switches = self._proc.num_ctx_switches() for metric in self._config["process.context_switches"]: if hasattr(ctx_switches, metric): self._context_switches_labels["type"] = metric yield Observation( getattr(ctx_switches, metric), self._context_switches_labels.copy(), ) def _get_cpu_time(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for CPU time""" proc_cpu = self._proc.cpu_times() for metric in self._config["process.cpu.time"]: if hasattr(proc_cpu, metric): self._cpu_time_labels["type"] = metric yield Observation( getattr(proc_cpu, metric), self._cpu_time_labels.copy(), ) def _get_cpu_utilization(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for CPU utilization""" proc_cpu_percent = self._proc.cpu_percent() # may return None so add a default of 1 in case num_cpus = psutil.cpu_count() or 1 yield Observation( proc_cpu_percent / 100 / num_cpus, self._cpu_utilization_labels.copy(), ) def _get_memory_usage(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for memory usage""" proc_memory = self._proc.memory_info() if hasattr(proc_memory, "rss"): yield Observation( proc_memory.rss, self._memory_usage_labels.copy(), ) def _get_memory_virtual(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for memory virtual""" proc_memory = self._proc.memory_info() if hasattr(proc_memory, "vms"): yield Observation( proc_memory.vms, self._memory_virtual_labels.copy(), ) def _get_thread_count(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for active thread count""" yield Observation(self._proc.num_threads(), self._thread_count_labels.copy()) def _get_process_disk_io(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for process disk IO""" try: proc_disk = self._proc.io_counters() except (AttributeError, NotImplementedError, PermissionError): # io_counters() is not available on all platforms (e.g. macOS) return for metric in self._config["process.disk.io"]: if metric == "read": self._process_disk_io_labels["direction"] = "read" yield Observation(proc_disk.read_bytes, self._process_disk_io_labels.copy()) elif metric == "write": self._process_disk_io_labels["direction"] = "write" yield Observation(proc_disk.write_bytes, self._process_disk_io_labels.copy()) # runtime callbacks def _get_runtime_memory(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for runtime memory""" proc_memory = self._proc.memory_info() for metric in self._config["process.runtime.memory"]: if hasattr(proc_memory, metric): self._runtime_memory_labels["type"] = metric yield Observation( getattr(proc_memory, metric), self._runtime_memory_labels.copy(), ) def _get_runtime_cpu_time(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for runtime CPU time""" proc_cpu = self._proc.cpu_times() for metric in self._config["process.runtime.cpu.time"]: if hasattr(proc_cpu, metric): self._runtime_cpu_time_labels["type"] = metric yield Observation( getattr(proc_cpu, metric), self._runtime_cpu_time_labels.copy(), ) def _get_runtime_gc_count(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for garbage collection""" for index, count in enumerate(gc.get_count()): self._runtime_gc_count_labels["count"] = str(index) yield Observation(count, self._runtime_gc_count_labels.copy()) def _get_runtime_gc_collections(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for garbage collection""" for index, stat in enumerate(gc.get_stats()): self._runtime_gc_collections_labels[CPYTHON_GC_GENERATION] = index # TODO: remove this a few releases after 1.40.0 self._runtime_gc_collections_labels["generation"] = str(index) yield Observation(stat["collections"], self._runtime_gc_collections_labels.copy()) def _get_runtime_gc_collected_objects(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for garbage collection collected objects""" for index, stat in enumerate(gc.get_stats()): self._runtime_gc_collected_objects_labels[CPYTHON_GC_GENERATION] = index # TODO: remove this a few releases after 1.40.0 self._runtime_gc_collected_objects_labels["generation"] = str(index) yield Observation( stat["collected"], self._runtime_gc_collected_objects_labels.copy(), ) def _get_runtime_gc_uncollectable_objects(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for garbage collection uncollectable objects""" for index, stat in enumerate(gc.get_stats()): self._runtime_gc_uncollectable_objects_labels[CPYTHON_GC_GENERATION] = index # TODO: remove this a few releases after 1.40.0 self._runtime_gc_uncollectable_objects_labels["generation"] = str(index) yield Observation( stat["uncollectable"], self._runtime_gc_uncollectable_objects_labels.copy(), ) def _get_runtime_thread_count(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for runtime active thread count""" yield Observation(self._proc.num_threads(), self._runtime_thread_count_labels.copy()) def _get_runtime_cpu_utilization(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for runtime CPU utilization""" proc_cpu_percent = self._proc.cpu_percent() yield Observation( proc_cpu_percent / 100, self._runtime_cpu_utilization_labels.copy(), ) def _get_runtime_context_switches(self, options: CallbackOptions) -> Iterable[Observation]: """Observer callback for runtime context switches""" ctx_switches = self._proc.num_ctx_switches() for metric in self._config["process.runtime.context_switches"]: if hasattr(ctx_switches, metric): self._runtime_context_switches_labels["type"] = metric yield Observation( getattr(ctx_switches, metric), self._runtime_context_switches_labels.copy(), ) environment_variables.py000066400000000000000000000022601527070072300530220ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS = "OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS" """ .. envvar:: OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS Specifies which system and process metrics should be excluded from collection when using the default configuration. The value should be provided as a comma separated list of glob patterns that match metric names to exclude. **Example Usage:** To exclude all CPU related metrics and specific process metrics: .. code:: bash export OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS="system.cpu.*,process.memory.*" To exclude a specific metric: .. code:: bash export OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS="system.network.io" **Supported Glob Patterns:** The environment variable supports standard glob patterns for metric filtering: - ``*`` - Matches any sequence of characters within a metric name **Example Patterns:** - ``system.*`` - Exclude all system metrics - ``process.cpu.*`` - Exclude all process CPU related metrics - ``*.utilization`` - Exclude all utilization metrics - ``system.memory.usage`` - Exclude the system memory usage metric """ package.py000066400000000000000000000001561527070072300500230ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("psutil >= 5",) py.typed000066400000000000000000000000001527070072300475410ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metricsversion.py000066400000000000000000000001441527070072300501120ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000004421527070072300403150ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-system-metricsasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 psutil==7.0.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-system-metrics tests/000077500000000000000000000000001527070072300352165ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-system-metrics__init__.py000066400000000000000000000000001527070072300373150ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-system-metrics/teststest_system_metrics.py000066400000000000000000001246551527070072300417160ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-system-metrics/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os # pylint: disable=protected-access,too-many-lines import sys import unittest from collections import namedtuple from platform import python_implementation from unittest import mock, skipIf from opentelemetry.instrumentation.system_metrics import ( _DEFAULT_CONFIG, OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS, SystemMetricsInstrumentor, _build_default_config, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.test.test_base import TestBase def _mock_netconnection(): NetConnection = namedtuple("NetworkConnection", ["family", "type", "status"]) Type = namedtuple("Type", ["value"]) return [ NetConnection( family=1, status="ESTABLISHED", type=Type(value=2), ), NetConnection( family=1, status="ESTABLISHED", type=Type(value=1), ), ] class _SystemMetricsResult: def __init__(self, attributes, value) -> None: self.attributes = attributes self.value = value # pylint:disable=too-many-public-methods class TestSystemMetrics(TestBase): def setUp(self): super().setUp() self.implementation = python_implementation().lower() self._patch_net_connections = mock.patch("psutil.net_connections", _mock_netconnection) self._patch_net_connections.start() # Reset the singleton class on each test run SystemMetricsInstrumentor._instance = None def tearDown(self): super().tearDown() self._patch_net_connections.stop() SystemMetricsInstrumentor().uninstrument() def test_system_metrics_instrumentor_initialization(self): try: SystemMetricsInstrumentor() SystemMetricsInstrumentor(config={}) except Exception as error: # pylint: disable=broad-except self.fail(f"Unexpected exception {error} raised") SystemMetricsInstrumentor._instance = None try: SystemMetricsInstrumentor(config={}) SystemMetricsInstrumentor() except Exception as error: # pylint: disable=broad-except self.fail(f"Unexpected exception {error} raised") SystemMetricsInstrumentor().instrument() def test_system_metrics_instrument(self): reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[reader]) system_metrics = SystemMetricsInstrumentor() system_metrics.instrument(meter_provider=meter_provider) metric_names = [] for resource_metrics in reader.get_metrics_data().resource_metrics: for scope_metrics in resource_metrics.scope_metrics: for metric in scope_metrics.metrics: metric_names.append(metric.name) observer_names = [ "system.cpu.time", "system.cpu.utilization", "system.memory.usage", "system.memory.utilization", "system.swap.usage", "system.swap.utilization", "system.disk.io", "system.disk.operations", "system.disk.time", "system.network.dropped_packets", "system.network.packets", "system.network.errors", "system.network.io", "system.thread_count", "process.context_switches", "process.cpu.time", "process.cpu.utilization", "process.memory.usage", "process.disk.io", "process.memory.virtual", "process.thread.count", f"process.runtime.{self.implementation}.memory", f"process.runtime.{self.implementation}.cpu_time", f"process.runtime.{self.implementation}.thread_count", f"process.runtime.{self.implementation}.context_switches", f"process.runtime.{self.implementation}.cpu.utilization", ] # platform dependent metrics if sys.platform != "win32": observer_names.append( "process.open_file_descriptor.count", ) if self.implementation != "pypy": observer_names.append( f"process.runtime.{self.implementation}.gc_count", ) observer_names.append( "cpython.gc.collections", ) observer_names.append( "cpython.gc.collected_objects", ) observer_names.append( "cpython.gc.uncollectable_objects", ) if sys.platform != "darwin": observer_names.append("system.network.connections") self.assertEqual(sorted(metric_names), sorted(observer_names)) def test_process_metrics_instrument(self): process_config = { "process.context_switches": ["involuntary", "voluntary"], "process.cpu.time": ["user", "system"], "process.cpu.utilization": None, "process.memory.usage": None, "process.memory.virtual": None, "process.open_file_descriptor.count": None, "process.thread.count": None, "process.disk.io": ["read", "write"], } reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[reader]) process_metrics = SystemMetricsInstrumentor(config=process_config) process_metrics.instrument(meter_provider=meter_provider) metric_names = [] for resource_metrics in reader.get_metrics_data().resource_metrics: for scope_metrics in resource_metrics.scope_metrics: for metric in scope_metrics.metrics: metric_names.append(metric.name) observer_names = [ "process.memory.usage", "process.memory.virtual", "process.cpu.time", "process.thread.count", "process.disk.io", "process.context_switches", "process.cpu.utilization", ] # platform dependent metrics if sys.platform != "win32": observer_names.append( "process.open_file_descriptor.count", ) self.assertEqual(sorted(metric_names), sorted(observer_names)) def test_runtime_metrics_instrument(self): runtime_config = { "process.runtime.memory": ["rss", "vms"], "process.runtime.cpu.time": ["user", "system"], "process.runtime.thread_count": None, "process.runtime.cpu.utilization": None, "process.runtime.context_switches": ["involuntary", "voluntary"], } if self.implementation != "pypy": runtime_config["process.runtime.gc_count"] = None reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[reader]) runtime_metrics = SystemMetricsInstrumentor(config=runtime_config) runtime_metrics.instrument(meter_provider=meter_provider) metric_names = [] for resource_metrics in reader.get_metrics_data().resource_metrics: for scope_metrics in resource_metrics.scope_metrics: for metric in scope_metrics.metrics: metric_names.append(metric.name) observer_names = [ f"process.runtime.{self.implementation}.memory", f"process.runtime.{self.implementation}.cpu_time", f"process.runtime.{self.implementation}.thread_count", f"process.runtime.{self.implementation}.context_switches", f"process.runtime.{self.implementation}.cpu.utilization", ] if self.implementation != "pypy": observer_names.append(f"process.runtime.{self.implementation}.gc_count") self.assertEqual(sorted(metric_names), sorted(observer_names)) def _assert_metrics(self, observer_name, reader, expected): assertions = 0 # pylint: disable=too-many-nested-blocks for resource_metrics in reader.get_metrics_data().resource_metrics: for scope_metrics in resource_metrics.scope_metrics: for metric in scope_metrics.metrics: for data_point in metric.data.data_points: for expect in expected: if dict(data_point.attributes) == expect.attributes and metric.name == observer_name: self.assertEqual( data_point.value, expect.value, ) assertions += 1 self.assertEqual(len(expected), assertions) @staticmethod def _setup_instrumentor() -> InMemoryMetricReader: reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[reader]) system_metrics = SystemMetricsInstrumentor() system_metrics.instrument(meter_provider=meter_provider) return reader def _test_metrics(self, observer_name, expected): reader = self._setup_instrumentor() self._assert_metrics(observer_name, reader, expected) def _assert_metrics_not_found(self, observer_name): reader = self._setup_instrumentor() seen_metrics = set() for resource_metrics in reader.get_metrics_data().resource_metrics: for scope_metrics in resource_metrics.scope_metrics: for metric in scope_metrics.metrics: seen_metrics.add(metric.name) self.assertNotIn(observer_name, seen_metrics) # This patch is added here to stop psutil from raising an exception # because we're patching cpu_times # pylint: disable=unused-argument @mock.patch("psutil.cpu_times_percent") @mock.patch("psutil.cpu_times") def test_system_cpu_time(self, mock_cpu_times, mock_cpu_times_percent): CPUTimes = namedtuple("CPUTimes", ["idle", "user", "system", "irq"]) mock_cpu_times.return_value = [ CPUTimes(idle=1.2, user=3.4, system=5.6, irq=7.8), CPUTimes(idle=1.2, user=3.4, system=5.6, irq=7.8), ] expected = [ _SystemMetricsResult( { "cpu": 1, "state": "idle", }, 1.2, ), _SystemMetricsResult( { "cpu": 1, "state": "user", }, 3.4, ), _SystemMetricsResult( { "cpu": 1, "state": "system", }, 5.6, ), _SystemMetricsResult( { "cpu": 1, "state": "irq", }, 7.8, ), _SystemMetricsResult( { "cpu": 2, "state": "idle", }, 1.2, ), _SystemMetricsResult( { "cpu": 2, "state": "user", }, 3.4, ), _SystemMetricsResult( { "cpu": 2, "state": "system", }, 5.6, ), _SystemMetricsResult( { "cpu": 2, "state": "irq", }, 7.8, ), ] self._test_metrics("system.cpu.time", expected) @mock.patch("psutil.cpu_times_percent") def test_system_cpu_utilization(self, mock_cpu_times_percent): CPUTimesPercent = namedtuple("CPUTimesPercent", ["idle", "user", "system", "irq"]) mock_cpu_times_percent.return_value = [ CPUTimesPercent(idle=1.2, user=3.4, system=5.6, irq=7.8), CPUTimesPercent(idle=1.2, user=3.4, system=5.6, irq=7.8), ] expected = [ _SystemMetricsResult({"cpu": 1, "state": "idle"}, 1.2 / 100), _SystemMetricsResult({"cpu": 1, "state": "user"}, 3.4 / 100), _SystemMetricsResult({"cpu": 1, "state": "system"}, 5.6 / 100), _SystemMetricsResult({"cpu": 1, "state": "irq"}, 7.8 / 100), _SystemMetricsResult({"cpu": 2, "state": "idle"}, 1.2 / 100), _SystemMetricsResult({"cpu": 2, "state": "user"}, 3.4 / 100), _SystemMetricsResult({"cpu": 2, "state": "system"}, 5.6 / 100), _SystemMetricsResult({"cpu": 2, "state": "irq"}, 7.8 / 100), ] self._test_metrics("system.cpu.utilization", expected) @mock.patch("psutil.virtual_memory") def test_system_memory_usage(self, mock_virtual_memory): VirtualMemory = namedtuple("VirtualMemory", ["used", "free", "cached", "total"]) mock_virtual_memory.return_value = VirtualMemory(used=1, free=2, cached=3, total=4) expected = [ _SystemMetricsResult({"state": "used"}, 1), _SystemMetricsResult({"state": "free"}, 2), _SystemMetricsResult({"state": "cached"}, 3), ] self._test_metrics("system.memory.usage", expected) @mock.patch("psutil.virtual_memory") def test_system_memory_utilization(self, mock_virtual_memory): VirtualMemory = namedtuple("VirtualMemory", ["used", "free", "cached", "total"]) mock_virtual_memory.return_value = VirtualMemory(used=1, free=2, cached=3, total=4) expected = [ _SystemMetricsResult({"state": "used"}, 1 / 4), _SystemMetricsResult({"state": "free"}, 2 / 4), _SystemMetricsResult({"state": "cached"}, 3 / 4), ] self._test_metrics("system.memory.utilization", expected) @mock.patch("psutil.swap_memory") def test_system_swap_usage(self, mock_swap_memory): SwapMemory = namedtuple("SwapMemory", ["used", "free", "total"]) mock_swap_memory.return_value = SwapMemory(used=1, free=2, total=3) expected = [ _SystemMetricsResult({"state": "used"}, 1), _SystemMetricsResult({"state": "free"}, 2), ] self._test_metrics("system.swap.usage", expected) @mock.patch("psutil.swap_memory") def test_system_swap_utilization(self, mock_swap_memory): SwapMemory = namedtuple("SwapMemory", ["used", "free", "total"]) mock_swap_memory.return_value = SwapMemory(used=1, free=2, total=3) expected = [ _SystemMetricsResult({"state": "used"}, 1 / 3), _SystemMetricsResult({"state": "free"}, 2 / 3), ] self._test_metrics("system.swap.utilization", expected) @mock.patch("psutil.disk_io_counters") def test_system_disk_io(self, mock_disk_io_counters): DiskIO = namedtuple( "DiskIO", [ "read_count", "write_count", "read_bytes", "write_bytes", "read_time", "write_time", "read_merged_count", "write_merged_count", ], ) mock_disk_io_counters.return_value = { "sda": DiskIO( read_count=1, write_count=2, read_bytes=3, write_bytes=4, read_time=5, write_time=6, read_merged_count=7, write_merged_count=8, ), "sdb": DiskIO( read_count=9, write_count=10, read_bytes=11, write_bytes=12, read_time=13, write_time=14, read_merged_count=15, write_merged_count=16, ), } expected = [ _SystemMetricsResult({"device": "sda", "direction": "read"}, 3), _SystemMetricsResult({"device": "sda", "direction": "write"}, 4), _SystemMetricsResult({"device": "sdb", "direction": "read"}, 11), _SystemMetricsResult({"device": "sdb", "direction": "write"}, 12), ] self._test_metrics("system.disk.io", expected) @mock.patch("psutil.disk_io_counters") def test_system_disk_operations(self, mock_disk_io_counters): DiskIO = namedtuple( "DiskIO", [ "read_count", "write_count", "read_bytes", "write_bytes", "read_time", "write_time", "read_merged_count", "write_merged_count", ], ) mock_disk_io_counters.return_value = { "sda": DiskIO( read_count=1, write_count=2, read_bytes=3, write_bytes=4, read_time=5, write_time=6, read_merged_count=7, write_merged_count=8, ), "sdb": DiskIO( read_count=9, write_count=10, read_bytes=11, write_bytes=12, read_time=13, write_time=14, read_merged_count=15, write_merged_count=16, ), } expected = [ _SystemMetricsResult({"device": "sda", "direction": "read"}, 1), _SystemMetricsResult({"device": "sda", "direction": "write"}, 2), _SystemMetricsResult({"device": "sdb", "direction": "read"}, 9), _SystemMetricsResult({"device": "sdb", "direction": "write"}, 10), ] self._test_metrics("system.disk.operations", expected) @mock.patch("psutil.disk_io_counters") def test_system_disk_time(self, mock_disk_io_counters): DiskIO = namedtuple( "DiskIO", [ "read_count", "write_count", "read_bytes", "write_bytes", "read_time", "write_time", "read_merged_count", "write_merged_count", ], ) mock_disk_io_counters.return_value = { "sda": DiskIO( read_count=1, write_count=2, read_bytes=3, write_bytes=4, read_time=5, write_time=6, read_merged_count=7, write_merged_count=8, ), "sdb": DiskIO( read_count=9, write_count=10, read_bytes=11, write_bytes=12, read_time=13, write_time=14, read_merged_count=15, write_merged_count=16, ), } expected = [ _SystemMetricsResult({"device": "sda", "direction": "read"}, 5 / 1000), _SystemMetricsResult({"device": "sda", "direction": "write"}, 6 / 1000), _SystemMetricsResult({"device": "sdb", "direction": "read"}, 13 / 1000), _SystemMetricsResult({"device": "sdb", "direction": "write"}, 14 / 1000), ] self._test_metrics("system.disk.time", expected) @mock.patch("psutil.net_io_counters") def test_system_network_dropped_packets(self, mock_net_io_counters): NetIO = namedtuple( "NetIO", [ "dropin", "dropout", "packets_sent", "packets_recv", "errin", "errout", "bytes_sent", "bytes_recv", ], ) mock_net_io_counters.return_value = { "eth0": NetIO( dropin=1, dropout=2, packets_sent=3, packets_recv=4, errin=5, errout=6, bytes_sent=7, bytes_recv=8, ), "eth1": NetIO( dropin=9, dropout=10, packets_sent=11, packets_recv=12, errin=13, errout=14, bytes_sent=15, bytes_recv=16, ), } expected = [ _SystemMetricsResult({"device": "eth0", "direction": "receive"}, 1), _SystemMetricsResult({"device": "eth0", "direction": "transmit"}, 2), _SystemMetricsResult({"device": "eth1", "direction": "receive"}, 9), _SystemMetricsResult({"device": "eth1", "direction": "transmit"}, 10), ] self._test_metrics("system.network.dropped_packets", expected) @mock.patch("psutil.net_io_counters") def test_system_network_packets(self, mock_net_io_counters): NetIO = namedtuple( "NetIO", [ "dropin", "dropout", "packets_sent", "packets_recv", "errin", "errout", "bytes_sent", "bytes_recv", ], ) mock_net_io_counters.return_value = { "eth0": NetIO( dropin=1, dropout=2, packets_sent=3, packets_recv=4, errin=5, errout=6, bytes_sent=7, bytes_recv=8, ), "eth1": NetIO( dropin=9, dropout=10, packets_sent=11, packets_recv=12, errin=13, errout=14, bytes_sent=15, bytes_recv=16, ), } expected = [ _SystemMetricsResult({"device": "eth0", "direction": "receive"}, 4), _SystemMetricsResult({"device": "eth0", "direction": "transmit"}, 3), _SystemMetricsResult({"device": "eth1", "direction": "receive"}, 12), _SystemMetricsResult({"device": "eth1", "direction": "transmit"}, 11), ] self._test_metrics("system.network.packets", expected) @mock.patch("psutil.net_io_counters") def test_system_network_errors(self, mock_net_io_counters): NetIO = namedtuple( "NetIO", [ "dropin", "dropout", "packets_sent", "packets_recv", "errin", "errout", "bytes_sent", "bytes_recv", ], ) mock_net_io_counters.return_value = { "eth0": NetIO( dropin=1, dropout=2, packets_sent=3, packets_recv=4, errin=5, errout=6, bytes_sent=7, bytes_recv=8, ), "eth1": NetIO( dropin=9, dropout=10, packets_sent=11, packets_recv=12, errin=13, errout=14, bytes_sent=15, bytes_recv=16, ), } expected = [ _SystemMetricsResult({"device": "eth0", "direction": "receive"}, 5), _SystemMetricsResult({"device": "eth0", "direction": "transmit"}, 6), _SystemMetricsResult({"device": "eth1", "direction": "receive"}, 13), _SystemMetricsResult({"device": "eth1", "direction": "transmit"}, 14), ] self._test_metrics("system.network.errors", expected) @mock.patch("psutil.net_io_counters") def test_system_network_io(self, mock_net_io_counters): NetIO = namedtuple( "NetIO", [ "dropin", "dropout", "packets_sent", "packets_recv", "errin", "errout", "bytes_sent", "bytes_recv", ], ) mock_net_io_counters.return_value = { "eth0": NetIO( dropin=1, dropout=2, packets_sent=3, packets_recv=4, errin=5, errout=6, bytes_sent=7, bytes_recv=8, ), "eth1": NetIO( dropin=9, dropout=10, packets_sent=11, packets_recv=12, errin=13, errout=14, bytes_sent=15, bytes_recv=16, ), } expected = [ _SystemMetricsResult({"device": "eth0", "direction": "receive"}, 8), _SystemMetricsResult({"device": "eth0", "direction": "transmit"}, 7), _SystemMetricsResult({"device": "eth1", "direction": "receive"}, 16), _SystemMetricsResult({"device": "eth1", "direction": "transmit"}, 15), ] self._test_metrics("system.network.io", expected) @skipIf(sys.platform == "darwin", "No network connections on macOS") @mock.patch("psutil.net_connections") def test_system_network_connections(self, mock_net_connections): NetConnection = namedtuple("NetworkConnection", ["family", "type", "status"]) Type = namedtuple("Type", ["value"]) mock_net_connections.return_value = [ NetConnection( family=1, status="ESTABLISHED", type=Type(value=2), ), NetConnection( family=1, status="ESTABLISHED", type=Type(value=1), ), ] expected = [ _SystemMetricsResult( { "family": 1, "protocol": "udp", "state": "ESTABLISHED", "type": Type(value=2), }, 1, ), _SystemMetricsResult( { "family": 1, "protocol": "tcp", "state": "ESTABLISHED", "type": Type(value=1), }, 1, ), ] self._test_metrics("system.network.connections", expected) @mock.patch("threading.active_count") def test_system_thread_count(self, threading_active_count): threading_active_count.return_value = 42 expected = [_SystemMetricsResult({}, 42)] self._test_metrics("system.thread_count", expected) @mock.patch("psutil.Process.memory_info") def test_memory_usage(self, mock_process_memory_info): PMem = namedtuple("PMem", ["rss", "vms"]) mock_process_memory_info.configure_mock(return_value=PMem(rss=1, vms=2)) expected = [ _SystemMetricsResult({}, 1), ] self._test_metrics("process.memory.usage", expected) @mock.patch("psutil.Process.memory_info") def test_memory_virtual(self, mock_process_memory_info): PMem = namedtuple("PMem", ["rss", "vms"]) mock_process_memory_info.configure_mock(return_value=PMem(rss=1, vms=2)) expected = [ _SystemMetricsResult({}, 2), ] self._test_metrics("process.memory.virtual", expected) @mock.patch("psutil.Process.cpu_times") def test_cpu_time(self, mock_process_cpu_times): PCPUTimes = namedtuple("PCPUTimes", ["user", "system"]) mock_process_cpu_times.configure_mock(return_value=PCPUTimes(user=1.1, system=2.2)) expected = [ _SystemMetricsResult({"type": "user"}, 1.1), _SystemMetricsResult({"type": "system"}, 2.2), ] self._test_metrics("process.cpu.time", expected) @mock.patch("psutil.Process.num_ctx_switches") def test_context_switches(self, mock_process_num_ctx_switches): PCtxSwitches = namedtuple("PCtxSwitches", ["voluntary", "involuntary"]) mock_process_num_ctx_switches.configure_mock(return_value=PCtxSwitches(voluntary=1, involuntary=2)) expected = [ _SystemMetricsResult({"type": "voluntary"}, 1), _SystemMetricsResult({"type": "involuntary"}, 2), ] self._test_metrics("process.context_switches", expected) @mock.patch("psutil.Process.num_ctx_switches") def test_context_switches_not_implemented_error(self, mock_process_num_ctx_switches): mock_process_num_ctx_switches.side_effect = NotImplementedError self._assert_metrics_not_found("process.context_switches") @mock.patch("psutil.Process.num_threads") def test_thread_count(self, mock_process_thread_num): mock_process_thread_num.configure_mock(return_value=42) expected = [_SystemMetricsResult({}, 42)] self._test_metrics("process.thread.count", expected) @mock.patch("psutil.Process.io_counters") def test_process_disk_io(self, mock_process_io_counters): PIOCounters = namedtuple("PIOCounters", ["read_bytes", "write_bytes"]) mock_process_io_counters.configure_mock(return_value=PIOCounters(read_bytes=1024, write_bytes=2048)) expected = [ _SystemMetricsResult({"direction": "read"}, 1024), _SystemMetricsResult({"direction": "write"}, 2048), ] self._test_metrics("process.disk.io", expected) @mock.patch("psutil.Process.io_counters") def test_process_disk_io_not_implemented_error(self, mock_process_io_counters): mock_process_io_counters.side_effect = NotImplementedError self._assert_metrics_not_found("process.disk.io") @mock.patch("psutil.Process.cpu_percent") @mock.patch("psutil.cpu_count") def test_cpu_utilization(self, mock_cpu_count, mock_process_cpu_percent): mock_cpu_count.return_value = 1 mock_process_cpu_percent.configure_mock(return_value=42) expected = [_SystemMetricsResult({}, 0.42)] self._test_metrics("process.cpu.utilization", expected) @skipIf(sys.platform == "win32", "No file descriptors on Windows") @mock.patch("psutil.Process.num_fds") def test_open_file_descriptor_count(self, mock_process_num_fds): mock_process_num_fds.configure_mock(return_value=3) expected = [_SystemMetricsResult({}, 3)] self._test_metrics( "process.open_file_descriptor.count", expected, ) mock_process_num_fds.assert_called() @mock.patch("psutil.Process.memory_info") def test_runtime_memory(self, mock_process_memory_info): PMem = namedtuple("PMem", ["rss", "vms"]) mock_process_memory_info.configure_mock(return_value=PMem(rss=1, vms=2)) expected = [ _SystemMetricsResult({"type": "rss"}, 1), _SystemMetricsResult({"type": "vms"}, 2), ] self._test_metrics(f"process.runtime.{self.implementation}.memory", expected) @mock.patch("psutil.Process.cpu_times") def test_runtime_cpu_time(self, mock_process_cpu_times): PCPUTimes = namedtuple("PCPUTimes", ["user", "system"]) mock_process_cpu_times.configure_mock(return_value=PCPUTimes(user=1.1, system=2.2)) expected = [ _SystemMetricsResult({"type": "user"}, 1.1), _SystemMetricsResult({"type": "system"}, 2.2), ] self._test_metrics(f"process.runtime.{self.implementation}.cpu_time", expected) @mock.patch("gc.get_count") @skipIf(python_implementation().lower() == "pypy", "not supported for pypy") def test_runtime_get_count(self, mock_gc_get_count): mock_gc_get_count.configure_mock(return_value=(1, 2, 3)) expected_gc_count = [ _SystemMetricsResult({"count": "0"}, 1), _SystemMetricsResult({"count": "1"}, 2), _SystemMetricsResult({"count": "2"}, 3), ] self._test_metrics( f"process.runtime.{self.implementation}.gc_count", expected_gc_count, ) @mock.patch("gc.get_stats") @skipIf(python_implementation().lower() == "pypy", "not supported for pypy") def test_runtime_get_gc_collections(self, mock_gc_get_stats): mock_gc_get_stats.configure_mock( return_value=[ {"collections": 10, "collected": 100, "uncollectable": 1}, {"collections": 20, "collected": 200, "uncollectable": 2}, {"collections": 30, "collected": 300, "uncollectable": 3}, ] ) expected_gc_collections = [ _SystemMetricsResult({"cpython.gc.generation": 0, "generation": "0"}, 10), _SystemMetricsResult({"cpython.gc.generation": 1, "generation": "1"}, 20), _SystemMetricsResult({"cpython.gc.generation": 2, "generation": "2"}, 30), ] self._test_metrics( "cpython.gc.collections", expected_gc_collections, ) @mock.patch("gc.get_stats") @skipIf(python_implementation().lower() == "pypy", "not supported for pypy") def test_runtime_get_gc_collected_objects(self, mock_gc_get_stats): mock_gc_get_stats.configure_mock( return_value=[ {"collections": 10, "collected": 100, "uncollectable": 1}, {"collections": 20, "collected": 200, "uncollectable": 2}, {"collections": 30, "collected": 300, "uncollectable": 3}, ] ) expected_gc_collected_objects = [ _SystemMetricsResult({"cpython.gc.generation": 0, "generation": "0"}, 100), _SystemMetricsResult({"cpython.gc.generation": 1, "generation": "1"}, 200), _SystemMetricsResult({"cpython.gc.generation": 2, "generation": "2"}, 300), ] self._test_metrics( "cpython.gc.collected_objects", expected_gc_collected_objects, ) @mock.patch("gc.get_stats") @skipIf(python_implementation().lower() == "pypy", "not supported for pypy") def test_runtime_get_gc_uncollectable_objects(self, mock_gc_get_stats): mock_gc_get_stats.configure_mock( return_value=[ {"collections": 10, "collected": 100, "uncollectable": 1}, {"collections": 20, "collected": 200, "uncollectable": 2}, {"collections": 30, "collected": 300, "uncollectable": 3}, ] ) expected_gc_uncollectable_objects = [ _SystemMetricsResult({"cpython.gc.generation": 0, "generation": "0"}, 1), _SystemMetricsResult({"cpython.gc.generation": 1, "generation": "1"}, 2), _SystemMetricsResult({"cpython.gc.generation": 2, "generation": "2"}, 3), ] self._test_metrics( "cpython.gc.uncollectable_objects", expected_gc_uncollectable_objects, ) @mock.patch("psutil.Process.num_ctx_switches") def test_runtime_context_switches(self, mock_process_num_ctx_switches): PCtxSwitches = namedtuple("PCtxSwitches", ["voluntary", "involuntary"]) mock_process_num_ctx_switches.configure_mock(return_value=PCtxSwitches(voluntary=1, involuntary=2)) expected = [ _SystemMetricsResult({"type": "voluntary"}, 1), _SystemMetricsResult({"type": "involuntary"}, 2), ] self._test_metrics(f"process.runtime.{self.implementation}.context_switches", expected) @mock.patch("psutil.Process.num_ctx_switches") def test_runtime_context_switches_not_implemented_error(self, mock_process_num_ctx_switches): mock_process_num_ctx_switches.side_effect = NotImplementedError self._assert_metrics_not_found( f"process.runtime.{self.implementation}.context_switches", ) @mock.patch("psutil.Process.num_threads") def test_runtime_thread_count(self, mock_process_thread_num): mock_process_thread_num.configure_mock(return_value=42) expected = [_SystemMetricsResult({}, 42)] self._test_metrics(f"process.runtime.{self.implementation}.thread_count", expected) @mock.patch("psutil.Process.cpu_percent") def test_runtime_cpu_utilization(self, mock_process_cpu_percent): mock_process_cpu_percent.configure_mock(return_value=42) expected = [_SystemMetricsResult({}, 0.42)] self._test_metrics(f"process.runtime.{self.implementation}.cpu.utilization", expected) class TestConfigSystemMetrics(TestBase): # pylint:disable=no-self-use def test_that_correct_config_is_read(self): for key, value in _DEFAULT_CONFIG.items(): meter_provider = MeterProvider([InMemoryMetricReader()]) instrumentor = SystemMetricsInstrumentor(config={key: value}) instrumentor.instrument(meter_provider=meter_provider) meter_provider.force_flush() instrumentor.uninstrument() class TestBuildDefaultConfig(unittest.TestCase): def setUp(self): self.env_patcher = mock.patch.dict("os.environ", {}, clear=False) self.env_patcher.start() def tearDown(self): self.env_patcher.stop() os.environ.pop(OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS, None) def test_default_config_without_exclusions(self): test_cases = [ { "name": "no_env_var_set", "env_value": None, }, { "name": "empty_string", "env_value": "", }, { "name": "whitespace_only", "env_value": " ", }, ] for test_case in test_cases: with self.subTest(test_case["name"]): if test_case["env_value"] is None: # Don't set the environment variable result = _build_default_config() else: with mock.patch.dict( "os.environ", {OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS: test_case["env_value"]}, ): result = _build_default_config() self.assertEqual(result, _DEFAULT_CONFIG) def test_exact_metric_exclusions(self): test_cases = [ { "name": "single_metric", "pattern": "system.cpu.time", "excluded": ["system.cpu.time"], "included": ["system.cpu.utilization", "system.memory.usage"], "expected_count": len(_DEFAULT_CONFIG) - 1, }, { "name": "multiple_metrics", "pattern": "system.cpu.time,system.memory.usage", "excluded": ["system.cpu.time", "system.memory.usage"], "included": ["system.cpu.utilization", "process.cpu.time"], "expected_count": len(_DEFAULT_CONFIG) - 2, }, { "name": "with_whitespace", "pattern": "system.cpu.time , system.memory.usage , process.cpu.time", "excluded": [ "system.cpu.time", "system.memory.usage", "process.cpu.time", ], "included": ["system.cpu.utilization"], "expected_count": len(_DEFAULT_CONFIG) - 3, }, { "name": "non_existent_metric", "pattern": "non.existent.metric", "excluded": [], "included": ["system.cpu.time", "process.cpu.time"], "expected_count": len(_DEFAULT_CONFIG), }, ] for test_case in test_cases: with self.subTest(test_case["name"]): with mock.patch.dict( "os.environ", {OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS: test_case["pattern"]}, ): result = _build_default_config() for metric in test_case["excluded"]: self.assertNotIn(metric, result, f"{metric} should be excluded") for metric in test_case["included"]: self.assertIn(metric, result, f"{metric} should be included") self.assertEqual(len(result), test_case["expected_count"]) def test_wildcard_patterns(self): test_cases = [ { "name": "all_system_metrics", "pattern": "system.*", "excluded_prefixes": ["system."], "included_prefixes": ["process.", "cpython."], }, { "name": "system_cpu_prefix", "pattern": "system.cpu.*", "excluded": ["system.cpu.time", "system.cpu.utilization"], "included": ["system.memory.usage", "system.disk.io"], }, { "name": "utilization_suffix", "pattern": "*.utilization", "excluded_suffixes": [".utilization"], "included": ["system.cpu.time", "system.memory.usage"], }, { "name": "all_metrics", "pattern": "*", "expected_count": 0, }, ] for test_case in test_cases: with self.subTest(test_case["name"]): with mock.patch.dict( "os.environ", {OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS: test_case["pattern"]}, ): result = _build_default_config() if "excluded" in test_case: for metric in test_case["excluded"]: self.assertNotIn(metric, result) if "included" in test_case: for metric in test_case["included"]: self.assertIn(metric, result) if "excluded_prefixes" in test_case: for prefix in test_case["excluded_prefixes"]: excluded_metrics = [k for k in result if k.startswith(prefix)] self.assertEqual( len(excluded_metrics), 0, ) if "included_prefixes" in test_case: for prefix in test_case["included_prefixes"]: included_metrics = [k for k in result if k.startswith(prefix)] self.assertGreater( len(included_metrics), 0, ) if "excluded_suffixes" in test_case: for suffix in test_case["excluded_suffixes"]: suffix_metrics = [k for k in result if k.endswith(suffix)] self.assertEqual( len(suffix_metrics), 0, ) if "expected_count" in test_case: self.assertEqual(len(result), test_case["expected_count"]) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-threading/000077500000000000000000000000001527070072300331105ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-threading/LICENSE000066400000000000000000000261351527070072300341240ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000015601527070072300345220ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-threadingOpenTelemetry threading Instrumentation ======================================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-threading.svg :target: https://pypi.org/project/opentelemetry-instrumentation-threading/ This library provides instrumentation for the `threading` module to ensure that the OpenTelemetry context is propagated across threads. It is important to note that this instrumentation does not produce any telemetry data on its own. It merely ensures that the context is correctly propagated when threads are used. Installation ------------ :: pip install opentelemetry-instrumentation-threading References ---------- * `OpenTelemetry Threading Tracing `_ * `OpenTelemetry Project `_ pyproject.toml000066400000000000000000000031001527070072300357370ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-threading[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-threading" dynamic = ["version"] description = "Thread context propagation support for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [] [project.entry-points.opentelemetry_instrumentor] threading = "opentelemetry.instrumentation.threading:ThreadingInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-threading" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/threading/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-threading/src/000077500000000000000000000000001527070072300336775ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300365145ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-threading/srcinstrumentation/000077500000000000000000000000001527070072300417575ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-threading/src/opentelemetrythreading/000077500000000000000000000000001527070072300437245ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-threading/src/opentelemetry/instrumentation__init__.py000066400000000000000000000125171527070072300460430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-threading/src/opentelemetry/instrumentation/threading# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Instrument threading to propagate OpenTelemetry context. Usage ----- .. code-block:: python from opentelemetry.instrumentation.threading import ThreadingInstrumentor ThreadingInstrumentor().instrument() This library provides instrumentation for the `threading` module to ensure that the OpenTelemetry context is propagated across threads. It is important to note that this instrumentation does not produce any telemetry data on its own. It merely ensures that the context is correctly propagated when threads are used. When instrumented, new threads created using threading.Thread, threading.Timer, or within futures.ThreadPoolExecutor will have the current OpenTelemetry context attached, and this context will be re-activated in the thread's run method or the executor's worker thread." """ from __future__ import annotations import threading from collections.abc import Callable, Collection from concurrent import futures from typing import TYPE_CHECKING, Any from wrapt import ( wrap_function_wrapper, # type: ignore[reportUnknownVariableType] ) from opentelemetry import context from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.threading.package import _instruments from opentelemetry.instrumentation.utils import unwrap if TYPE_CHECKING: from typing import Protocol, TypeVar R = TypeVar("R") class HasOtelContext(Protocol): _otel_context: context.Context class ThreadingInstrumentor(BaseInstrumentor): __WRAPPER_START_METHOD = "start" __WRAPPER_RUN_METHOD = "run" __WRAPPER_SUBMIT_METHOD = "submit" def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs: Any): self._instrument_thread() self._instrument_timer() self._instrument_thread_pool() def _uninstrument(self, **kwargs: Any): self._uninstrument_thread() self._uninstrument_timer() self._uninstrument_thread_pool() @staticmethod def _instrument_thread(): wrap_function_wrapper( threading.Thread, ThreadingInstrumentor.__WRAPPER_START_METHOD, ThreadingInstrumentor.__wrap_threading_start, ) wrap_function_wrapper( threading.Thread, ThreadingInstrumentor.__WRAPPER_RUN_METHOD, ThreadingInstrumentor.__wrap_threading_run, ) @staticmethod def _instrument_timer(): wrap_function_wrapper( threading.Timer, ThreadingInstrumentor.__WRAPPER_START_METHOD, ThreadingInstrumentor.__wrap_threading_start, ) wrap_function_wrapper( threading.Timer, ThreadingInstrumentor.__WRAPPER_RUN_METHOD, ThreadingInstrumentor.__wrap_threading_run, ) @staticmethod def _instrument_thread_pool(): wrap_function_wrapper( futures.ThreadPoolExecutor, ThreadingInstrumentor.__WRAPPER_SUBMIT_METHOD, ThreadingInstrumentor.__wrap_thread_pool_submit, ) @staticmethod def _uninstrument_thread(): unwrap(threading.Thread, ThreadingInstrumentor.__WRAPPER_START_METHOD) unwrap(threading.Thread, ThreadingInstrumentor.__WRAPPER_RUN_METHOD) @staticmethod def _uninstrument_timer(): unwrap(threading.Timer, ThreadingInstrumentor.__WRAPPER_START_METHOD) unwrap(threading.Timer, ThreadingInstrumentor.__WRAPPER_RUN_METHOD) @staticmethod def _uninstrument_thread_pool(): unwrap( futures.ThreadPoolExecutor, ThreadingInstrumentor.__WRAPPER_SUBMIT_METHOD, ) @staticmethod def __wrap_threading_start( call_wrapped: Callable[[], None], instance: HasOtelContext, args: tuple[()], kwargs: dict[str, Any], ) -> None: instance._otel_context = context.get_current() return call_wrapped(*args, **kwargs) @staticmethod def __wrap_threading_run( call_wrapped: Callable[..., R], instance: HasOtelContext, args: tuple[Any, ...], kwargs: dict[str, Any], ) -> R: token = None try: if hasattr(instance, "_otel_context"): token = context.attach(instance._otel_context) return call_wrapped(*args, **kwargs) finally: if token is not None: context.detach(token) @staticmethod def __wrap_thread_pool_submit( call_wrapped: Callable[..., R], instance: futures.ThreadPoolExecutor, args: tuple[Callable[..., Any], ...], kwargs: dict[str, Any], ) -> R: # obtain the original function and wrapped kwargs original_func = args[0] otel_context = context.get_current() def wrapped_func(*func_args: Any, **func_kwargs: Any) -> R: token = None try: token = context.attach(otel_context) return original_func(*func_args, **func_kwargs) finally: if token is not None: context.detach(token) # replace the original function with the wrapped function new_args: tuple[Callable[..., Any], ...] = (wrapped_func,) + args[1:] return call_wrapped(*new_args, **kwargs) package.py000066400000000000000000000001721527070072300456710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-threading/src/opentelemetry/instrumentation/threading# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = () _supports_metrics = False version.py000066400000000000000000000001441527070072300457620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-threading/src/opentelemetry/instrumentation/threading# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000004171527070072300372740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-threadingasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-threading opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-threading/tests/000077500000000000000000000000001527070072300342525ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300362720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-threading/teststest_threading.py000066400000000000000000000272051527070072300375570ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-threading/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import threading from concurrent.futures import ( Future, ThreadPoolExecutor, ) from unittest.mock import MagicMock, patch from opentelemetry import trace from opentelemetry.instrumentation.threading import ThreadingInstrumentor from opentelemetry.test.test_base import TestBase # pylint: disable=too-many-public-methods class TestThreading(TestBase): def setUp(self): super().setUp() self._tracer = self.tracer_provider.get_tracer(__name__) self._mock_span_contexts: list[trace.SpanContext] = [] ThreadingInstrumentor().instrument() def tearDown(self): ThreadingInstrumentor().uninstrument() super().tearDown() def get_root_span(self): return self._tracer.start_as_current_span("rootSpan") def test_trace_context_propagation_in_thread(self): self.run_threading_test(threading.Thread(target=self.fake_func)) def test_trace_context_propagation_in_timer(self): self.run_threading_test(threading.Timer(interval=1, function=self.fake_func)) def run_threading_test(self, thread: threading.Thread): with self.get_root_span() as span: expected_span_context = span.get_span_context() thread.start() thread.join() # check result self.assertEqual(len(self._mock_span_contexts), 1) self.assertEqual(self._mock_span_contexts[0], expected_span_context) def test_trace_context_propagation_in_thread_pool_with_multiple_workers( self, ): max_workers = 10 executor = ThreadPoolExecutor(max_workers=max_workers) expected_span_contexts: list[trace.SpanContext] = [] futures_list: list[Future[trace.SpanContext]] = [] for num in range(max_workers): with self._tracer.start_as_current_span(f"trace_{num}") as span: expected_span_context = span.get_span_context() expected_span_contexts.append(expected_span_context) future = executor.submit(self.get_current_span_context_for_test) futures_list.append(future) result_span_contexts = [future.result() for future in futures_list] # check result self.assertEqual(result_span_contexts, expected_span_contexts) def test_trace_context_propagation_in_thread_pool_with_single_worker(self): max_workers = 1 with ThreadPoolExecutor(max_workers=max_workers) as executor: # test propagation of the same trace context across multiple tasks with self._tracer.start_as_current_span("task") as task_span: expected_task_context = task_span.get_span_context() future1 = executor.submit(self.get_current_span_context_for_test) future2 = executor.submit(self.get_current_span_context_for_test) # check result self.assertEqual(future1.result(), expected_task_context) self.assertEqual(future2.result(), expected_task_context) # test propagation of different trace contexts across tasks in sequence with self._tracer.start_as_current_span("task1") as task1_span: expected_task1_context = task1_span.get_span_context() future1 = executor.submit(self.get_current_span_context_for_test) # check result self.assertEqual(future1.result(), expected_task1_context) with self._tracer.start_as_current_span("task2") as task2_span: expected_task2_context = task2_span.get_span_context() future2 = executor.submit(self.get_current_span_context_for_test) # check result self.assertEqual(future2.result(), expected_task2_context) def fake_func(self): span_context = self.get_current_span_context_for_test() self._mock_span_contexts.append(span_context) @staticmethod def get_current_span_context_for_test() -> trace.SpanContext: return trace.get_current_span().get_span_context() def print_square(self, num: float) -> int | float: with self._tracer.start_as_current_span("square"): return num * num def print_cube(self, num: float) -> int | float: with self._tracer.start_as_current_span("cube"): return num * num * num def print_square_with_thread(self, num: float) -> int | float: with self._tracer.start_as_current_span("square"): cube_thread = threading.Thread(target=self.print_cube, args=(10,)) cube_thread.start() cube_thread.join() return num * num def calculate(self, num: float) -> None: with self._tracer.start_as_current_span("calculate"): square_thread = threading.Thread(target=self.print_square, args=(num,)) cube_thread = threading.Thread(target=self.print_cube, args=(num,)) square_thread.start() square_thread.join() cube_thread.start() cube_thread.join() def test_without_thread_nesting(self): square_thread = threading.Thread(target=self.print_square, args=(10,)) with self._tracer.start_as_current_span("root"): square_thread.start() square_thread.join() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) # pylint: disable=unbalanced-tuple-unpacking target, root = spans[:2] self.assertIs(target.parent, root.get_span_context()) self.assertIsNone(root.parent) def test_with_thread_nesting(self): # # Following scenario is tested. # threadA -> methodA -> threadB -> methodB # square_thread = threading.Thread(target=self.print_square_with_thread, args=(10,)) with self._tracer.start_as_current_span("root"): square_thread.start() square_thread.join() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) # pylint: disable=unbalanced-tuple-unpacking cube, square, root = spans[:3] self.assertIs(cube.parent, square.get_span_context()) self.assertIs(square.parent, root.get_span_context()) self.assertIsNone(root.parent) def test_with_thread_multi_nesting(self): # # Following scenario is tested. # / threadB -> methodB # threadA -> methodA -> # \ threadC -> methodC # calculate_thread = threading.Thread(target=self.calculate, args=(10,)) with self._tracer.start_as_current_span("root"): calculate_thread.start() calculate_thread.join() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 4) # pylint: disable=unbalanced-tuple-unpacking cube, square, calculate, root = spans[:4] self.assertIs(cube.parent, calculate.get_span_context()) self.assertIs(square.parent, calculate.get_span_context()) self.assertIs(calculate.parent, root.get_span_context()) self.assertIsNone(root.parent) def test_uninstrumented(self): ThreadingInstrumentor().uninstrument() square_thread = threading.Thread(target=self.print_square, args=(10,)) square_thread.start() square_thread.join() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) ThreadingInstrumentor().instrument() @patch( "opentelemetry.context.attach", new=MagicMock(return_value=None), ) @patch( "opentelemetry.context.detach", autospec=True, ) def test_threading_with_none_context_token(self, mock_detach: MagicMock): with self.get_root_span(): thread = threading.Thread(target=self.fake_func) thread.start() thread.join() mock_detach.assert_not_called() @patch( "opentelemetry.context._RUNTIME_CONTEXT.attach", new=MagicMock(return_value=MagicMock()), ) @patch( "opentelemetry.context._RUNTIME_CONTEXT.detach", new=MagicMock(return_value=None), ) @patch("opentelemetry.context.detach", autospec=True) def test_threading_with_valid_context_token(self, mock_detach: MagicMock): with self.get_root_span(): thread = threading.Thread(target=self.fake_func) thread.start() thread.join() mock_detach.assert_called_once() @patch( "opentelemetry.context.attach", new=MagicMock(return_value=None), ) @patch( "opentelemetry.context.detach", autospec=True, ) def test_thread_pool_with_none_context_token(self, mock_detach: MagicMock): with ( self.get_root_span(), ThreadPoolExecutor(max_workers=1) as executor, ): future = executor.submit(self.get_current_span_context_for_test) future.result() mock_detach.assert_not_called() @patch( "opentelemetry.context._RUNTIME_CONTEXT.attach", new=MagicMock(return_value=MagicMock()), ) @patch( "opentelemetry.context._RUNTIME_CONTEXT.detach", new=MagicMock(return_value=None), ) @patch("opentelemetry.context.detach", autospec=True) def test_threadpool_with_valid_context_token(self, mock_detach: MagicMock): with ( self.get_root_span(), ThreadPoolExecutor(max_workers=1) as executor, ): future = executor.submit(self.get_current_span_context_for_test) future.result() mock_detach.assert_called_once() def test_threading_run_without_start(self): square_thread = threading.Thread(target=self.print_square, args=(10,)) with self._tracer.start_as_current_span("root"): square_thread.run() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) root_span = next(span for span in spans if span.name == "root") self.assertIsNotNone(root_span) self.assertIsNone(root_span.parent) square_span = next(span for span in spans if span.name == "square") self.assertIsNotNone(square_span) self.assertIs(square_span.parent, root_span.get_span_context()) def test_threading_run_with_custom_run(self): _tracer = self._tracer class ThreadWithCustomRun(threading.Thread): def run(self): # don't call super().run() on purpose # Thread.run() cannot be called twice with _tracer.start_as_current_span("square"): pass square_thread = ThreadWithCustomRun(target=self.print_square, args=(10,)) with self._tracer.start_as_current_span("run_1"): square_thread.run() with self._tracer.start_as_current_span("run_2"): square_thread.run() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 4) run_1_span = next(span for span in spans if span.name == "run_1") run_2_span = next(span for span in spans if span.name == "run_2") square_spans = [span for span in spans if span.name == "square"] square_spans.sort(key=lambda x: x.start_time or 0) run_1_child_span = square_spans[0] run_2_child_span = square_spans[1] self.assertIs(run_1_child_span.parent, run_1_span.get_span_context()) self.assertIs(run_2_child_span.parent, run_2_span.get_span_context()) self.assertIsNone(run_1_span.parent) self.assertIsNone(run_2_span.parent) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornado/000077500000000000000000000000001527070072300326115ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornado/LICENSE000066400000000000000000000261351527070072300336250ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornado/README.rst000066400000000000000000000014111527070072300342750ustar00rootroot00000000000000OpenTelemetry Tornado Instrumentation ====================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-tornado.svg :target: https://pypi.org/project/opentelemetry-instrumentation-tornado/ This library builds on the OpenTelemetry WSGI middleware to track web requests in Tornado applications. Installation ------------ :: pip install opentelemetry-instrumentation-tornado References ---------- * `OpenTelemetry Tornado Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031351527070072300354500ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornado[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-tornado" dynamic = ["version"] description = "Tornado instrumentation for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-util-http == 0.66b2", ] [project.optional-dependencies] instruments = [ "tornado >= 5.1.1", ] [project.entry-points.opentelemetry_instrumentor] tornado = "opentelemetry.instrumentation.tornado:TornadoInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-tornado" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/tornado/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornado/src/000077500000000000000000000000001527070072300334005ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300362155ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornado/srcinstrumentation/000077500000000000000000000000001527070072300414605ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetrytornado/000077500000000000000000000000001527070072300431265ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation__init__.py000066400000000000000000001016501527070072300452420ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines """ This library uses OpenTelemetry to track web requests in Tornado applications. Usage ----- .. code-block:: python import tornado.web from opentelemetry.instrumentation.tornado import TornadoInstrumentor # apply tornado instrumentation TornadoInstrumentor().instrument() class Handler(tornado.web.RequestHandler): def get(self): self.set_status(200) app = tornado.web.Application([(r"/", Handler)]) app.listen(8080) tornado.ioloop.IOLoop.current().start() Configuration ------------- The following environment variables are supported as configuration options: - ``OTEL_PYTHON_TORNADO_EXCLUDED_URLS`` (or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) A comma separated list of paths that should not be automatically traced. For example, if this is set to :: export OTEL_PYTHON_TORNADO_EXCLUDED_URLS='/healthz,/ping' Then any requests made to ``/healthz`` and ``/ping`` will not be automatically traced. Request attributes ****************** To extract certain attributes from Tornado's request object and use them as span attributes, set the environment variable ``OTEL_PYTHON_TORNADO_TRACED_REQUEST_ATTRS`` to a comma delimited list of request attribute names. For example, :: export OTEL_PYTHON_TORNADO_TRACED_REQUEST_ATTRS='uri,query' will extract path_info and content_type attributes from every traced request and add them as span attributes. Request/Response hooks ********************** Tornado instrumentation supports extending tracing behaviour with the help of hooks. Its ``instrument()`` method accepts three optional functions that get called back with the created span and some other contextual information. Example: .. code-block:: python from opentelemetry.instrumentation.tornado import TornadoInstrumentor # will be called for each incoming request to Tornado # web server. `handler` is an instance of # `tornado.web.RequestHandler`. def server_request_hook(span, handler): pass # will be called just before sending out a request with # `tornado.httpclient.AsyncHTTPClient.fetch`. # `request` is an instance of ``tornado.httpclient.HTTPRequest`. def client_request_hook(span, request): pass # will be called after a outgoing request made with # `tornado.httpclient.AsyncHTTPClient.fetch` finishes. # `response`` is an instance of ``Future[tornado.httpclient.HTTPResponse]`. def client_response_hook(span, future): pass # apply tornado instrumentation with hooks TornadoInstrumentor().instrument( server_request_hook=server_request_hook, client_request_hook=client_request_hook, client_response_hook=client_response_hook, ) Capture HTTP request and response headers ***************************************** You can configure the agent to capture predefined HTTP headers as span attributes, according to the `semantic conventions `_. Request headers *************** To capture predefined HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma-separated list of HTTP header names. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" will extract ``content-type`` and ``custom_request_header`` from request headers and add them as span attributes. It is recommended that you should give the correct names of the headers to be captured in the environment variable. Request header names in tornado are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``. The name of the added span attribute will follow the format ``http.request.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). The value of the attribute will be single item list containing all the header values. Example of the added span attribute, ``http.request.header.custom_request_header = [","]`` Response headers **************** To capture predefined HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma-separated list of HTTP header names. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" will extract ``content-type`` and ``custom_response_header`` from response headers and add them as span attributes. It is recommended that you should give the correct names of the headers to be captured in the environment variable. Response header names captured in tornado are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``. The name of the added span attribute will follow the format ``http.response.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). The value of the attribute will be single item list containing all the header values. Example of the added span attribute, ``http.response.header.custom_response_header = [","]`` Note: Environment variable names to capture http headers are still experimental, and thus are subject to change. API --- """ from collections import namedtuple from collections.abc import Collection from functools import partial from logging import getLogger from time import time_ns from timeit import default_timer import tornado.web import tornado.websocket import wrapt from wrapt import wrap_function_wrapper from opentelemetry import context, trace from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, _get_schema_url, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _report_new, _report_old, _set_http_flavor_version, _set_http_host_server, _set_http_method, _set_http_scheme, _set_http_target, _set_http_url, _set_http_user_agent, _set_status, _StabilityMode, ) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.propagators import ( FuncSetter, get_global_response_propagator, ) from opentelemetry.instrumentation.tornado._utils import ( find_matched_rule, route_from_rule, ) from opentelemetry.instrumentation.tornado.package import _instruments from opentelemetry.instrumentation.tornado.version import __version__ from opentelemetry.instrumentation.utils import ( _start_internal_or_server_span, extract_attributes_from_object, unwrap, ) from opentelemetry.metrics import get_meter from opentelemetry.metrics._internal.instrument import Histogram from opentelemetry.propagators import textmap from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_CLIENT_IP, HTTP_FLAVOR, HTTP_HOST, HTTP_METHOD, HTTP_ROUTE, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_TARGET, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_IP, ) from opentelemetry.semconv.attributes.client_attributes import ( CLIENT_ADDRESS, ) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, ) from opentelemetry.semconv.attributes.network_attributes import ( NETWORK_PEER_ADDRESS, NETWORK_PROTOCOL_VERSION, ) from opentelemetry.semconv.attributes.url_attributes import ( URL_SCHEME, ) from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, _parse_url_query, get_custom_headers, get_excluded_urls, get_traced_request_attrs, normalise_request_header_name, normalise_response_header_name, normalize_user_agent, redact_url, sanitize_method, ) from .client import fetch_async # pylint: disable=E0401 _logger = getLogger(__name__) _TraceContext = namedtuple("TraceContext", ["activation", "span", "token"]) _HANDLER_STATE_KEY = "_otel_state_key" _HANDLER_CONTEXT_KEY = "_otel_trace_context_key" _OTEL_PATCHED_KEY = "_otel_patched_key" _START_TIME = "start_time" _excluded_urls = get_excluded_urls("TORNADO") _traced_request_attrs = get_traced_request_attrs("TORNADO") response_propagation_setter = FuncSetter(tornado.web.RequestHandler.add_header) class TornadoInstrumentor(BaseInstrumentor): patched_handlers = [] original_handler_new = None def __init__(self): super().__init__() self._sem_conv_opt_in_mode = _StabilityMode.DEFAULT def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): # pylint: disable=too-many-locals """ _instrument patches tornado.web.RequestHandler and tornado.httpclient.AsyncHTTPClient classes to automatically instrument requests both received and sent by Tornado. We don't patch RequestHandler._execute as it causes some issues with contextvars based context. Mainly the context fails to detach from within RequestHandler.on_finish() if it is attached inside RequestHandler._execute. Same issue plagues RequestHandler.initialize. RequestHandler.prepare works perfectly on the other hand as it executes in the same context as on_finish and log_exection which are patched to finish a span after a request is served. However, we cannot just patch RequestHandler's prepare method because it is supposed to be overridden by sub-classes and since the parent prepare method does not do anything special, sub-classes don't have to call super() when overriding the method. In order to work around this, we patch the __init__ method of RequestHandler and then dynamically patch the prepare, on_finish and log_exception methods of the derived classes _only_ the first time we see them. Note that the patch does not apply on every single __init__ call, only the first one for the entire process lifetime. """ # Initialize semantic conventions opt-in mode _OpenTelemetrySemanticConventionStability._initialize() sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) self._sem_conv_opt_in_mode = sem_conv_opt_in_mode tracer_provider = kwargs.get("tracer_provider") tracer = trace.get_tracer( __name__, __version__, tracer_provider, schema_url=_get_schema_url(sem_conv_opt_in_mode), ) meter_provider = kwargs.get("meter_provider") # Create meters for old and new semconv based on opt-in mode meter_old = None meter_new = None if _report_old(sem_conv_opt_in_mode): meter_old = get_meter( __name__, __version__, meter_provider, schema_url=_get_schema_url(_StabilityMode.DEFAULT), ) if _report_new(sem_conv_opt_in_mode): meter_new = get_meter( __name__, __version__, meter_provider, schema_url=_get_schema_url(_StabilityMode.HTTP), ) client_histograms = _create_client_histograms(meter_old, meter_new, sem_conv_opt_in_mode) server_histograms = _create_server_histograms(meter_old, meter_new, sem_conv_opt_in_mode) client_request_hook = kwargs.get("client_request_hook", None) client_response_hook = kwargs.get("client_response_hook", None) server_request_hook = kwargs.get("server_request_hook", None) def handler_init(init, handler, args, kwargs): cls = handler.__class__ if patch_handler_class( tracer, server_histograms, cls, server_request_hook, sem_conv_opt_in_mode, ): self.patched_handlers.append(cls) return init(*args, **kwargs) wrap_function_wrapper("tornado.web", "RequestHandler.__init__", handler_init) duration_old = client_histograms.get("old_duration") duration_new = client_histograms.get("new_duration") request_size_old = client_histograms.get("old_request_size") request_size_new = client_histograms.get("new_request_size") response_size_old = client_histograms.get("old_response_size") response_size_new = client_histograms.get("new_response_size") wrap_function_wrapper( "tornado.httpclient", "AsyncHTTPClient.fetch", partial( fetch_async, tracer, client_request_hook, client_response_hook, duration_old, duration_new, request_size_old, request_size_new, response_size_old, response_size_new, sem_conv_opt_in_mode, ), ) def _uninstrument(self, **kwargs): self._sem_conv_opt_in_mode = _StabilityMode.DEFAULT unwrap(tornado.web.RequestHandler, "__init__") unwrap(tornado.httpclient.AsyncHTTPClient, "fetch") for handler in self.patched_handlers: unpatch_handler_class(handler) self.patched_handlers = [] def _create_server_histograms(meter_old, meter_new, sem_conv_opt_in_mode) -> dict[str, Histogram]: histograms = {} # Create old semconv metrics if _report_old(sem_conv_opt_in_mode): histograms["old_duration"] = meter_old.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", description="measures the duration of inbound HTTP requests", ) histograms["old_request_size"] = meter_old.create_histogram( name=MetricInstruments.HTTP_SERVER_REQUEST_SIZE, unit="By", description="measures the size of HTTP request messages (compressed)", ) histograms["old_response_size"] = meter_old.create_histogram( name=MetricInstruments.HTTP_SERVER_RESPONSE_SIZE, unit="By", description="measures the size of HTTP response messages (compressed)", ) # Create new semconv metrics if _report_new(sem_conv_opt_in_mode): histograms["new_duration"] = meter_new.create_histogram( name="http.server.request.duration", unit="s", description="Duration of HTTP server requests.", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) histograms["new_request_size"] = meter_new.create_histogram( name="http.server.request.body.size", unit="By", description="Size of HTTP server request bodies.", ) histograms["new_response_size"] = meter_new.create_histogram( name="http.server.response.body.size", unit="By", description="Size of HTTP server response bodies.", ) # Active request counter for old/new semantic conventions same # because the attributes are the same for both # Use meter_old if available, otherwise meter_new active_meter = meter_old if meter_old is not None else meter_new if active_meter is not None: histograms["active_requests"] = active_meter.create_up_down_counter( name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, unit="requests", description="measures the number of concurrent HTTP requests that are currently in-flight", ) return histograms def _create_client_histograms(meter_old, meter_new, sem_conv_opt_in_mode) -> dict[str, Histogram]: histograms = {} # Create old semconv metrics if _report_old(sem_conv_opt_in_mode): histograms["old_duration"] = meter_old.create_histogram( name=MetricInstruments.HTTP_CLIENT_DURATION, unit="ms", description="measures the duration outbound HTTP requests", ) histograms["old_request_size"] = meter_old.create_histogram( name=MetricInstruments.HTTP_CLIENT_REQUEST_SIZE, unit="By", description="measures the size of HTTP request messages (compressed)", ) histograms["old_response_size"] = meter_old.create_histogram( name=MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE, unit="By", description="measures the size of HTTP response messages (compressed)", ) # Create new semconv metrics if _report_new(sem_conv_opt_in_mode): histograms["new_duration"] = meter_new.create_histogram( name="http.client.request.duration", unit="s", description="Duration of HTTP client requests.", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) histograms["new_request_size"] = meter_new.create_histogram( name="http.client.request.body.size", unit="By", description="Size of HTTP client request bodies.", ) histograms["new_response_size"] = meter_new.create_histogram( name="http.client.response.body.size", unit="By", description="Size of HTTP client response bodies.", ) return histograms def patch_handler_class( tracer, server_histograms, cls, request_hook=None, sem_conv_opt_in_mode=_StabilityMode.DEFAULT, ): if getattr(cls, _OTEL_PATCHED_KEY, False): return False setattr(cls, _OTEL_PATCHED_KEY, True) _wrap( cls, "prepare", partial( _prepare, tracer, server_histograms, request_hook, sem_conv_opt_in_mode, ), ) _wrap( cls, "log_exception", partial(_log_exception, tracer, server_histograms, sem_conv_opt_in_mode), ) if issubclass(cls, tornado.websocket.WebSocketHandler): _wrap( cls, "on_close", partial( _websockethandler_on_close, tracer, server_histograms, sem_conv_opt_in_mode, ), ) else: _wrap( cls, "on_finish", partial(_on_finish, tracer, server_histograms, sem_conv_opt_in_mode), ) return True def unpatch_handler_class(cls): if not getattr(cls, _OTEL_PATCHED_KEY, False): return unwrap(cls, "prepare") unwrap(cls, "log_exception") if issubclass(cls, tornado.websocket.WebSocketHandler): unwrap(cls, "on_close") else: unwrap(cls, "on_finish") delattr(cls, _OTEL_PATCHED_KEY) def _wrap(cls, method_name, wrapper): original = getattr(cls, method_name) wrapper = wrapt.FunctionWrapper(original, wrapper) wrapt.apply_patch(cls, method_name, wrapper) def _prepare( tracer, server_histograms, request_hook, sem_conv_opt_in_mode, func, handler, args, kwargs, ): request = handler.request otel_handler_state = { _START_TIME: default_timer(), "exclude_request": _excluded_urls.url_disabled(request.uri), } setattr(handler, _HANDLER_STATE_KEY, otel_handler_state) if otel_handler_state["exclude_request"]: return func(*args, **kwargs) _record_prepare_metrics(server_histograms, handler, sem_conv_opt_in_mode) ctx = _start_span(tracer, handler, sem_conv_opt_in_mode) if request_hook: request_hook(ctx.span, handler) return func(*args, **kwargs) def _on_finish( tracer, server_histograms, sem_conv_opt_in_mode, func, handler, args, kwargs, ): try: return func(*args, **kwargs) finally: _record_on_finish_metrics(server_histograms, handler, None, sem_conv_opt_in_mode) _finish_span(tracer, handler, None, sem_conv_opt_in_mode) def _websockethandler_on_close( tracer, server_histograms, sem_conv_opt_in_mode, func, handler, args, kwargs, ): try: func() finally: _record_on_finish_metrics(server_histograms, handler, None, sem_conv_opt_in_mode) _finish_span(tracer, handler, None, sem_conv_opt_in_mode) def _log_exception( tracer, server_histograms, sem_conv_opt_in_mode, func, handler, args, kwargs, ): error = None if len(args) == 3: error = args[1] _record_on_finish_metrics(server_histograms, handler, error, sem_conv_opt_in_mode) _finish_span(tracer, handler, error, sem_conv_opt_in_mode) return func(*args, **kwargs) def _collect_custom_request_headers_attributes(request_headers): custom_request_headers_name = get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST) attributes = {} for header_name in custom_request_headers_name: header_values = request_headers.get(header_name) if header_values: key = normalise_request_header_name(header_name.lower()) attributes[key] = [header_values] return attributes def _collect_custom_response_headers_attributes(response_headers): custom_response_headers_name = get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE) attributes = {} for header_name in custom_response_headers_name: header_values = response_headers.get(header_name) if header_values: key = normalise_response_header_name(header_name.lower()) attributes[key] = [header_values] return attributes def _get_attributes_from_request(request, sem_conv_opt_in_mode): attrs = {} # Set attributes based on semconv mode _set_http_method( attrs, request.method, sanitize_method(request.method), sem_conv_opt_in_mode, ) _set_http_scheme(attrs, request.protocol, sem_conv_opt_in_mode) _set_http_host_server(attrs, request.host, sem_conv_opt_in_mode) uri = redact_url(request.uri) _, query = _parse_url_query(uri) _set_http_target( attrs, uri, request.path, query, sem_conv_opt_in_mode, ) _set_http_url(attrs, redact_url(request.uri), sem_conv_opt_in_mode) user_agent = request.headers.get("user-agent") if user_agent: _set_http_user_agent( attrs, normalize_user_agent(user_agent), sem_conv_opt_in_mode, ) # HTTP version if request.version: _set_http_flavor_version(attrs, request.version, sem_conv_opt_in_mode) if request.remote_ip: # Client IP address # e.g. if Tornado is set to trust X-Forwarded-For headers (xheaders=True) if _report_old(sem_conv_opt_in_mode): attrs[HTTP_CLIENT_IP] = request.remote_ip if _report_new(sem_conv_opt_in_mode): attrs[CLIENT_ADDRESS] = request.remote_ip # Network peer IP if different from remote_ip if hasattr(request.connection, "context") and getattr(request.connection.context, "_orig_remote_ip", None): if _report_old(sem_conv_opt_in_mode): attrs[NET_PEER_IP] = request.connection.context._orig_remote_ip if _report_new(sem_conv_opt_in_mode): attrs[NETWORK_PEER_ADDRESS] = request.connection.context._orig_remote_ip return extract_attributes_from_object(request, _traced_request_attrs, attrs) def _get_default_span_name(handler): """ Default span name is the HTTP method and route, or just the method. https://github.com/open-telemetry/opentelemetry-specification/pull/3165 https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#name Args: handler: Tornado handler object. Returns: Default span name. """ method = sanitize_method(handler.request.method) if method == "_OTHER": method = "HTTP" rule = find_matched_rule(handler) # if there's no rule, like for 404 just return the method route = route_from_rule(rule, handler) if rule else None if method and route: return f"{method} {route}" return f"{method}" def _get_full_handler_name(handler): klass = type(handler) return f"{klass.__module__}.{klass.__qualname__}" def _start_span(tracer, handler, sem_conv_opt_in_mode) -> _TraceContext: attributes = _get_attributes_from_request(handler.request, sem_conv_opt_in_mode) span, token = _start_internal_or_server_span( tracer=tracer, span_name=_get_default_span_name(handler), start_time=time_ns(), context_carrier=handler.request.headers, context_getter=textmap.default_getter, attributes=attributes, ) if span.is_recording(): span.set_attribute("tornado.handler", _get_full_handler_name(handler)) if span.kind == trace.SpanKind.SERVER: custom_attributes = _collect_custom_request_headers_attributes(handler.request.headers) if len(custom_attributes) > 0: span.set_attributes(custom_attributes) activation = trace.use_span(span, end_on_exit=True) activation.__enter__() # pylint: disable=unnecessary-dunder-call ctx = _TraceContext(activation, span, token) setattr(handler, _HANDLER_CONTEXT_KEY, ctx) # finish handler is called after the response is sent back to # the client so it is too late to inject trace response headers # there. propagator = get_global_response_propagator() if propagator: propagator.inject(handler, setter=response_propagation_setter) return ctx def _finish_span(tracer, handler, error, sem_conv_opt_in_mode): status_code = handler.get_status() finish_args = (None, None, None) ctx = getattr(handler, _HANDLER_CONTEXT_KEY, None) if error: if isinstance(error, tornado.web.HTTPError): status_code = error.status_code if not ctx and status_code in (404, 405): ctx = _start_span(tracer, handler, sem_conv_opt_in_mode) else: status_code = 500 if status_code >= 500: finish_args = ( type(error), error, getattr(error, "__traceback__", None), ) if not ctx: return if ctx.span.is_recording(): metric_attributes = {} _set_status( ctx.span, metric_attributes, status_code, str(status_code) if status_code else None, server_span=True, sem_conv_opt_in_mode=sem_conv_opt_in_mode, ) if ctx.span.is_recording() and ctx.span.kind == trace.SpanKind.SERVER: custom_attributes = _collect_custom_response_headers_attributes(handler._headers) if len(custom_attributes) > 0: ctx.span.set_attributes(custom_attributes) ctx.activation.__exit__(*finish_args) # pylint: disable=unnecessary-dunder-call if ctx.token: context.detach(ctx.token) delattr(handler, _HANDLER_CONTEXT_KEY) def _record_prepare_metrics(server_histograms, handler, sem_conv_opt_in_mode): request_size = int(handler.request.headers.get("Content-Length", 0)) # Record old semconv metrics if _report_old(sem_conv_opt_in_mode): metric_attributes_old = _create_metric_attributes_old(handler) server_histograms["old_request_size"].record(request_size, attributes=metric_attributes_old) active_requests_attributes_old = _create_active_requests_attributes_old(handler) # in case of http/dup send both old and new attributes since we will add one entry if _report_new(sem_conv_opt_in_mode): active_requests_attributes_new = _create_active_requests_attributes_new(handler.request) active_requests_attributes_old_or_dup = active_requests_attributes_old | active_requests_attributes_new else: active_requests_attributes_old_or_dup = active_requests_attributes_old server_histograms["active_requests"].add(1, attributes=active_requests_attributes_old_or_dup) # Record new semconv metrics if _report_new(sem_conv_opt_in_mode): metric_attributes_new = _create_metric_attributes_new(handler) server_histograms["new_request_size"].record(request_size, attributes=metric_attributes_new) # Don't add to active_requests again if already added in old mode if not _report_old(sem_conv_opt_in_mode): active_requests_attributes_new = _create_active_requests_attributes_new(handler.request) server_histograms["active_requests"].add(1, attributes=active_requests_attributes_new) def _record_on_finish_metrics(server_histograms, handler, error, sem_conv_opt_in_mode): otel_handler_state = getattr(handler, _HANDLER_STATE_KEY, None) or {} if otel_handler_state.get("exclude_request"): return start_time = otel_handler_state.get(_START_TIME, None) or default_timer() elapsed_time_s = default_timer() - start_time elapsed_time_ms = round(elapsed_time_s * 1000) response_size = int(handler._headers.get("Content-Length", 0)) status_code = handler.get_status() if isinstance(error, tornado.web.HTTPError): status_code = error.status_code # Record old semconv metrics if _report_old(sem_conv_opt_in_mode): metric_attributes_old = _create_metric_attributes_old(handler) if isinstance(error, tornado.web.HTTPError): metric_attributes_old[HTTP_STATUS_CODE] = status_code server_histograms["old_response_size"].record(response_size, attributes=metric_attributes_old) server_histograms["old_duration"].record(elapsed_time_ms, attributes=metric_attributes_old) active_requests_attributes_old = _create_active_requests_attributes_old(handler) # in case of http/dup send both old and new attributes since we will add one entry if _report_new(sem_conv_opt_in_mode): active_requests_attributes_new = _create_active_requests_attributes_new(handler.request) active_requests_attributes_old_or_dup = active_requests_attributes_old | active_requests_attributes_new else: active_requests_attributes_old_or_dup = active_requests_attributes_old server_histograms["active_requests"].add(-1, attributes=active_requests_attributes_old_or_dup) # Record new semconv metrics if _report_new(sem_conv_opt_in_mode): metric_attributes_new = _create_metric_attributes_new(handler) if isinstance(error, tornado.web.HTTPError): metric_attributes_new[HTTP_RESPONSE_STATUS_CODE] = status_code server_histograms["new_response_size"].record(response_size, attributes=metric_attributes_new) server_histograms["new_duration"].record(elapsed_time_s, attributes=metric_attributes_new) # Don't subtract from active_requests again if already done in old mode if not _report_old(sem_conv_opt_in_mode): active_requests_attributes_new = _create_active_requests_attributes_new(handler.request) server_histograms["active_requests"].add(-1, attributes=active_requests_attributes_new) def _create_active_requests_attributes_old(handler): """Create metric attributes for active requests using old semconv.""" request = handler.request metric_attributes = { HTTP_METHOD: sanitize_method(request.method), HTTP_SCHEME: request.protocol, HTTP_FLAVOR: request.version, HTTP_HOST: request.host, } if rule := find_matched_rule(handler): route = route_from_rule(rule, handler) if route is not None: metric_attributes[HTTP_TARGET] = route return metric_attributes def _create_active_requests_attributes_new(request): """Create metric attributes for active requests using new semconv.""" metric_attributes = { HTTP_REQUEST_METHOD: sanitize_method(request.method), URL_SCHEME: request.protocol, } if request.version: metric_attributes[NETWORK_PROTOCOL_VERSION] = request.version return metric_attributes def _create_metric_attributes_old(handler): """Create metric attributes using old semconv.""" metric_attributes = _create_active_requests_attributes_old(handler) metric_attributes[HTTP_STATUS_CODE] = handler.get_status() return metric_attributes def _create_metric_attributes_new(handler): """Create metric attributes using new semconv.""" metric_attributes = _create_active_requests_attributes_new(handler.request) metric_attributes[HTTP_RESPONSE_STATUS_CODE] = handler.get_status() if handler.request.path: rule = find_matched_rule(handler) if rule: route = route_from_rule(rule, handler) if route is not None: metric_attributes[HTTP_ROUTE] = route return metric_attributes _utils.py000066400000000000000000000056141527070072300450050ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import inspect from typing import Any from tornado import httputil from tornado.routing import Router, Rule from tornado.web import RequestHandler # Distinguishes "keep scanning sibling rules" from "matched a branch we can't # inspect further, so stop and report no reliable result". _NOT_FOUND = object() def find_matched_rule(handler: RequestHandler) -> Rule | None: result = _find_rule( handler.application.default_router, handler.request, handler.__class__, ) return None if result is _NOT_FOUND else result # pylint: disable=too-many-return-statements def _find_rule( router: Any, request: httputil.HTTPServerRequest, handler_class: type[RequestHandler], ) -> Rule | _NOT_FOUND | None: rules = getattr(router, "rules", None) if rules is None: # Opaque custom router; cannot inspect reliably. return _NOT_FOUND for rule in rules: params = rule.matcher.match(request) if params is None: continue target = getattr(rule, "target", None) if _is_handler_target(target): if target is handler_class: return rule # A different handler matched first, so Tornado would stop here too. return _NOT_FOUND if hasattr(target, "rules"): nested = _find_rule(target, request, handler_class) if nested is None: # Nested router did not resolve anything; keep scanning siblings. continue return nested if isinstance(target, Router): # Custom nested router matched, but we cannot see inside it. return _NOT_FOUND # Callable / connection delegate / other terminal target. return _NOT_FOUND return None def _is_handler_target(target: Any) -> bool: return isinstance(target, type) and issubclass(target, RequestHandler) def route_from_rule(rule: Rule, handler: RequestHandler) -> str | None: """Return a path with the dynamic parts as named parameters to reduce cardinality""" route = None if not hasattr(rule.matcher, "_find_groups"): return None format_str, num_params = rule.matcher._find_groups() if num_params is None: return None if num_params == 0: # for regex routes without captured parameters, use the matcher format string # to preserve a low-cardinality route value return format_str method = getattr(handler, handler.request.method.lower()) # wrap the parameters with curly brackets so we can distinguish them from the fixed path method_args = tuple(f"{{{param}}}" for param in inspect.signature(method).parameters.keys()) if len(method_args) == num_params: route = format_str % method_args else: route = format_str return route client.py000066400000000000000000000162301527070072300447600ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import functools from time import time_ns from tornado.httpclient import HTTPError, HTTPRequest from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( _report_new, _report_old, _set_http_method, _set_http_url, _set_status, ) from opentelemetry.propagate import inject from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_METHOD, HTTP_STATUS_CODE, HTTP_URL, ) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.semconv.attributes.url_attributes import URL_FULL from opentelemetry.util.http import redact_url, sanitize_method def _normalize_request(args, kwargs): req = args[0] if not isinstance(req, str): return (args, kwargs) new_kwargs = {} for param in ("callback", "raise_error"): if param in kwargs: new_kwargs[param] = kwargs.pop(param) req = HTTPRequest(req, **kwargs) new_args = [req] new_args.extend(args[1:]) return (new_args, new_kwargs) def fetch_async( # pylint: disable=too-many-locals tracer, request_hook, response_hook, duration_histogram_old, duration_histogram_new, request_size_histogram_old, request_size_histogram_new, response_size_histogram_old, response_size_histogram_new, sem_conv_opt_in_mode, func, _, args, kwargs, ): start_time = time_ns() # Return immediately if no args were provided (error) # or original_request is set (meaning we are in a redirect step). if len(args) == 0 or hasattr(args[0], "original_request"): return func(*args, **kwargs) # Force the creation of a HTTPRequest object if needed, # so we can inject the context into the headers. args, kwargs = _normalize_request(args, kwargs) request = args[0] span_name = sanitize_method(request.method) if span_name == "_OTHER": span_name = "HTTP" span = tracer.start_span( span_name, kind=trace.SpanKind.CLIENT, start_time=start_time, ) if request_hook: request_hook(span, request) if span.is_recording(): attributes = {} _set_http_url(attributes, redact_url(request.url), sem_conv_opt_in_mode) _set_http_method( attributes, request.method, sanitize_method(request.method), sem_conv_opt_in_mode, ) for key, value in attributes.items(): span.set_attribute(key, value) with trace.use_span(span): inject(request.headers) future = func(*args, **kwargs) future.add_done_callback( functools.partial( _finish_tracing_callback, span=span, response_hook=response_hook, duration_histogram_old=duration_histogram_old, duration_histogram_new=duration_histogram_new, request_size_histogram_old=request_size_histogram_old, request_size_histogram_new=request_size_histogram_new, response_size_histogram_old=response_size_histogram_old, response_size_histogram_new=response_size_histogram_new, sem_conv_opt_in_mode=sem_conv_opt_in_mode, ) ) return future def _finish_tracing_callback( # pylint: disable=too-many-locals,too-many-branches future, span, response_hook, duration_histogram_old, duration_histogram_new, request_size_histogram_old, request_size_histogram_new, response_size_histogram_old, response_size_histogram_new, sem_conv_opt_in_mode, ): response = None status_code = None exc = future.exception() if exc: if isinstance(exc, HTTPError): response = exc.response status_code = exc.code else: span.record_exception(exc) else: response = future.result() status_code = response.code # Set status using semconv helper metric_attributes = {} _set_status( span, metric_attributes, status_code if status_code is not None else -1, str(status_code) if status_code is not None else "Exception", server_span=False, sem_conv_opt_in_mode=sem_conv_opt_in_mode, ) if response is not None: request_size = int(response.request.headers.get("Content-Length", 0)) response_size = int(response.headers.get("Content-Length", 0)) # Record old semconv metrics if _report_old(sem_conv_opt_in_mode) and duration_histogram_old is not None: metric_attributes_old = _create_metric_attributes_old(response) if duration_histogram_old: duration_histogram_old.record(response.request_time, attributes=metric_attributes_old) if request_size_histogram_old: request_size_histogram_old.record(request_size, attributes=metric_attributes_old) if response_size_histogram_old: response_size_histogram_old.record(response_size, attributes=metric_attributes_old) # Record new semconv metrics (duration in seconds) if _report_new(sem_conv_opt_in_mode) and duration_histogram_new is not None: metric_attributes_new = _create_metric_attributes_new(response) # Convert request_time from seconds to seconds (it's already in seconds) if duration_histogram_new: duration_histogram_new.record(response.request_time, attributes=metric_attributes_new) if request_size_histogram_new: request_size_histogram_new.record(request_size, attributes=metric_attributes_new) if response_size_histogram_new: response_size_histogram_new.record(response_size, attributes=metric_attributes_new) if response_hook: response_hook(span, future) span.end() def _create_metric_attributes_old(response): """Create metric attributes using old semconv.""" metric_attributes = { HTTP_STATUS_CODE: response.code, HTTP_URL: redact_url(response.request.url), HTTP_METHOD: sanitize_method(response.request.method), } return metric_attributes def _create_metric_attributes_new(response): """Create metric attributes using new semconv.""" metric_attributes = { HTTP_RESPONSE_STATUS_CODE: response.code, URL_FULL: redact_url(response.request.url), HTTP_REQUEST_METHOD: sanitize_method(response.request.method), } # Add server address and port if available if hasattr(response.request, "host") and response.request.host: host = response.request.host if ":" in host: server_address, port_str = host.rsplit(":", 1) metric_attributes[SERVER_ADDRESS] = server_address try: metric_attributes[SERVER_PORT] = int(port_str) except ValueError: pass else: metric_attributes[SERVER_ADDRESS] = host return metric_attributes package.py000066400000000000000000000002541527070072300450740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("tornado >= 5.1.1",) _supports_metrics = True _semconv_status = "migration" version.py000066400000000000000000000001441527070072300451640ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000006651527070072300370020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornadoasgiref==3.8.1 certifi==2024.7.4 charset-normalizer==3.3.2 click==8.1.7 Deprecated==1.2.14 http_server_mock==1.7 idna==3.7 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 requests==2.33.1 tomli==2.0.1 tornado==6.4.2 typing_extensions==4.12.2 urllib3==2.7.0 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-tornado opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornado/tests/000077500000000000000000000000001527070072300337535ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300357730ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornado/teststest_instrumentation.py000066400000000000000000001035221527070072300405530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornado/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio from unittest.mock import Mock, patch import tornado.websocket from http_server_mock import HttpServerMock from tornado.httpclient import HTTPClientError from tornado.testing import AsyncHTTPTestCase from opentelemetry import trace from opentelemetry.instrumentation.propagators import ( TraceResponsePropagator, get_global_response_propagator, set_global_response_propagator, ) from opentelemetry.instrumentation.tornado import ( TornadoInstrumentor, patch_handler_class, unpatch_handler_class, ) from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_CLIENT_IP, HTTP_HOST, HTTP_METHOD, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_TARGET, HTTP_URL, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_IP, ) from opentelemetry.test.test_base import TestBase from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.trace import SpanKind, StatusCode from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, get_excluded_urls, get_traced_request_attrs, ) from .tornado_test_app import ( AsyncHandler, DynamicHandler, MainHandler, make_app, ) class TornadoTest(AsyncHTTPTestCase, TestBase): # pylint:disable=no-self-use def get_app(self): tracer = trace.get_tracer(__name__) app = make_app(tracer) return app def setUp(self): super().setUp() # Reset semconv initialization to ensure clean state # pylint: disable=import-outside-toplevel from opentelemetry.instrumentation._semconv import ( # noqa: PLC0415 _OpenTelemetrySemanticConventionStability, ) _OpenTelemetrySemanticConventionStability._initialized = False # pylint: disable=protected-access TornadoInstrumentor().instrument( server_request_hook=getattr(self, "server_request_hook", None), client_request_hook=getattr(self, "client_request_hook", None), client_response_hook=getattr(self, "client_response_hook", None), ) # pylint: disable=protected-access self.env_patch = patch.dict( "os.environ", { "OTEL_PYTHON_TORNADO_EXCLUDED_URLS": "healthz,ping", "OTEL_PYTHON_TORNADO_TRACED_REQUEST_ATTRS": "uri,full_url,query", }, ) self.env_patch.start() self.exclude_patch = patch( "opentelemetry.instrumentation.tornado._excluded_urls", get_excluded_urls("TORNADO"), ) self.traced_patch = patch( "opentelemetry.instrumentation.tornado._traced_request_attrs", get_traced_request_attrs("TORNADO"), ) self.exclude_patch.start() self.traced_patch.start() def tearDown(self): TornadoInstrumentor().uninstrument() self.env_patch.stop() self.exclude_patch.stop() self.traced_patch.stop() super().tearDown() class TestTornadoInstrumentor(TornadoTest): def test_patch_references(self): self.assertEqual(len(TornadoInstrumentor().patched_handlers), 0) self.fetch("/") self.fetch("/async") self.assertEqual(TornadoInstrumentor().patched_handlers, [MainHandler, AsyncHandler]) self.fetch("/async") self.fetch("/") self.assertEqual(TornadoInstrumentor().patched_handlers, [MainHandler, AsyncHandler]) TornadoInstrumentor().uninstrument() self.assertEqual(TornadoInstrumentor().patched_handlers, []) def test_patch_applied_only_once(self): tracer = trace.get_tracer(__name__) self.assertTrue(patch_handler_class(tracer, {}, AsyncHandler)) self.assertFalse(patch_handler_class(tracer, {}, AsyncHandler)) self.assertFalse(patch_handler_class(tracer, {}, AsyncHandler)) unpatch_handler_class(AsyncHandler) class TestTornadoInstrumentation(TornadoTest, WsgiTestBase): def test_http_calls(self): methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] for method in methods: self._test_http_method_call(method) def _test_http_method_call(self, method): body = "" if method in ["POST", "PUT", "PATCH"] else None response = self.fetch("/", method=method, body=body) self.assertEqual(response.code, 201) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) manual, server, client = self.sorted_spans(spans) self.assertEqual(manual.name, "manual") self.assertEqual(manual.parent, server.context) self.assertEqual(manual.context.trace_id, client.context.trace_id) self.assertEqual(server.name, f"{method} /") self.assertTrue(server.parent.is_remote) self.assertNotEqual(server.parent, client.context) self.assertEqual(server.parent.span_id, client.context.span_id) self.assertEqual(server.context.trace_id, client.context.trace_id) self.assertEqual(server.kind, SpanKind.SERVER) self.assertSpanHasAttributes( server, { HTTP_METHOD: method, HTTP_SCHEME: "http", HTTP_HOST: "127.0.0.1:" + str(self.get_http_port()), HTTP_TARGET: "/", HTTP_CLIENT_IP: "127.0.0.1", HTTP_STATUS_CODE: 201, "tornado.handler": "tests.tornado_test_app.MainHandler", }, ) self.assertEqual(client.name, method) self.assertFalse(client.context.is_remote) self.assertIsNone(client.parent) self.assertEqual(client.kind, SpanKind.CLIENT) self.assertSpanHasAttributes( client, { HTTP_URL: self.get_url("/"), HTTP_METHOD: method, HTTP_STATUS_CODE: 201, }, ) self.memory_exporter.clear() def test_unknown_method_span_name(self): response = self.fetch("/", method="UNKNOWN", allow_nonstandard_methods=True) self.assertEqual(response.code, 405) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) server, client = self.sorted_spans(spans) self.assertEqual(server.name, "HTTP /") self.assertSpanHasAttributes( server, { HTTP_METHOD: "_OTHER", }, ) self.assertEqual(client.name, "HTTP") self.assertSpanHasAttributes( client, { HTTP_METHOD: "_OTHER", }, ) self.memory_exporter.clear() def test_not_recording(self): mock_tracer = Mock() mock_span = Mock() mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span with patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer self.fetch("/") self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) def test_async_handler(self): self._test_async_handler("/async", "AsyncHandler") def test_coroutine_handler(self): self._test_async_handler("/cor", "CoroutineHandler") def _test_async_handler(self, url, handler_name): response = self.fetch(url) self.assertEqual(response.code, 201) spans = self.get_finished_spans() self.assertEqual(len(spans), 5) client = spans.by_name("GET") server = spans.by_name(f"GET {url}") sub_wrapper = spans.by_name("sub-task-wrapper") sub2 = spans.by_name("sub-task-2") self.assertEqual(sub2.name, "sub-task-2") self.assertEqual(sub2.parent, sub_wrapper.context) self.assertEqual(sub2.context.trace_id, client.context.trace_id) sub1 = spans.by_name("sub-task-1") self.assertEqual(sub1.name, "sub-task-1") self.assertEqual(sub1.parent, sub_wrapper.context) self.assertEqual(sub1.context.trace_id, client.context.trace_id) self.assertEqual(sub_wrapper.name, "sub-task-wrapper") self.assertEqual(sub_wrapper.parent, server.context) self.assertEqual(sub_wrapper.context.trace_id, client.context.trace_id) self.assertEqual(server.name, f"GET {url}") self.assertTrue(server.parent.is_remote) self.assertNotEqual(server.parent, client.context) self.assertEqual(server.parent.span_id, client.context.span_id) self.assertEqual(server.context.trace_id, client.context.trace_id) self.assertEqual(server.kind, SpanKind.SERVER) self.assertSpanHasAttributes( server, { HTTP_METHOD: "GET", HTTP_SCHEME: "http", HTTP_HOST: "127.0.0.1:" + str(self.get_http_port()), HTTP_TARGET: url, HTTP_CLIENT_IP: "127.0.0.1", HTTP_STATUS_CODE: 201, "tornado.handler": f"tests.tornado_test_app.{handler_name}", }, ) self.assertEqual(client.name, "GET") self.assertFalse(client.context.is_remote) self.assertIsNone(client.parent) self.assertEqual(client.kind, SpanKind.CLIENT) self.assertSpanHasAttributes( client, { HTTP_URL: self.get_url(url), HTTP_METHOD: "GET", HTTP_STATUS_CODE: 201, }, ) def test_500(self): response = self.fetch("/error") self.assertEqual(response.code, 500) spans = self.get_finished_spans() self.assertEqual(len(spans), 2) client = spans.by_name("GET") server = spans.by_name("GET /error") self.assertEqual(server.name, "GET /error") self.assertEqual(server.kind, SpanKind.SERVER) self.assertSpanHasAttributes( server, { HTTP_METHOD: "GET", HTTP_SCHEME: "http", HTTP_HOST: "127.0.0.1:" + str(self.get_http_port()), HTTP_TARGET: "/error", HTTP_CLIENT_IP: "127.0.0.1", HTTP_STATUS_CODE: 500, "tornado.handler": "tests.tornado_test_app.BadHandler", }, ) self.assertEqual(client.name, "GET") self.assertEqual(client.kind, SpanKind.CLIENT) self.assertSpanHasAttributes( client, { HTTP_URL: self.get_url("/error"), HTTP_METHOD: "GET", HTTP_STATUS_CODE: 500, }, ) def test_404(self): response = self.fetch("/missing-url") self.assertEqual(response.code, 404) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 2) server, client = spans self.assertEqual(server.name, "GET") self.assertEqual(server.kind, SpanKind.SERVER) self.assertSpanHasAttributes( server, { HTTP_METHOD: "GET", HTTP_SCHEME: "http", HTTP_HOST: "127.0.0.1:" + str(self.get_http_port()), HTTP_TARGET: "/missing-url", HTTP_CLIENT_IP: "127.0.0.1", HTTP_STATUS_CODE: 404, "tornado.handler": "tornado.web.ErrorHandler", }, ) self.assertEqual(client.name, "GET") self.assertEqual(client.kind, SpanKind.CLIENT) self.assertSpanHasAttributes( client, { HTTP_URL: self.get_url("/missing-url"), HTTP_METHOD: "GET", HTTP_STATUS_CODE: 404, }, ) def test_parametrized_url(self): response = self.fetch("/parametrized/hello/") self.assertEqual(response.code, 200) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 2) server, client = spans self.assertEqual(server.name, "GET /parametrized/{message}/") self.assertEqual(server.kind, SpanKind.SERVER) self.assertSpanHasAttributes( server, { HTTP_METHOD: "GET", HTTP_SCHEME: "http", HTTP_HOST: "127.0.0.1:" + str(self.get_http_port()), HTTP_TARGET: "/parametrized/hello/", HTTP_CLIENT_IP: "127.0.0.1", HTTP_STATUS_CODE: 200, "tornado.handler": "tests.tornado_test_app.ParametrizedHandler", }, ) self.assertEqual(client.name, "GET") self.assertEqual(client.kind, SpanKind.CLIENT) self.assertSpanHasAttributes( client, { HTTP_URL: self.get_url("/parametrized/hello/"), HTTP_METHOD: "GET", HTTP_STATUS_CODE: 200, }, ) def test_no_group_url(self): response = self.fetch("/nogroup/0/") self.assertEqual(response.code, 200) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 2) server, client = spans self.assertEqual(server.name, "GET /nogroup/[0-9]/") self.assertEqual(server.kind, SpanKind.SERVER) self.assertSpanHasAttributes( server, { HTTP_METHOD: "GET", HTTP_SCHEME: "http", HTTP_HOST: "127.0.0.1:" + str(self.get_http_port()), HTTP_TARGET: "/nogroup/0/", HTTP_CLIENT_IP: "127.0.0.1", HTTP_STATUS_CODE: 200, "tornado.handler": "tests.tornado_test_app.NoGroupHandler", }, ) self.assertEqual(client.name, "GET") self.assertEqual(client.kind, SpanKind.CLIENT) self.assertSpanHasAttributes( client, { HTTP_URL: self.get_url("/nogroup/0/"), HTTP_METHOD: "GET", HTTP_STATUS_CODE: 200, }, ) def test_http_error(self): response = self.fetch("/raise_403") self.assertEqual(response.code, 403) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 2) server, client = spans self.assertEqual(server.name, "GET /raise_403") self.assertEqual(server.kind, SpanKind.SERVER) self.assertSpanHasAttributes( server, { HTTP_METHOD: "GET", HTTP_SCHEME: "http", HTTP_HOST: "127.0.0.1:" + str(self.get_http_port()), HTTP_TARGET: "/raise_403", HTTP_CLIENT_IP: "127.0.0.1", HTTP_STATUS_CODE: 403, "tornado.handler": "tests.tornado_test_app.RaiseHTTPErrorHandler", }, ) self.assertEqual(client.name, "GET") self.assertEqual(client.kind, SpanKind.CLIENT) self.assertSpanHasAttributes( client, { HTTP_URL: self.get_url("/raise_403"), HTTP_METHOD: "GET", HTTP_STATUS_CODE: 403, }, ) def test_dynamic_handler(self): response = self.fetch("/dyna") self.assertEqual(response.code, 404) self.memory_exporter.clear() self._app.add_handlers(r".+", [(r"/dyna", DynamicHandler)]) response = self.fetch("/dyna") self.assertEqual(response.code, 202) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 2) server, client = spans self.assertEqual(server.name, "GET /dyna") self.assertTrue(server.parent.is_remote) self.assertNotEqual(server.parent, client.context) self.assertEqual(server.parent.span_id, client.context.span_id) self.assertEqual(server.context.trace_id, client.context.trace_id) self.assertEqual(server.kind, SpanKind.SERVER) self.assertSpanHasAttributes( server, { HTTP_METHOD: "GET", HTTP_SCHEME: "http", HTTP_HOST: "127.0.0.1:" + str(self.get_http_port()), HTTP_TARGET: "/dyna", HTTP_CLIENT_IP: "127.0.0.1", HTTP_STATUS_CODE: 202, "tornado.handler": "tests.tornado_test_app.DynamicHandler", }, ) self.assertEqual(client.name, "GET") self.assertFalse(client.context.is_remote) self.assertIsNone(client.parent) self.assertEqual(client.kind, SpanKind.CLIENT) self.assertSpanHasAttributes( client, { HTTP_URL: self.get_url("/dyna"), HTTP_METHOD: "GET", HTTP_STATUS_CODE: 202, }, ) def test_handler_on_finish(self): response = self.fetch("/on_finish") self.assertEqual(response.code, 200) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 3) auditor, server, client = spans self.assertEqual(server.name, "GET /on_finish") self.assertTrue(server.parent.is_remote) self.assertNotEqual(server.parent, client.context) self.assertEqual(server.parent.span_id, client.context.span_id) self.assertEqual(server.context.trace_id, client.context.trace_id) self.assertEqual(server.kind, SpanKind.SERVER) self.assertSpanHasAttributes( server, { HTTP_METHOD: "GET", HTTP_SCHEME: "http", HTTP_HOST: "127.0.0.1:" + str(self.get_http_port()), HTTP_TARGET: "/on_finish", HTTP_CLIENT_IP: "127.0.0.1", HTTP_STATUS_CODE: 200, "tornado.handler": "tests.tornado_test_app.FinishedHandler", }, ) self.assertEqual(client.name, "GET") self.assertFalse(client.context.is_remote) self.assertIsNone(client.parent) self.assertEqual(client.kind, SpanKind.CLIENT) self.assertSpanHasAttributes( client, { HTTP_URL: self.get_url("/on_finish"), HTTP_METHOD: "GET", HTTP_STATUS_CODE: 200, }, ) self.assertEqual(auditor.name, "audit_task") self.assertFalse(auditor.context.is_remote) self.assertEqual(auditor.parent.span_id, server.context.span_id) self.assertEqual(auditor.context.trace_id, client.context.trace_id) self.assertEqual(auditor.kind, SpanKind.INTERNAL) @tornado.testing.gen_test() async def test_websockethandler(self): ws_client = await tornado.websocket.websocket_connect(f"ws://127.0.0.1:{self.get_http_port()}/echo_socket") await ws_client.write_message("world") resp = await ws_client.read_message() self.assertEqual(resp, "hello world") ws_client.close() await asyncio.sleep(0.5) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 3) close_span, msg_span, req_span = spans self.assertEqual(req_span.name, "GET /echo_socket") self.assertEqual(req_span.context.trace_id, msg_span.context.trace_id) self.assertIsNone(req_span.parent) self.assertEqual(req_span.kind, SpanKind.SERVER) self.assertSpanHasAttributes( req_span, { HTTP_METHOD: "GET", HTTP_SCHEME: "http", HTTP_HOST: f"127.0.0.1:{self.get_http_port()}", HTTP_TARGET: "/echo_socket", HTTP_CLIENT_IP: "127.0.0.1", HTTP_STATUS_CODE: 101, "tornado.handler": "tests.tornado_test_app.EchoWebSocketHandler", }, ) self.assertEqual(msg_span.name, "audit_message") self.assertFalse(msg_span.context.is_remote) self.assertEqual(msg_span.kind, SpanKind.INTERNAL) self.assertEqual(msg_span.parent.span_id, req_span.context.span_id) self.assertEqual(close_span.name, "audit_on_close") self.assertFalse(close_span.context.is_remote) self.assertEqual(close_span.parent.span_id, req_span.context.span_id) self.assertEqual(close_span.context.trace_id, msg_span.context.trace_id) self.assertEqual(close_span.kind, SpanKind.INTERNAL) def test_exclude_lists(self): def test_excluded(path): self.fetch(path) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 1) client = spans[0] self.assertEqual(client.name, "GET") self.assertEqual(client.kind, SpanKind.CLIENT) self.assertSpanHasAttributes( client, { HTTP_URL: self.get_url(path), HTTP_METHOD: "GET", HTTP_STATUS_CODE: 200, }, ) self.memory_exporter.clear() test_excluded("/healthz") test_excluded("/ping") def test_traced_attrs(self): self.fetch("/pong?q=abc&b=123") spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 2) server_span = spans[0] self.assertEqual(server_span.kind, SpanKind.SERVER) self.assertSpanHasAttributes(server_span, {"uri": "/pong?q=abc&b=123", "query": "q=abc&b=123"}) self.memory_exporter.clear() def test_response_headers(self): orig = get_global_response_propagator() set_global_response_propagator(TraceResponsePropagator()) response = self.fetch("/") spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 3) self.assertTraceResponseHeaderMatchesSpan(response.headers, spans[1]) set_global_response_propagator(orig) def test_remove_sensitive_params(self): app = HttpServerMock("test_remove_sensitive_params") @app.route("/status/200") def index(): return "hello" with app.run("localhost", 5000): response = self.fetch("http://username:password@localhost:5000/status/200?Signature=secret") self.assertEqual(response.code, 200) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 1) client = spans[0] self.assertEqual(client.name, "GET") self.assertEqual(client.kind, SpanKind.CLIENT) self.assertSpanHasAttributes( client, { HTTP_URL: "http://REDACTED:REDACTED@localhost:5000/status/200?Signature=REDACTED", HTTP_METHOD: "GET", HTTP_STATUS_CODE: 200, }, ) self.memory_exporter.clear() def test_invalid_request_uri_does_not_break_request(self): # `request.uri` comes straight off the request line and need not be a # parsable URL. Instrumentation must not change the outcome of such a # request; this target matches no route, so the app answers 404. It # previously became a 500 from urlparse raising in _parse_url_query. response = self.fetch("//exa[mple", raise_error=False) self.assertEqual(response.code, 404) spans = self.memory_exporter.get_finished_spans() server = next(span for span in spans if span.kind is SpanKind.SERVER) self.assertEqual(server.attributes[HTTP_TARGET], "//exa[mple") self.assertEqual(server.attributes[HTTP_STATUS_CODE], 404) class TestTornadoInstrumentationWithXHeaders(TornadoTest): def get_httpserver_options(self): # pylint: disable=no-self-use return {"xheaders": True} def test_xheaders(self): response = self.fetch("/", headers={"X-Forwarded-For": "12.34.56.78"}) self.assertEqual(response.code, 201) spans = self.get_finished_spans() self.assertSpanHasAttributes( spans.by_name("GET /"), { HTTP_METHOD: "GET", HTTP_SCHEME: "http", HTTP_HOST: "127.0.0.1:" + str(self.get_http_port()), HTTP_TARGET: "/", HTTP_CLIENT_IP: "12.34.56.78", HTTP_STATUS_CODE: 201, NET_PEER_IP: "127.0.0.1", "tornado.handler": "tests.tornado_test_app.MainHandler", }, ) class TornadoHookTest(TornadoTest): _client_request_hook = None _client_response_hook = None _server_request_hook = None def client_request_hook(self, span, handler): if self._client_request_hook is not None: self._client_request_hook(span, handler) def client_response_hook(self, span, handler): if self._client_response_hook is not None: self._client_response_hook(span, handler) def server_request_hook(self, span, handler): if self._server_request_hook is not None: self._server_request_hook(span, handler) def test_hooks(self): def server_request_hook(span, handler): span.update_name("name from server hook") handler.set_header("hello", "world") def client_request_hook(span, request): span.update_name("name from client hook") def client_response_hook(span, response): span.set_attribute("attr-from-hook", "value") self._server_request_hook = server_request_hook self._client_request_hook = client_request_hook self._client_response_hook = client_response_hook response = self.fetch("/") self.assertEqual(response.headers.get("hello"), "world") spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 3) server_span = spans[1] self.assertEqual(server_span.kind, SpanKind.SERVER) self.assertEqual(server_span.name, "name from server hook") self.assertSpanHasAttributes(server_span, {"uri": "/"}) self.memory_exporter.clear() client_span = spans[2] self.assertEqual(client_span.kind, SpanKind.CLIENT) self.assertEqual(client_span.name, "name from client hook") self.assertSpanHasAttributes(client_span, {"attr-from-hook": "value"}) self.memory_exporter.clear() class TestTornadoHTTPClientInstrumentation(TornadoTest, WsgiTestBase): def test_http_client_success_response(self): response = self.fetch("/") self.assertEqual(response.code, 201) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) manual, server, client = self.sorted_spans(spans) self.assertEqual(manual.name, "manual") self.assertEqual(server.name, "GET /") self.assertEqual(client.name, "GET") self.assertEqual(client.status.status_code, StatusCode.UNSET) self.memory_exporter.clear() def test_http_client_failed_response(self): # when an exception isn't thrown response = self.fetch("/some-404") self.assertEqual(response.code, 404) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) server, client = self.sorted_spans(spans) self.assertEqual(server.name, "GET") self.assertEqual(client.name, "GET") self.assertEqual(client.status.status_code, StatusCode.ERROR) self.memory_exporter.clear() # when an exception is thrown try: response = self.fetch("/some-404", raise_error=True) self.assertEqual(response.code, 404) except HTTPClientError: pass # expected exception - continue spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) server, client = self.sorted_spans(spans) self.assertEqual(server.name, "GET") self.assertEqual(client.name, "GET") self.assertEqual(client.status.status_code, StatusCode.ERROR) self.memory_exporter.clear() class TestTornadoUninstrument(TornadoTest): def test_uninstrument(self): response = self.fetch("/") self.assertEqual(response.code, 201) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) manual, server, client = self.sorted_spans(spans) self.assertEqual(manual.name, "manual") self.assertEqual(server.name, "GET /") self.assertEqual(client.name, "GET") self.memory_exporter.clear() TornadoInstrumentor().uninstrument() response = self.fetch("/") self.assertEqual(response.code, 201) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) manual = spans[0] self.assertEqual(manual.name, "manual") class TestTornadoWrappedWithOtherFramework(TornadoTest): def get_app(self): tracer = trace.get_tracer(__name__) app = make_app(tracer) def middleware(request): """Wraps the request with a server span""" with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER): app(request) return middleware def test_mark_span_internal_in_presence_of_another_span(self): response = self.fetch("/") self.assertEqual(response.code, 201) spans = self.sorted_spans(self.memory_exporter.get_finished_spans()) self.assertEqual(len(spans), 4) tornado_handler_span = spans[1] self.assertEqual(trace.SpanKind.INTERNAL, tornado_handler_span.kind) test_span = spans[2] self.assertEqual(trace.SpanKind.SERVER, test_span.kind) self.assertEqual(test_span.context.span_id, tornado_handler_span.parent.span_id) class TestTornadoCustomRequestResponseHeadersAddedWithServerSpan(TornadoTest): @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3" }, ) def test_custom_request_headers_added_in_server_span(self): headers = { "Custom-Test-Header-1": "Test Value 1", "Custom-Test-Header-2": "TestValue2,TestValue3", } response = self.fetch("/", headers=headers) self.assertEqual(response.code, 201) _, tornado_span, _ = self.sorted_spans(self.memory_exporter.get_finished_spans()) expected = { "http.request.header.custom_test_header_1": ("Test Value 1",), "http.request.header.custom_test_header_2": ("TestValue2,TestValue3",), } self.assertEqual(tornado_span.kind, trace.SpanKind.SERVER) self.assertSpanHasAttributes(tornado_span, expected) @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header" }, ) def test_custom_response_headers_added_in_server_span(self): response = self.fetch("/test_custom_response_headers") self.assertEqual(response.code, 200) tornado_span, _ = self.sorted_spans(self.memory_exporter.get_finished_spans()) expected = { "http.response.header.content_type": ("text/plain; charset=utf-8",), "http.response.header.content_length": ("0",), "http.response.header.my_custom_header": ("my-custom-value-1,my-custom-header-2",), } self.assertEqual(tornado_span.kind, trace.SpanKind.SERVER) self.assertSpanHasAttributes(tornado_span, expected) class TestTornadoCustomRequestResponseHeadersNotAddedWithInternalSpan(TornadoTest): def get_app(self): tracer = trace.get_tracer(__name__) app = make_app(tracer) def middleware(request): """Wraps the request with a server span""" with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER): app(request) return middleware @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3" }, ) def test_custom_request_headers_not_added_in_internal_span(self): headers = { "Custom-Test-Header-1": "Test Value 1", "Custom-Test-Header-2": "TestValue2,TestValue3", } response = self.fetch("/", headers=headers) self.assertEqual(response.code, 201) _, tornado_span, _, _ = self.sorted_spans(self.memory_exporter.get_finished_spans()) not_expected = { "http.request.header.custom_test_header_1": ("Test Value 1",), "http.request.header.custom_test_header_2": ("TestValue2,TestValue3",), } self.assertEqual(tornado_span.kind, trace.SpanKind.INTERNAL) for key, _ in not_expected.items(): self.assertNotIn(key, tornado_span.attributes) @patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header" }, ) def test_custom_response_headers_not_added_in_internal_span(self): response = self.fetch("/test_custom_response_headers") self.assertEqual(response.code, 200) tornado_span, _, _ = self.sorted_spans(self.memory_exporter.get_finished_spans()) not_expected = { "http.response.header.content_type": ("text/plain; charset=utf-8",), "http.response.header.content_length": ("0",), "http.response.header.my_custom_header": ("my-custom-value-1,my-custom-header-2",), } self.assertEqual(tornado_span.kind, trace.SpanKind.INTERNAL) for key, _ in not_expected.items(): self.assertNotIn(key, tornado_span.attributes) test_metrics_instrumentation.py000066400000000000000000000652671527070072300423160ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornado/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio import platform from timeit import default_timer from unittest.mock import patch import tornado.testing from tornado.testing import AsyncHTTPTestCase from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.tornado import TornadoInstrumentor from opentelemetry.sdk.metrics.export import HistogramDataPoint from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_HOST, HTTP_METHOD, HTTP_ROUTE, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_TARGET, HTTP_URL, HTTP_USER_AGENT, ) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, ) from opentelemetry.semconv.attributes.url_attributes import ( URL_FULL, URL_PATH, URL_QUERY, URL_SCHEME, ) from opentelemetry.semconv.attributes.user_agent_attributes import ( USER_AGENT_ORIGINAL, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind from .test_instrumentation import ( # pylint: disable=no-name-in-module,import-error TornadoTest, ) from .tornado_test_app import make_app NEW_SERVER_METRICS = ( "http.server.request.duration", "http.server.request.body.size", "http.server.response.body.size", ) SCOPE = "opentelemetry.instrumentation.tornado" class TestTornadoMetricsInstrumentation(TornadoTest): # Return Sequence with one histogram def create_histogram_data_points(self, sum_data_point, attributes): return [self.create_histogram_data_point(sum_data_point, 1, sum_data_point, sum_data_point, attributes)] def test_basic_metrics(self): start_time = default_timer() response = self.fetch("/") client_duration_estimated = (default_timer() - start_time) * 1000 metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 7) ( client_duration, client_request_size, client_response_size, ) = metrics[:3] ( server_active_request, server_duration, server_request_size, server_response_size, ) = metrics[3:] self.assertEqual(server_active_request.name, "http.server.active_requests") self.assert_metric_expected( server_active_request, [ self.create_number_data_point( 0, attributes={ "http.method": "GET", "http.flavor": "HTTP/1.1", "http.scheme": "http", "http.target": "/", "http.host": response.request.headers["host"], }, ), ], ) self.assertEqual(server_duration.name, "http.server.duration") self.assert_metric_expected( server_duration, self.create_histogram_data_points( client_duration_estimated, attributes={ "http.method": "GET", "http.flavor": "HTTP/1.1", "http.scheme": "http", "http.target": "/", "http.host": response.request.headers["host"], "http.status_code": response.code, }, ), est_value_delta=200, ) self.assertEqual(server_request_size.name, "http.server.request.size") self.assert_metric_expected( server_request_size, self.create_histogram_data_points( 0, attributes={ "http.status_code": 200, "http.method": "GET", "http.flavor": "HTTP/1.1", "http.scheme": "http", "http.target": "/", "http.host": response.request.headers["host"], }, ), ) self.assertEqual(server_response_size.name, "http.server.response.size") self.assert_metric_expected( server_response_size, self.create_histogram_data_points( len(response.body), attributes={ "http.status_code": response.code, "http.method": "GET", "http.flavor": "HTTP/1.1", "http.scheme": "http", "http.target": "/", "http.host": response.request.headers["host"], }, ), ) self.assertEqual(client_duration.name, "http.client.duration") self.assert_metric_expected( client_duration, self.create_histogram_data_points( client_duration_estimated, attributes={ "http.status_code": response.code, "http.method": "GET", "http.url": response.effective_url, }, ), est_value_delta=200, ) self.assertEqual(client_request_size.name, "http.client.request.size") self.assert_metric_expected( client_request_size, self.create_histogram_data_points( 0, attributes={ "http.status_code": response.code, "http.method": "GET", "http.url": response.effective_url, }, ), ) self.assertEqual(client_response_size.name, "http.client.response.size") self.assert_metric_expected( client_response_size, self.create_histogram_data_points( len(response.body), attributes={ "http.status_code": response.code, "http.method": "GET", "http.url": response.effective_url, }, ), ) @tornado.testing.gen_test async def test_metrics_concurrent_requests(self): """ Test that metrics can handle concurrent requests and calculate in an async-safe way. """ req1 = self.http_client.fetch(self.get_url("/slow?duration=1.0")) req2 = self.http_client.fetch(self.get_url("/async")) await asyncio.gather(req1, req2) metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 7) client_duration = metrics[0] server_duration = metrics[4] self.assertEqual(client_duration.name, "http.client.duration") self.assertEqual(server_duration.name, "http.server.duration") # Calculating duration requires tracking state via `_HANDLER_STATE_KEY`, so we want to make sure # duration is calculated properly per request, and doesn't affect concurrent requests. req1_client_duration_data_point = next( dp for dp in client_duration.data.data_points if "/slow" in dp.attributes.get("http.url") ) req1_server_duration_data_point = next( dp for dp in server_duration.data.data_points if "/slow" in dp.attributes.get("http.target") ) req2_client_duration_data_point = next( dp for dp in client_duration.data.data_points if "/async" in dp.attributes.get("http.url") ) req2_server_duration_data_point = next( dp for dp in server_duration.data.data_points if "/async" in dp.attributes.get("http.target") ) # Server and client durations should be similar (adjusting for msecs vs secs) # Give bigger delta for PyPy coarse timers def metrics_delta(delta): return 0.05 if platform.python_implementation() == "PyPy" else delta self.assertAlmostEqual( abs(req1_server_duration_data_point.sum / 1000.0 - req1_client_duration_data_point.sum), 0.0, delta=metrics_delta(0.01), ) self.assertAlmostEqual( abs(req2_server_duration_data_point.sum / 1000.0 - req2_client_duration_data_point.sum), 0.0, delta=metrics_delta(0.02), ) # Make sure duration is roughly equivalent to expected (req1/slow) should be around 1 second self.assertAlmostEqual( req1_server_duration_data_point.sum / 1000.0, 1.0, delta=metrics_delta(0.1), msg="Should have been about 1 second", ) self.assertAlmostEqual( req2_server_duration_data_point.sum / 1000.0, 0.0, delta=metrics_delta(0.1), msg="Should have been really short", ) def test_metric_uninstrument(self): self.fetch("/") TornadoInstrumentor().uninstrument() self.fetch("/") metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 7) for metric in metrics: for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) def test_exclude_lists(self): def test_excluded(path): self.fetch(path) # Verify no server metrics written (only client ones should exist) metrics = self.get_sorted_metrics(SCOPE) for metric in metrics: self.assertTrue("http.server" not in metric.name, metric) self.assertEqual(len(metrics), 3, metrics) test_excluded("/healthz") test_excluded("/ping") class TornadoSemconvTestBase(AsyncHTTPTestCase, TestBase): def get_app(self): tracer = trace.get_tracer(__name__) app = make_app(tracer) return app def tearDown(self): TornadoInstrumentor().uninstrument() super().tearDown() @staticmethod def _get_server_span(spans): for span in spans: if span.kind == SpanKind.SERVER: return span return None @staticmethod def _get_client_span(spans): for span in spans: if span.kind == SpanKind.CLIENT: return span return None class TestTornadoSemconvDefault(TornadoSemconvTestBase): def setUp(self): super().setUp() _OpenTelemetrySemanticConventionStability._initialized = False TornadoInstrumentor().instrument() def test_server_span_attributes_old_semconv(self): response = self.fetch("/") self.assertEqual(response.code, 201) spans = self.memory_exporter.get_finished_spans() server_span = self._get_server_span(spans) self.assertIsNotNone(server_span) # Verify old semconv attributes are present self.assertIn(HTTP_METHOD, server_span.attributes) self.assertIn(HTTP_SCHEME, server_span.attributes) self.assertIn(HTTP_HOST, server_span.attributes) self.assertIn(HTTP_TARGET, server_span.attributes) self.assertIn(HTTP_STATUS_CODE, server_span.attributes) self.assertIn(HTTP_URL, server_span.attributes) self.assertIn(HTTP_USER_AGENT, server_span.attributes) # Verify new semconv attributes are NOT present self.assertNotIn(HTTP_REQUEST_METHOD, server_span.attributes) self.assertNotIn(URL_SCHEME, server_span.attributes) self.assertNotIn(URL_PATH, server_span.attributes) self.assertNotIn(HTTP_RESPONSE_STATUS_CODE, server_span.attributes) self.assertNotIn(URL_FULL, server_span.attributes) self.assertNotIn(USER_AGENT_ORIGINAL, server_span.attributes) # Verify schema URL self.assertEqual( server_span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.11.0", ) def test_client_span_attributes_old_semconv(self): response = self.fetch("/") self.assertEqual(response.code, 201) spans = self.memory_exporter.get_finished_spans() client_span = self._get_client_span(spans) self.assertIsNotNone(client_span) # Verify old semconv attributes are present self.assertIn(HTTP_METHOD, client_span.attributes) self.assertIn(HTTP_URL, client_span.attributes) self.assertIn(HTTP_STATUS_CODE, client_span.attributes) # Verify new semconv attributes are NOT present self.assertNotIn(HTTP_REQUEST_METHOD, client_span.attributes) self.assertNotIn(URL_FULL, client_span.attributes) self.assertNotIn(HTTP_RESPONSE_STATUS_CODE, client_span.attributes) def test_server_metrics_old_semconv(self): """Test that server metrics use old semantic conventions by default.""" response = self.fetch("/parametrized/hello/?foo=bar") self.assertEqual(response.code, 200) metrics = self.get_sorted_metrics(SCOPE) # Find old semconv metrics old_duration_found = False new_duration_found = False for metric in metrics: if not metric.name.startswith("http.server"): continue if metric.name == "http.server.duration": old_duration_found = True # Verify unit is milliseconds for old semconv self.assertEqual(metric.unit, "ms") elif metric.name == "http.server.request.duration": new_duration_found = True for data_point in metric.data.data_points: attributes = dict(data_point.attributes) self.assertIn(HTTP_TARGET, attributes) self.assertEqual(attributes[HTTP_TARGET], "/parametrized/{message}/") self.assertTrue(old_duration_found, "Old semconv metric not found") self.assertFalse(new_duration_found, "New semconv metric should not be present") class TestTornadoSemconvHttpNew(TornadoSemconvTestBase): def setUp(self): super().setUp() _OpenTelemetrySemanticConventionStability._initialized = False with patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "http"}): TornadoInstrumentor().instrument() def test_server_span_attributes_new_semconv(self): response = self.fetch("/") self.assertEqual(response.code, 201) spans = self.memory_exporter.get_finished_spans() server_span = self._get_server_span(spans) self.assertIsNotNone(server_span) # Verify new semconv attributes are present self.assertIn(HTTP_REQUEST_METHOD, server_span.attributes) self.assertIn(URL_SCHEME, server_span.attributes) self.assertIn(HTTP_RESPONSE_STATUS_CODE, server_span.attributes) self.assertIn(URL_FULL, server_span.attributes) self.assertIn(USER_AGENT_ORIGINAL, server_span.attributes) self.assertIn(URL_PATH, server_span.attributes) # URL_QUERY should not be present for requests without query strings self.assertNotIn(URL_QUERY, server_span.attributes) # Verify old semconv attributes are NOT present self.assertNotIn(HTTP_METHOD, server_span.attributes) self.assertNotIn(HTTP_SCHEME, server_span.attributes) self.assertNotIn(HTTP_TARGET, server_span.attributes) self.assertNotIn(HTTP_STATUS_CODE, server_span.attributes) self.assertNotIn(HTTP_URL, server_span.attributes) self.assertNotIn(HTTP_USER_AGENT, server_span.attributes) # Verify schema URL self.assertEqual( server_span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.21.0", ) def test_client_span_attributes_new_semconv(self): """Test that client spans use new semantic conventions in http mode.""" response = self.fetch("/") self.assertEqual(response.code, 201) spans = self.memory_exporter.get_finished_spans() client_span = self._get_client_span(spans) self.assertIsNotNone(client_span) # Verify new semconv attributes are present self.assertIn(HTTP_REQUEST_METHOD, client_span.attributes) self.assertIn(URL_FULL, client_span.attributes) self.assertIn(HTTP_RESPONSE_STATUS_CODE, client_span.attributes) # Verify old semconv attributes are NOT present self.assertNotIn(HTTP_METHOD, client_span.attributes) self.assertNotIn(HTTP_URL, client_span.attributes) self.assertNotIn(HTTP_STATUS_CODE, client_span.attributes) def test_server_metrics_new_semconv(self): """Test that server metrics use new semantic conventions in http mode.""" response = self.fetch("/parametrized/hello/?foo=bar") self.assertEqual(response.code, 200) metrics = self.get_sorted_metrics(SCOPE) # Find new semconv metrics old_duration_found = False new_duration_found = False for metric in metrics: if not metric.name.startswith("http.server"): continue if metric.name == "http.server.duration": old_duration_found = True elif metric.name == "http.server.request.duration": new_duration_found = True # Verify unit is seconds for new semconv self.assertEqual(metric.unit, "s") for data_point in metric.data.data_points: attributes = dict(data_point.attributes) self.assertNotIn(URL_QUERY, attributes) self.assertNotIn(URL_PATH, attributes) if metric.name in NEW_SERVER_METRICS: self.assertIn(HTTP_ROUTE, attributes) self.assertEqual(attributes[HTTP_ROUTE], "/parametrized/{message}/") self.assertFalse(old_duration_found, "Old semconv metric should not be present") self.assertTrue(new_duration_found, "New semconv metric not found") def test_url_query_attribute_new_semconv(self): """Test that URL_QUERY is set when request has query string.""" response = self.fetch("/?foo=bar&baz=qux") self.assertEqual(response.code, 201) spans = self.memory_exporter.get_finished_spans() server_span = self._get_server_span(spans) self.assertIsNotNone(server_span) # Verify URL_QUERY is present when there's a query string self.assertIn(URL_QUERY, server_span.attributes) self.assertEqual(server_span.attributes[URL_QUERY], "foo=bar&baz=qux") # Verify URL_PATH is also present self.assertIn(URL_PATH, server_span.attributes) self.assertEqual(server_span.attributes[URL_PATH], "/") class TestTornadoSemconvHttpDup(TornadoSemconvTestBase): def setUp(self): super().setUp() _OpenTelemetrySemanticConventionStability._initialized = False with patch.dict("os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: "http/dup"}): TornadoInstrumentor().instrument() def test_server_span_attributes_both_semconv(self): response = self.fetch("/") self.assertEqual(response.code, 201) spans = self.memory_exporter.get_finished_spans() server_span = self._get_server_span(spans) self.assertIsNotNone(server_span) # Verify old semconv attributes are present self.assertIn(HTTP_METHOD, server_span.attributes) self.assertIn(HTTP_SCHEME, server_span.attributes) self.assertIn(HTTP_HOST, server_span.attributes) self.assertIn(HTTP_TARGET, server_span.attributes) self.assertIn(HTTP_STATUS_CODE, server_span.attributes) self.assertIn(HTTP_URL, server_span.attributes) self.assertIn(HTTP_USER_AGENT, server_span.attributes) # Verify new semconv attributes are also present self.assertIn(HTTP_REQUEST_METHOD, server_span.attributes) self.assertIn(URL_SCHEME, server_span.attributes) self.assertIn(HTTP_RESPONSE_STATUS_CODE, server_span.attributes) self.assertIn(URL_FULL, server_span.attributes) self.assertIn(USER_AGENT_ORIGINAL, server_span.attributes) self.assertIn(URL_PATH, server_span.attributes) # URL_QUERY should not be present for requests without query strings self.assertNotIn(URL_QUERY, server_span.attributes) # Verify values match between old and new self.assertEqual( server_span.attributes[HTTP_METHOD], server_span.attributes[HTTP_REQUEST_METHOD], ) self.assertEqual( server_span.attributes[HTTP_STATUS_CODE], server_span.attributes[HTTP_RESPONSE_STATUS_CODE], ) self.assertEqual( server_span.attributes[HTTP_SCHEME], server_span.attributes[URL_SCHEME], ) self.assertEqual( server_span.attributes[HTTP_URL], server_span.attributes[URL_FULL], ) self.assertEqual( server_span.attributes[HTTP_USER_AGENT], server_span.attributes[USER_AGENT_ORIGINAL], ) # Verify schema URL (in dup mode, schema_url should be the new one) self.assertEqual( server_span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.21.0", ) def test_client_span_attributes_both_semconv(self): response = self.fetch("/") self.assertEqual(response.code, 201) spans = self.memory_exporter.get_finished_spans() client_span = self._get_client_span(spans) self.assertIsNotNone(client_span) # Verify old semconv attributes are present self.assertIn(HTTP_METHOD, client_span.attributes) self.assertIn(HTTP_URL, client_span.attributes) self.assertIn(HTTP_STATUS_CODE, client_span.attributes) # Verify new semconv attributes are also present self.assertIn(HTTP_REQUEST_METHOD, client_span.attributes) self.assertIn(URL_FULL, client_span.attributes) self.assertIn(HTTP_RESPONSE_STATUS_CODE, client_span.attributes) # Verify values match between old and new self.assertEqual( client_span.attributes[HTTP_METHOD], client_span.attributes[HTTP_REQUEST_METHOD], ) self.assertEqual( client_span.attributes[HTTP_STATUS_CODE], client_span.attributes[HTTP_RESPONSE_STATUS_CODE], ) def test_server_metrics_both_semconv(self): response = self.fetch("/") self.assertEqual(response.code, 201) metrics = self.get_sorted_metrics(SCOPE) # Find both old and new semconv metrics old_duration_found = False new_duration_found = False for metric in metrics: if not metric.name.startswith("http.server"): continue if metric.name == "http.server.duration": old_duration_found = True self.assertEqual(metric.unit, "ms") elif metric.name == "http.server.request.duration": new_duration_found = True self.assertEqual(metric.unit, "s") for data_point in metric.data.data_points: attributes = dict(data_point.attributes) self.assertNotIn(URL_QUERY, attributes) self.assertNotIn(URL_PATH, attributes) if metric.name in NEW_SERVER_METRICS: self.assertIn(HTTP_ROUTE, attributes) self.assertEqual(attributes[HTTP_ROUTE], "/") self.assertTrue(old_duration_found, "Old semconv metric not found") self.assertTrue(new_duration_found, "New semconv metric not found") def test_server_metrics_method_is_sanitized(self): response = self.fetch("/", method="UNKNOWN", allow_nonstandard_methods=True) self.assertEqual(response.code, 405) metrics = self.get_sorted_metrics(SCOPE) old_server_found = False new_server_found = False old_client_found = False new_client_found = False for metric in metrics: if metric.name not in ( "http.server.active_requests", "http.client.duration", "http.client.request.duration", ): continue for data_point in metric.data.data_points: attributes = dict(data_point.attributes) if old_attribute := attributes.get("http.method"): self.assertEqual(old_attribute, "_OTHER") old_server_found = True if metric.name == "http.server.active_requests" else old_server_found old_client_found = True if metric.name == "http.client.duration" else old_client_found if new_attribute := attributes.get("http.request.method"): self.assertEqual(new_attribute, "_OTHER") new_server_found = True if metric.name == "http.server.active_requests" else new_server_found new_client_found = True if metric.name == "http.client.request.duration" else new_client_found self.assertTrue(old_server_found, "Old semconv server metric not found") self.assertTrue(new_server_found, "New semconv server metric not found") self.assertTrue(old_client_found, "Old semconv client metric not found") self.assertTrue(new_client_found, "New semconv client metric not found") def test_url_query_attribute_both_semconv(self): """Test that URL_QUERY is set in dup mode when request has query string.""" response = self.fetch("/?test=value&another=param") self.assertEqual(response.code, 201) spans = self.memory_exporter.get_finished_spans() server_span = self._get_server_span(spans) self.assertIsNotNone(server_span) # Verify URL_QUERY is present in new semconv self.assertIn(URL_QUERY, server_span.attributes) self.assertEqual(server_span.attributes[URL_QUERY], "test=value&another=param") # Verify URL_PATH is also present self.assertIn(URL_PATH, server_span.attributes) self.assertEqual(server_span.attributes[URL_PATH], "/") # Verify HTTP_TARGET still contains the full target with query self.assertIn(HTTP_TARGET, server_span.attributes) self.assertEqual(server_span.attributes[HTTP_TARGET], "/?test=value&another=param") def test_client_metrics_both_semconv(self): response = self.fetch("/") self.assertEqual(response.code, 201) metrics = self.get_sorted_metrics(SCOPE) # Find both old and new semconv metrics old_duration_found = False new_duration_found = False for metric in metrics: if metric.name == "http.client.duration": old_duration_found = True self.assertEqual(metric.unit, "ms") elif metric.name == "http.client.request.duration": new_duration_found = True self.assertEqual(metric.unit, "s") self.assertTrue(old_duration_found, "Old semconv client metric not found") self.assertTrue(new_duration_found, "New semconv client metric not found") tornado_test_app.py000066400000000000000000000110341527070072300376120ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tornado/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=W0223,R0201 import asyncio import time import tornado.web import tornado.websocket from tornado import gen class AsyncHandler(tornado.web.RequestHandler): async def get(self): with self.application.tracer.start_as_current_span("sub-task-wrapper"): await self.do_something1() await self.do_something2() self.set_status(201) self.write("{}") async def do_something1(self): with self.application.tracer.start_as_current_span("sub-task-1"): tornado.gen.sleep(0.1) async def do_something2(self): with self.application.tracer.start_as_current_span("sub-task-2"): tornado.gen.sleep(0.1) class CoroutineHandler(tornado.web.RequestHandler): @gen.coroutine def get(self): with self.application.tracer.start_as_current_span("sub-task-wrapper"): yield self.do_something1() yield self.do_something2() self.set_status(201) self.write("{}") @gen.coroutine def do_something1(self): with self.application.tracer.start_as_current_span("sub-task-1"): tornado.gen.sleep(0.1) @gen.coroutine def do_something2(self): with self.application.tracer.start_as_current_span("sub-task-2"): tornado.gen.sleep(0.1) class MainHandler(tornado.web.RequestHandler): def _handler(self): with self.application.tracer.start_as_current_span("manual"): self.write("Hello, world") self.set_status(201) def get(self): return self._handler() def post(self): return self._handler() def patch(self): return self._handler() def delete(self): return self._handler() def put(self): return self._handler() def head(self): return self._handler() def options(self): return self._handler() class BadHandler(tornado.web.RequestHandler): def get(self): raise NameError("some random name error") class DynamicHandler(tornado.web.RequestHandler): def get(self): self.set_status(202) class FinishedHandler(tornado.web.RequestHandler): def on_finish(self): with self.application.tracer.start_as_current_span("audit_task"): time.sleep(0.05) def get(self): self.write("Test can finish") self.set_status(200) class HealthCheckHandler(tornado.web.RequestHandler): def get(self): self.set_status(200) class CustomResponseHeaderHandler(tornado.web.RequestHandler): def get(self): self.set_header("content-type", "text/plain; charset=utf-8") self.set_header("content-length", "0") self.set_header("my-custom-header", "my-custom-value-1,my-custom-header-2") self.set_status(200) class SlowHandler(tornado.web.RequestHandler): async def get(self): with self.application.tracer.start_as_current_span("slow"): duration = float(self.get_argument("duration", "1.0")) await asyncio.sleep(duration) class RaiseHTTPErrorHandler(tornado.web.RequestHandler): def get(self): raise tornado.web.HTTPError(403) class ParametrizedHandler(tornado.web.RequestHandler): async def get(self, message: str): self.write(message) self.set_status(200) class NoGroupHandler(tornado.web.RequestHandler): async def get(self): self.set_status(200) class EchoWebSocketHandler(tornado.websocket.WebSocketHandler): async def on_message(self, message): with self.application.tracer.start_as_current_span("audit_message"): self.write_message(f"hello {message}") def on_close(self): with self.application.tracer.start_as_current_span("audit_on_close"): time.sleep(0.05) def make_app(tracer): app = tornado.web.Application( [ (r"/", MainHandler), (r"/error", BadHandler), (r"/cor", CoroutineHandler), (r"/async", AsyncHandler), (r"/on_finish", FinishedHandler), (r"/healthz", HealthCheckHandler), (r"/ping", HealthCheckHandler), (r"/test_custom_response_headers", CustomResponseHeaderHandler), (r"/raise_403", RaiseHTTPErrorHandler), (r"/slow", SlowHandler), (r"/echo_socket", EchoWebSocketHandler), (r"/parametrized/(.*)/", ParametrizedHandler), (r"/nogroup/[0-9]/", NoGroupHandler), ] ) app.tracer = tracer return app opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tortoiseorm/000077500000000000000000000000001527070072300335315ustar00rootroot00000000000000LICENSE000066400000000000000000000261351527070072300344660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tortoiseorm Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. README.rst000066400000000000000000000012661527070072300351460ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tortoiseormOpenTelemetry Tortoise ORM Instrumentation ========================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-tortoiseorm.svg :target: https://pypi.org/project/opentelemetry-instrumentation-tortoiseorm/ This library allows tracing queries made by tortoise ORM backends, mysql, postgres and sqlite. Installation ------------ :: pip install opentelemetry-instrumentation-tortoiseorm References ---------- * `OpenTelemetry Project `_ * `Tortoise ORM `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000032311527070072300363650ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tortoiseorm[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-tortoiseorm" dynamic = ["version"] description = "OpenTelemetry Instrumentation for Tortoise ORM" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", ] [project.optional-dependencies] instruments = [ "tortoise-orm >= 0.17.0", "pydantic >= 1.10.2" ] [project.entry-points.opentelemetry_instrumentor] tortoiseorm = "opentelemetry.instrumentation.tortoiseorm:TortoiseORMInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-tortoiseorm" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/tortoiseorm/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/000077500000000000000000000000001527070072300343205ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300371355ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tortoiseorm/srcinstrumentation/000077500000000000000000000000001527070072300424005ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetrytortoiseorm/000077500000000000000000000000001527070072300447665ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation__init__.py000066400000000000000000000262431527070072300471060ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Instrument tortoise-orm to report SQL queries. Usage ----- .. code:: python from fastapi import FastAPI from tortoise.contrib.fastapi import register_tortoise from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.instrumentation.tortoiseorm import TortoiseORMInstrumentor app = FastAPI() tracer = TracerProvider(resource=Resource({SERVICE_NAME: "FastAPI"})) TortoiseORMInstrumentor().instrument(tracer_provider=tracer) register_tortoise( app, db_url="sqlite://sample.db", modules={"models": ["example_app.db_models"]} ) API --- """ from collections.abc import Collection import wrapt from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( _get_schema_url_for_signal_types, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _set_db_name, _set_db_statement, _set_db_system, _set_db_user, _set_http_net_peer_name_client, _set_http_peer_port_client, ) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.tortoiseorm.package import _instruments from opentelemetry.instrumentation.tortoiseorm.version import __version__ from opentelemetry.instrumentation.utils import ( is_instrumentation_enabled, unwrap, ) from opentelemetry.semconv._incubating.attributes.db_attributes import ( DbSystemValues, ) from opentelemetry.trace import SpanKind from opentelemetry.trace.status import Status, StatusCode try: import tortoise.backends.asyncpg.client TORTOISE_POSTGRES_SUPPORT = True except ModuleNotFoundError: TORTOISE_POSTGRES_SUPPORT = False try: import tortoise.backends.mysql.client TORTOISE_MYSQL_SUPPORT = True except ModuleNotFoundError: TORTOISE_MYSQL_SUPPORT = False try: import tortoise.backends.sqlite.client TORTOISE_SQLITE_SUPPORT = True except ModuleNotFoundError: TORTOISE_SQLITE_SUPPORT = False import tortoise.contrib.pydantic.base class TortoiseORMInstrumentor(BaseInstrumentor): """An instrumentor for Tortoise-ORM See `BaseInstrumentor` """ def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): """Instruments Tortoise ORM backend methods. Args: **kwargs: Optional arguments ``tracer_provider``: a TracerProvider, defaults to global ``capture_parameters``: set to True to capture SQL query parameters Returns: None """ _OpenTelemetrySemanticConventionStability._initialize() self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.DATABASE ) tracer_provider = kwargs.get("tracer_provider") self._tracer = trace.get_tracer( __name__, __version__, tracer_provider, schema_url=_get_schema_url_for_signal_types([_OpenTelemetryStabilitySignalType.DATABASE]), ) self.capture_parameters = kwargs.get("capture_parameters", False) if TORTOISE_SQLITE_SUPPORT: funcs = [ "SqliteClient.execute_many", "SqliteClient.execute_query", "SqliteClient.execute_insert", "SqliteClient.execute_query_dict", "SqliteClient.execute_script", ] for func in funcs: wrapt.wrap_function_wrapper( "tortoise.backends.sqlite.client", func, self._do_execute, ) if TORTOISE_POSTGRES_SUPPORT: funcs = [ "AsyncpgDBClient.execute_many", "AsyncpgDBClient.execute_query", "AsyncpgDBClient.execute_insert", "AsyncpgDBClient.execute_query_dict", "AsyncpgDBClient.execute_script", ] for func in funcs: wrapt.wrap_function_wrapper( "tortoise.backends.asyncpg.client", func, self._do_execute, ) if TORTOISE_MYSQL_SUPPORT: funcs = [ "MySQLClient.execute_many", "MySQLClient.execute_query", "MySQLClient.execute_insert", "MySQLClient.execute_query_dict", "MySQLClient.execute_script", ] for func in funcs: wrapt.wrap_function_wrapper( "tortoise.backends.mysql.client", func, self._do_execute, ) wrapt.wrap_function_wrapper( "tortoise.contrib.pydantic.base", "PydanticModel.from_queryset", self._from_queryset, ) wrapt.wrap_function_wrapper( "tortoise.contrib.pydantic.base", "PydanticModel.from_queryset_single", self._from_queryset, ) wrapt.wrap_function_wrapper( "tortoise.contrib.pydantic.base", "PydanticListModel.from_queryset", self._from_queryset, ) def _uninstrument(self, **kwargs): if TORTOISE_SQLITE_SUPPORT: unwrap(tortoise.backends.sqlite.client.SqliteClient, "execute_query") unwrap(tortoise.backends.sqlite.client.SqliteClient, "execute_many") unwrap(tortoise.backends.sqlite.client.SqliteClient, "execute_insert") unwrap( tortoise.backends.sqlite.client.SqliteClient, "execute_query_dict", ) unwrap(tortoise.backends.sqlite.client.SqliteClient, "execute_script") if TORTOISE_MYSQL_SUPPORT: unwrap(tortoise.backends.mysql.client.MySQLClient, "execute_query") unwrap(tortoise.backends.mysql.client.MySQLClient, "execute_many") unwrap(tortoise.backends.mysql.client.MySQLClient, "execute_insert") unwrap( tortoise.backends.mysql.client.MySQLClient, "execute_query_dict", ) unwrap(tortoise.backends.mysql.client.MySQLClient, "execute_script") if TORTOISE_POSTGRES_SUPPORT: unwrap( tortoise.backends.asyncpg.client.AsyncpgDBClient, "execute_query", ) unwrap( tortoise.backends.asyncpg.client.AsyncpgDBClient, "execute_many", ) unwrap( tortoise.backends.asyncpg.client.AsyncpgDBClient, "execute_insert", ) unwrap( tortoise.backends.asyncpg.client.AsyncpgDBClient, "execute_query_dict", ) unwrap( tortoise.backends.asyncpg.client.AsyncpgDBClient, "execute_script", ) unwrap(tortoise.contrib.pydantic.base.PydanticModel, "from_queryset") unwrap( tortoise.contrib.pydantic.base.PydanticModel, "from_queryset_single", ) unwrap(tortoise.contrib.pydantic.base.PydanticListModel, "from_queryset") def _hydrate_span_from_args(self, connection, query, parameters) -> dict: """Get network and database attributes from connection.""" span_attributes = {} mode = self._sem_conv_opt_in_mode capabilities = getattr(connection, "capabilities", None) if capabilities is not None: if capabilities.dialect == "sqlite": _set_db_system(span_attributes, DbSystemValues.SQLITE.value, mode) elif capabilities.dialect == "postgres": _set_db_system(span_attributes, DbSystemValues.POSTGRESQL.value, mode) elif capabilities.dialect == "mysql": _set_db_system(span_attributes, DbSystemValues.MYSQL.value, mode) dbname = getattr(connection, "filename", None) if dbname: _set_db_name(span_attributes, dbname, mode) dbname = getattr(connection, "database", None) if dbname: _set_db_name(span_attributes, dbname, mode) if query is not None: _set_db_statement(span_attributes, query, mode) user = getattr(connection, "user", None) if user: _set_db_user(span_attributes, user, mode) host = getattr(connection, "host", None) if host: _set_http_net_peer_name_client(span_attributes, host, mode) port = getattr(connection, "port", None) if port: _set_http_peer_port_client(span_attributes, port, mode) if self.capture_parameters: if parameters is not None and len(parameters) > 0: span_attributes["db.statement.parameters"] = str(parameters) return span_attributes async def _do_execute(self, func, instance, args, kwargs): if not is_instrumentation_enabled(): return await func(*args, **kwargs) exception = None name = args[0].split()[0] with self._tracer.start_as_current_span(name, kind=SpanKind.CLIENT) as span: if span.is_recording(): span_attributes = self._hydrate_span_from_args( instance, args[0], args[1:], ) for attribute, value in span_attributes.items(): span.set_attribute(attribute, value) try: result = await func(*args, **kwargs) except Exception as exc: # pylint: disable=W0703 exception = exc raise finally: if span.is_recording() and exception is not None: span.set_status(Status(StatusCode.ERROR)) return result async def _from_queryset(self, func, modelcls, args, kwargs): if not is_instrumentation_enabled(): return await func(*args, **kwargs) exception = None name = f"pydantic.{func.__name__}" with self._tracer.start_as_current_span( name, kind=SpanKind.INTERNAL, ) as span: if span.is_recording(): span_attributes = {} model_config = getattr(modelcls, "Config", None) if model_config: model_title = getattr( modelcls.Config, "title", modelcls.__name__, ) if model_title: span_attributes["pydantic.model"] = model_title for attribute, value in span_attributes.items(): span.set_attribute(attribute, value) try: result = await func(*args, **kwargs) except Exception as exc: # pylint: disable=W0703 exception = exc raise finally: if span.is_recording() and exception is not None: span.set_status(Status(StatusCode.ERROR)) return result package.py000066400000000000000000000002301527070072300467260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("tortoise-orm >= 0.17.0",) _semconv_status = "migration" version.py000066400000000000000000000001441527070072300470240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000006511527070072300377150ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tortoiseormaiosqlite==0.17.0 annotated-types==0.7.0 asgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 iso8601==1.1.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pydantic==2.12.5 pydantic_core==2.41.5 pypika-tortoise==0.1.6 pytest==7.4.4 pytz==2024.1 tomli==2.0.1 tortoise-orm==0.20.0 typing_extensions==4.14.1 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e instrumentation/opentelemetry-instrumentation-tortoiseorm opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tortoiseorm/tests/000077500000000000000000000000001527070072300346735ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300367130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tortoiseorm/teststest_tortoiseorm_instrumentation.py000066400000000000000000000204361527070072300441430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-tortoiseorm/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio import contextlib from unittest import mock from tortoise import Tortoise, fields, models from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.tortoiseorm import TortoiseORMInstrumentor from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_STATEMENT, DB_SYSTEM, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_NAMESPACE, DB_QUERY_TEXT, DB_SYSTEM_NAME, ) from opentelemetry.test.test_base import TestBase class MockModel(models.Model): id = fields.IntField(pk=True) name = fields.TextField() def __str__(self): return self.name @contextlib.contextmanager def use_semconv_opt_in(sem_conv_mode): """Context manager to set OTEL_SEMCONV_STABILITY_OPT_IN and reset state.""" env_patch = mock.patch.dict( "os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode}, ) _OpenTelemetrySemanticConventionStability._initialized = False env_patch.start() try: yield finally: env_patch.stop() _OpenTelemetrySemanticConventionStability._initialized = False class TestTortoiseORMInstrumentor(TestBase): def setUp(self): super().setUp() _OpenTelemetrySemanticConventionStability._initialized = False TortoiseORMInstrumentor().instrument() def tearDown(self): super().tearDown() TortoiseORMInstrumentor().uninstrument() _OpenTelemetrySemanticConventionStability._initialized = False if Tortoise._inited: self._async_call(Tortoise._drop_databases()) # pylint: disable-next=no-self-use def _async_call(self, coro): return asyncio.run(coro) # pylint: disable-next=no-self-use async def _init_tortoise(self): await Tortoise.init( db_url="sqlite://:memory:", modules={"models": [__name__]}, ) await Tortoise.generate_schemas() def test_trace_integration(self): """Default mode (no opt-in): old semconv attributes only.""" async def run(): await self._init_tortoise() await MockModel.create(name="Test 1") await MockModel.filter(name="Test 1").first() self._async_call(run()) spans = self.memory_exporter.get_finished_spans() crud_spans = [s for s in spans if s.name in ("INSERT", "SELECT")] self.assertGreaterEqual(len(crud_spans), 2) insert_span = next(s for s in crud_spans if s.name == "INSERT") self.assertEqual(insert_span.kind, trace.SpanKind.CLIENT) # Old attrs present self.assertEqual(insert_span.attributes[DB_SYSTEM], "sqlite") self.assertEqual(insert_span.attributes[DB_NAME], ":memory:") self.assertIn("INSERT", insert_span.attributes[DB_STATEMENT]) # New attrs must NOT be present in default mode self.assertNotIn(DB_SYSTEM_NAME, insert_span.attributes) self.assertNotIn(DB_NAMESPACE, insert_span.attributes) self.assertNotIn(DB_QUERY_TEXT, insert_span.attributes) select_span = next(s for s in crud_spans if s.name == "SELECT") self.assertEqual(select_span.kind, trace.SpanKind.CLIENT) self.assertEqual(select_span.attributes[DB_SYSTEM], "sqlite") self.assertEqual(select_span.attributes[DB_NAME], ":memory:") self.assertIn("SELECT", select_span.attributes[DB_STATEMENT]) def test_trace_integration_new_semconv(self): """database opt-in mode: new stable semconv attributes only.""" TortoiseORMInstrumentor().uninstrument() with use_semconv_opt_in("database"): TortoiseORMInstrumentor().instrument() async def run(): await self._init_tortoise() await MockModel.create(name="Test New Semconv") await MockModel.filter(name="Test New Semconv").first() self._async_call(run()) spans = self.memory_exporter.get_finished_spans() crud_spans = [s for s in spans if s.name in ("INSERT", "SELECT")] self.assertGreaterEqual(len(crud_spans), 2) insert_span = next(s for s in crud_spans if s.name == "INSERT") # New attrs present self.assertEqual(insert_span.attributes[DB_SYSTEM_NAME], "sqlite") self.assertEqual(insert_span.attributes[DB_NAMESPACE], ":memory:") self.assertIn("INSERT", insert_span.attributes[DB_QUERY_TEXT]) # Old attrs must NOT be present self.assertNotIn(DB_SYSTEM, insert_span.attributes) self.assertNotIn(DB_NAME, insert_span.attributes) self.assertNotIn(DB_STATEMENT, insert_span.attributes) select_span = next(s for s in crud_spans if s.name == "SELECT") self.assertEqual(select_span.attributes[DB_SYSTEM_NAME], "sqlite") self.assertIn("SELECT", select_span.attributes[DB_QUERY_TEXT]) def test_trace_integration_dup_semconv(self): """database/dup mode: both old and new semconv attributes present.""" TortoiseORMInstrumentor().uninstrument() with use_semconv_opt_in("database/dup"): TortoiseORMInstrumentor().instrument() async def run(): await self._init_tortoise() await MockModel.create(name="Test Dup Semconv") self._async_call(run()) spans = self.memory_exporter.get_finished_spans() insert_span = next((s for s in spans if s.name == "INSERT"), None) self.assertIsNotNone(insert_span) # Old attrs present self.assertIn(DB_SYSTEM, insert_span.attributes) self.assertIn(DB_NAME, insert_span.attributes) self.assertIn(DB_STATEMENT, insert_span.attributes) # New attrs also present self.assertIn(DB_SYSTEM_NAME, insert_span.attributes) self.assertIn(DB_NAMESPACE, insert_span.attributes) self.assertIn(DB_QUERY_TEXT, insert_span.attributes) # Values are consistent self.assertEqual(insert_span.attributes[DB_SYSTEM], "sqlite") self.assertEqual(insert_span.attributes[DB_SYSTEM_NAME], "sqlite") self.assertEqual(insert_span.attributes[DB_NAME], ":memory:") self.assertEqual(insert_span.attributes[DB_NAMESPACE], ":memory:") def test_capture_parameters(self): TortoiseORMInstrumentor().uninstrument() TortoiseORMInstrumentor().instrument(capture_parameters=True) async def run(): await self._init_tortoise() await MockModel.create(name="Test Parameterized") self._async_call(run()) spans = self.memory_exporter.get_finished_spans() insert_span = next(s for s in spans if s.name == "INSERT") self.assertIn("db.statement.parameters", insert_span.attributes) self.assertIn( "Test Parameterized", insert_span.attributes["db.statement.parameters"], ) def test_uninstrument(self): TortoiseORMInstrumentor().uninstrument() async def run(): await self._init_tortoise() await MockModel.create(name="Test Uninstrumented") self._async_call(run()) spans = self.memory_exporter.get_finished_spans() crud_spans = [s for s in spans if s.name in ("INSERT", "SELECT")] self.assertEqual(len(crud_spans), 0) def test_suppress_instrumentation(self): async def run(): await self._init_tortoise() with suppress_instrumentation(): await MockModel.create(name="Test Suppressed") self._async_call(run()) spans = self.memory_exporter.get_finished_spans() crud_spans = [s for s in spans if s.name in ("INSERT", "SELECT")] self.assertEqual(len(crud_spans), 0) def test_no_op_tracer_provider(self): TortoiseORMInstrumentor().uninstrument() TortoiseORMInstrumentor().instrument(tracer_provider=trace.NoOpTracerProvider()) async def run(): await self._init_tortoise() await MockModel.create(name="Test NoOp") self._async_call(run()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib/000077500000000000000000000000001527070072300324345ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib/LICENSE000066400000000000000000000261351527070072300334500ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib/README.rst000066400000000000000000000041341527070072300341250ustar00rootroot00000000000000OpenTelemetry urllib Instrumentation ==================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-urllib.svg :target: https://pypi.org/project/opentelemetry-instrumentation-urllib/ This library allows tracing HTTP requests made by the `urllib `_ library. Installation ------------ :: pip install opentelemetry-instrumentation-urllib Configuration ------------- Request/Response hooks ********************** The urllib instrumentation supports extending tracing behavior with the help of request and response hooks. These are functions that are called back by the instrumentation right after a Span is created for a request and right before the span is finished processing a response respectively. The hooks can be configured as follows: .. code:: python from opentelemetry.instrumentation.urllib import URLLibInstrumentor # `request_obj` is an instance of urllib.request.Request def request_hook(span, request_obj): pass # `request_obj` is an instance of urllib.request.Request # `response` is an instance of http.client.HTTPResponse def response_hook(span, request_obj, response): pass URLLibInstrumentor().instrument( request_hook=request_hook, response_hook=response_hook ) Exclude lists ************* To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_URLLIB_EXCLUDED_URLS`` (or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude. For example, :: export OTEL_PYTHON_URLLIB_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. References ---------- * `OpenTelemetry urllib Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000031441527070072300352730ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-urllib" dynamic = ["version"] description = "OpenTelemetry urllib instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-util-http == 0.66b2", ] [project.optional-dependencies] instruments = [] [project.entry-points.opentelemetry_instrumentor] urllib = "opentelemetry.instrumentation.urllib:URLLibInstrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-urllib" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/urllib/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib/src/000077500000000000000000000000001527070072300332235ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300360405ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib/srcinstrumentation/000077500000000000000000000000001527070072300413035ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetryurllib/000077500000000000000000000000001527070072300425745ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation__init__.py000066400000000000000000000516621527070072300447170ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This library allows tracing HTTP requests made by the `urllib `_ library. Usage ----- .. code-block:: python from urllib import request from opentelemetry.instrumentation.urllib import URLLibInstrumentor # You can optionally pass a custom TracerProvider to # URLLibInstrumentor().instrument() URLLibInstrumentor().instrument() req = request.Request("https://postman-echo.com/post", method="POST") r = request.urlopen(req) Configuration ------------- Request/Response hooks ********************** The urllib instrumentation supports extending tracing behavior with the help of request and response hooks. These are functions that are called back by the instrumentation right after a Span is created for a request and right before the span is finished processing a response respectively. The hooks can be configured as follows: .. code:: python from http.client import HTTPResponse from urllib.request import Request from opentelemetry.instrumentation.urllib import URLLibInstrumentor from opentelemetry.trace import Span def request_hook(span: Span, request: Request): pass def response_hook(span: Span, request: Request, response: HTTPResponse): pass URLLibInstrumentor().instrument( request_hook=request_hook, response_hook=response_hook ) Exclude lists ************* To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_URLLIB_EXCLUDED_URLS`` (or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude. For example, :: export OTEL_PYTHON_URLLIB_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. Capture HTTP request and response headers ***************************************** You can configure the agent to capture specified HTTP headers as span attributes, according to the `semantic conventions `_. Request headers *************** To capture HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST`` to a comma delimited list of HTTP header names. For example using the environment variable, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST="content-type,custom_request_header" will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. Request header names in urllib are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST="Accept.*,X-.*" Would match all request headers that start with ``Accept`` and ``X-``. To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST=".*" The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a single item list containing all the header values. For example: ``http.request.header.custom_request_header = ["", ""]`` .. note:: Some headers are injected at a lower level by the ``http.client`` module and so are not captured by this instrumentation Response headers **************** To capture HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE`` to a comma delimited list of HTTP header names. For example using the environment variable, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE="content-type,custom_response_header" will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. Response header names in urllib are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE="Content.*,X-.*" Would match all response headers that start with ``Content`` and ``X-``. To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE=".*" The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a list containing the header values. For example: ``http.response.header.custom_response_header = ["", ""]`` Sanitizing headers ****************** In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be matched in a case-insensitive manner. For example using the environment variable, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. Note: The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. API --- """ from __future__ import annotations import functools import types from collections.abc import Callable, Collection from http import client from timeit import default_timer from typing import Any from urllib.request import ( # pylint: disable=no-name-in-module,import-error OpenerDirector, Request, ) from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, _client_duration_attrs_new, _client_duration_attrs_old, _filter_semconv_duration_attrs, _get_schema_url, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _report_new, _report_old, _set_http_method, _set_http_network_protocol_version, _set_http_url, _set_status, _StabilityMode, ) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.urllib.package import _instruments from opentelemetry.instrumentation.urllib.version import __version__ from opentelemetry.instrumentation.utils import ( is_http_instrumentation_enabled, suppress_http_instrumentation, ) from opentelemetry.metrics import Histogram, Meter, get_meter from opentelemetry.propagate import inject from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_URL, ) from opentelemetry.semconv._incubating.metrics.http_metrics import ( HTTP_CLIENT_REQUEST_BODY_SIZE, HTTP_CLIENT_RESPONSE_BODY_SIZE, create_http_client_request_body_size, create_http_client_response_body_size, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.semconv.metrics.http_metrics import ( HTTP_CLIENT_REQUEST_DURATION, ) from opentelemetry.trace import Span, SpanKind, Tracer, get_tracer from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, ExcludeList, get_custom_header_attributes, get_custom_headers, get_excluded_urls, normalise_request_header_name, normalise_response_header_name, parse_excluded_urls, redact_url, sanitize_method, ) from opentelemetry.util.types import Attributes _excluded_urls_from_env = get_excluded_urls("URLLIB") _RequestHookT = Callable[[Span, Request], None] | None _ResponseHookT = Callable[[Span, Request, client.HTTPResponse], None] | None class URLLibInstrumentor(BaseInstrumentor): """An instrumentor for urllib See `BaseInstrumentor` """ def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs: Any): """Instruments urllib module Args: **kwargs: Optional arguments ``tracer_provider``: a TracerProvider, defaults to global ``request_hook``: An optional callback invoked that is invoked right after a span is created. ``response_hook``: An optional callback which is invoked right before the span is finished processing a response ``excluded_urls``: A string containing a comma-delimited list of regexes used to exclude URLs from tracking ``captured_request_headers``: A comma-separated list of regexes to match against request headers to capture ``captured_response_headers``: A comma-separated list of regexes to match against response headers to capture ``sensitive_headers``: A comma-separated list of regexes to match against captured headers to be sanitized """ # initialize semantic conventions opt-in if needed _OpenTelemetrySemanticConventionStability._initialize() sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) schema_url = _get_schema_url(sem_conv_opt_in_mode) tracer_provider = kwargs.get("tracer_provider") tracer = get_tracer( __name__, __version__, tracer_provider, schema_url=schema_url, ) excluded_urls = kwargs.get("excluded_urls") meter_provider = kwargs.get("meter_provider") meter = get_meter( __name__, __version__, meter_provider, schema_url=schema_url, ) histograms = _create_client_histograms(meter, sem_conv_opt_in_mode) _instrument( tracer, histograms, request_hook=kwargs.get("request_hook"), response_hook=kwargs.get("response_hook"), excluded_urls=(_excluded_urls_from_env if excluded_urls is None else parse_excluded_urls(excluded_urls)), sem_conv_opt_in_mode=sem_conv_opt_in_mode, captured_request_headers=kwargs.get( "captured_request_headers", get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST), ), captured_response_headers=kwargs.get( "captured_response_headers", get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE), ), sensitive_headers=kwargs.get( "sensitive_headers", get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS), ), ) def _uninstrument(self, **kwargs: Any): _uninstrument() def uninstrument_opener(self, opener: OpenerDirector): # pylint: disable=no-self-use """uninstrument_opener a specific instance of urllib.request.OpenerDirector""" _uninstrument_from(opener, restore_as_bound_func=True) # pylint: disable=too-many-statements def _instrument( tracer: Tracer, histograms: dict[str, Histogram], request_hook: _RequestHookT = None, response_hook: _ResponseHookT = None, excluded_urls: ExcludeList | None = None, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, captured_request_headers: list[str] | None = None, captured_response_headers: list[str] | None = None, sensitive_headers: list[str] | None = None, ): """Enables tracing of all requests calls that go through :code:`urllib.Client._make_request`""" opener_open = OpenerDirector.open @functools.wraps(opener_open) def instrumented_open(opener, fullurl, data=None, timeout=None): if isinstance(fullurl, str): # in case of multiple entries for the same header Opener.open sends the first value request_ = Request(fullurl, data, headers=dict(reversed(opener.addheaders))) else: request_ = fullurl def get_or_create_headers(): return getattr(request_, "headers", {}) def call_wrapped(): return opener_open(opener, request_, data=data, timeout=timeout) return _instrumented_open_call(opener, request_, call_wrapped, get_or_create_headers) def _instrumented_open_call(_, request, call_wrapped, get_or_create_headers): # pylint: disable=too-many-locals if not is_http_instrumentation_enabled(): return call_wrapped() url = request.full_url if excluded_urls and excluded_urls.url_disabled(url): return call_wrapped() method = request.get_method().upper() span_name = _get_span_name(method) url = redact_url(url) data = getattr(request, "data", None) request_size = 0 if data is None else len(data) labels = {} _set_http_method( labels, method, sanitize_method(method), sem_conv_opt_in_mode, ) _set_http_url(labels, url, sem_conv_opt_in_mode) headers = get_or_create_headers() labels.update( get_custom_header_attributes( headers, captured_request_headers, sensitive_headers, normalise_request_header_name, ) ) with tracer.start_as_current_span(span_name, kind=SpanKind.CLIENT, attributes=labels) as span: exception = None if callable(request_hook): request_hook(span, request) inject(headers) with suppress_http_instrumentation(): start_time = default_timer() try: result = call_wrapped() # *** PROCEED except Exception as exc: # pylint: disable=W0703 exception = exc result = getattr(exc, "file", None) finally: duration_s = default_timer() - start_time response_size = 0 if result is not None: response_size = int(result.headers.get("Content-Length", 0)) code_ = result.getcode() # set http status code based on semconv if code_: _set_status_code_attribute(span, code_, labels, sem_conv_opt_in_mode) ver_ = str(getattr(result, "version", "")) if ver_: _set_http_network_protocol_version(labels, f"{ver_[:1]}.{ver_[:-1]}", sem_conv_opt_in_mode) if span.is_recording(): span.set_attributes( get_custom_header_attributes( result.headers, captured_response_headers, sensitive_headers, normalise_response_header_name, ) ) if exception is not None and _report_new(sem_conv_opt_in_mode): span.set_attribute(ERROR_TYPE, type(exception).__qualname__) labels[ERROR_TYPE] = type(exception).__qualname__ duration_attrs_old = _filter_semconv_duration_attrs( labels, _client_duration_attrs_old, _client_duration_attrs_new, sem_conv_opt_in_mode=_StabilityMode.DEFAULT, ) duration_attrs_new = _filter_semconv_duration_attrs( labels, _client_duration_attrs_old, _client_duration_attrs_new, sem_conv_opt_in_mode=_StabilityMode.HTTP, ) duration_attrs_old[HTTP_URL] = url _record_histograms( histograms, duration_attrs_old, duration_attrs_new, request_size, response_size, duration_s, sem_conv_opt_in_mode, ) if callable(response_hook): response_hook(span, request, result) if exception is not None: raise exception.with_traceback(exception.__traceback__) return result instrumented_open.opentelemetry_instrumentation_urllib_applied = True OpenerDirector.open = instrumented_open def _uninstrument(): """Disables instrumentation of :code:`urllib` through this module. Note that this only works if no other module also patches urllib.""" _uninstrument_from(OpenerDirector) def _uninstrument_from(instr_root, restore_as_bound_func: bool = False): instr_func_name = "open" instr_func = getattr(instr_root, instr_func_name) if not getattr( instr_func, "opentelemetry_instrumentation_urllib_applied", False, ): return original = instr_func.__wrapped__ # pylint:disable=no-member if restore_as_bound_func: original = types.MethodType(original, instr_root) setattr(instr_root, instr_func_name, original) def _get_span_name(method: str) -> str: method = sanitize_method(method.strip()) if method == "_OTHER": method = "HTTP" return method def _set_status_code_attribute( span: Span, status_code: int, metric_attributes: dict[str, Any] | None = None, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ) -> None: status_code_str = str(status_code) try: status_code = int(status_code) except ValueError: status_code = -1 if metric_attributes is None: metric_attributes = {} _set_status( span, metric_attributes, status_code, status_code_str, server_span=False, sem_conv_opt_in_mode=sem_conv_opt_in_mode, ) def _create_client_histograms( meter: Meter, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT ) -> dict[str, Histogram]: histograms = {} if _report_old(sem_conv_opt_in_mode): histograms[MetricInstruments.HTTP_CLIENT_DURATION] = meter.create_histogram( name=MetricInstruments.HTTP_CLIENT_DURATION, unit="ms", description="Measures the duration of the outbound HTTP request", ) histograms[MetricInstruments.HTTP_CLIENT_REQUEST_SIZE] = meter.create_histogram( name=MetricInstruments.HTTP_CLIENT_REQUEST_SIZE, unit="By", description="Measures the size of HTTP request messages.", ) histograms[MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE] = meter.create_histogram( name=MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE, unit="By", description="Measures the size of HTTP response messages.", ) if _report_new(sem_conv_opt_in_mode): histograms[HTTP_CLIENT_REQUEST_DURATION] = meter.create_histogram( name=HTTP_CLIENT_REQUEST_DURATION, unit="s", description="Duration of HTTP client requests.", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) histograms[HTTP_CLIENT_REQUEST_BODY_SIZE] = create_http_client_request_body_size(meter) histograms[HTTP_CLIENT_RESPONSE_BODY_SIZE] = create_http_client_response_body_size(meter) return histograms def _record_histograms( histograms: dict[str, Histogram], metric_attributes_old: Attributes, metric_attributes_new: Attributes, request_size: int, response_size: int, duration_s: float, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ): if _report_old(sem_conv_opt_in_mode): duration = max(round(duration_s * 1000), 0) histograms[MetricInstruments.HTTP_CLIENT_DURATION].record(duration, attributes=metric_attributes_old) histograms[MetricInstruments.HTTP_CLIENT_REQUEST_SIZE].record(request_size, attributes=metric_attributes_old) histograms[MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE].record(response_size, attributes=metric_attributes_old) if _report_new(sem_conv_opt_in_mode): histograms[HTTP_CLIENT_REQUEST_DURATION].record(duration_s, attributes=metric_attributes_new) histograms[HTTP_CLIENT_REQUEST_BODY_SIZE].record(request_size, attributes=metric_attributes_new) histograms[HTTP_CLIENT_RESPONSE_BODY_SIZE].record(response_size, attributes=metric_attributes_new) package.py000066400000000000000000000003221527070072300445360ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations _instruments: tuple[str, ...] = tuple() _supports_metrics = True _semconv_status = "migration" py.typed000066400000000000000000000000001527070072300442610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllibversion.py000066400000000000000000000001441527070072300446320ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000004731527070072300366220ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllibasgiref==3.8.1 Deprecated==1.2.14 mocket==3.14.1 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-urllib opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib/tests/000077500000000000000000000000001527070072300335765ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300356160ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib/teststest_metrics_instrumentation.py000066400000000000000000000372611527070072300421320ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from platform import python_implementation from timeit import default_timer from unittest.mock import patch from urllib import request from urllib.parse import urlencode from mocket import Mocketizer from mocket.mocks.mockhttp import Entry from pytest import mark from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.urllib import ( # pylint: disable=no-name-in-module,import-error URLLibInstrumentor, ) from opentelemetry.semconv._incubating.metrics.http_metrics import ( HTTP_CLIENT_REQUEST_BODY_SIZE, HTTP_CLIENT_RESPONSE_BODY_SIZE, ) from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.semconv.metrics.http_metrics import ( HTTP_CLIENT_REQUEST_DURATION, ) from opentelemetry.test.test_base import TestBase SCOPE = "opentelemetry.instrumentation.urllib" class TestUrllibMetricsInstrumentation(TestBase): URL = "http://mock/status/200" URL_POST = "http://mock/post" def setUp(self): super().setUp() test_name = "" if hasattr(self, "_testMethodName"): test_name = self._testMethodName sem_conv_mode = "default" if "new_semconv" in test_name: sem_conv_mode = "http" elif "both_semconv" in test_name: sem_conv_mode = "http/dup" self.env_patch = patch.dict( "os.environ", { OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) _OpenTelemetrySemanticConventionStability._initialized = False self.env_patch.start() URLLibInstrumentor().instrument() self.mocketizer = Mocketizer(strict_mode=True) self.mocketizer.enter() Entry.single_register(Entry.GET, self.URL, body=b"Hello!") Entry.single_register(Entry.POST, self.URL_POST, body=b"Hello World!") def tearDown(self): super().tearDown() URLLibInstrumentor().uninstrument() self.mocketizer.exit() # Return Sequence with one histogram def create_histogram_data_points(self, sum_data_point, attributes, explicit_bounds=None): return [ self.create_histogram_data_point( sum_data_point, 1, sum_data_point, sum_data_point, attributes, explicit_bounds=explicit_bounds, ) ] def test_basic_metric(self): start_time = default_timer() with request.urlopen(self.URL) as result: client_duration_estimated = (default_timer() - start_time) * 1000 metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 3) ( client_duration, client_request_size, client_response_size, ) = metrics[:3] self.assertEqual(client_duration.name, MetricInstruments.HTTP_CLIENT_DURATION) self.assert_metric_expected( client_duration, self.create_histogram_data_points( client_duration_estimated, attributes={ "http.status_code": int(result.code), "http.method": "GET", "http.url": str(result.url), "http.flavor": "1.1", }, ), est_value_delta=40, ) self.assertEqual( client_request_size.name, MetricInstruments.HTTP_CLIENT_REQUEST_SIZE, ) self.assert_metric_expected( client_request_size, self.create_histogram_data_points( 0, attributes={ "http.status_code": int(result.code), "http.method": "GET", "http.url": str(result.url), "http.flavor": "1.1", }, ), ) self.assertEqual( client_response_size.name, MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE, ) self.assert_metric_expected( client_response_size, self.create_histogram_data_points( result.length, attributes={ "http.status_code": int(result.code), "http.method": "GET", "http.url": str(result.url), "http.flavor": "1.1", }, ), ) def test_basic_metric_new_semconv(self): start_time = default_timer() with request.urlopen(self.URL) as result: duration_s = default_timer() - start_time metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 3) ( client_request_body_size, client_request_duration, client_response_body_size, ) = metrics[:3] self.assertEqual(client_request_duration.name, HTTP_CLIENT_REQUEST_DURATION) self.assert_metric_expected( client_request_duration, self.create_histogram_data_points( duration_s, attributes={ "http.response.status_code": int(result.code), "http.request.method": "GET", "network.protocol.version": "1.1", }, explicit_bounds=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ), est_value_delta=40, ) self.assertEqual( client_request_body_size.name, HTTP_CLIENT_REQUEST_BODY_SIZE, ) self.assert_metric_expected( client_request_body_size, self.create_histogram_data_points( 0, attributes={ "http.response.status_code": int(result.code), "http.request.method": "GET", "network.protocol.version": "1.1", }, ), ) self.assertEqual( client_response_body_size.name, HTTP_CLIENT_RESPONSE_BODY_SIZE, ) self.assert_metric_expected( client_response_body_size, self.create_histogram_data_points( result.length, attributes={ "http.response.status_code": int(result.code), "http.request.method": "GET", "network.protocol.version": "1.1", }, ), ) def test_basic_metric_both_semconv(self): start_time = default_timer() with request.urlopen(self.URL) as result: duration_s = default_timer() - start_time duration = max(round(duration_s * 1000), 0) metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 6) ( client_duration, client_request_body_size, client_request_duration, client_request_size, client_response_body_size, client_response_size, ) = metrics[:6] self.assertEqual(client_duration.name, MetricInstruments.HTTP_CLIENT_DURATION) self.assert_metric_expected( client_duration, self.create_histogram_data_points( duration, attributes={ "http.status_code": int(result.code), "http.method": "GET", "http.url": str(result.url), "http.flavor": "1.1", }, ), est_value_delta=40, ) self.assertEqual( client_request_size.name, MetricInstruments.HTTP_CLIENT_REQUEST_SIZE, ) self.assert_metric_expected( client_request_size, self.create_histogram_data_points( 0, attributes={ "http.status_code": int(result.code), "http.method": "GET", "http.url": str(result.url), "http.flavor": "1.1", }, ), ) self.assertEqual( client_response_size.name, MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE, ) self.assert_metric_expected( client_response_size, self.create_histogram_data_points( result.length, attributes={ "http.status_code": int(result.code), "http.method": "GET", "http.url": str(result.url), "http.flavor": "1.1", }, ), ) self.assertEqual(client_request_duration.name, HTTP_CLIENT_REQUEST_DURATION) self.assert_metric_expected( client_request_duration, self.create_histogram_data_points( duration_s, attributes={ "http.response.status_code": int(result.code), "http.request.method": "GET", "network.protocol.version": "1.1", }, explicit_bounds=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ), est_value_delta=40, ) self.assertEqual( client_request_body_size.name, HTTP_CLIENT_REQUEST_BODY_SIZE, ) self.assert_metric_expected( client_request_body_size, self.create_histogram_data_points( 0, attributes={ "http.response.status_code": int(result.code), "http.request.method": "GET", "network.protocol.version": "1.1", }, ), ) self.assertEqual( client_response_body_size.name, HTTP_CLIENT_RESPONSE_BODY_SIZE, ) self.assert_metric_expected( client_response_body_size, self.create_histogram_data_points( result.length, attributes={ "http.response.status_code": int(result.code), "http.request.method": "GET", "network.protocol.version": "1.1", }, ), ) def test_basic_metric_request_not_empty(self): data = {"header1": "value1", "header2": "value2"} data_encoded = urlencode(data).encode() start_time = default_timer() with request.urlopen(self.URL_POST, data=data_encoded) as result: client_duration_estimated = (default_timer() - start_time) * 1000 metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 3) ( client_duration, client_request_size, client_response_size, ) = metrics[:3] self.assertEqual(client_duration.name, MetricInstruments.HTTP_CLIENT_DURATION) self.assert_metric_expected( client_duration, self.create_histogram_data_points( client_duration_estimated, attributes={ "http.status_code": int(result.code), "http.method": "POST", "http.url": str(result.url), "http.flavor": "1.1", }, ), est_value_delta=40, ) self.assertEqual( client_request_size.name, MetricInstruments.HTTP_CLIENT_REQUEST_SIZE, ) self.assert_metric_expected( client_request_size, self.create_histogram_data_points( len(data_encoded), attributes={ "http.status_code": int(result.code), "http.method": "POST", "http.url": str(result.url), "http.flavor": "1.1", }, ), ) self.assertEqual( client_response_size.name, MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE, ) self.assert_metric_expected( client_response_size, self.create_histogram_data_points( result.length, attributes={ "http.status_code": int(result.code), "http.method": "POST", "http.url": str(result.url), "http.flavor": "1.1", }, ), ) @mark.skipif(python_implementation() == "PyPy", reason="Fails randomly in pypy") def test_metric_uninstrument(self): with request.urlopen(self.URL): metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 3) ( client_duration, client_request_size, client_response_size, ) = metrics[:3] self.assertEqual( sum(client_duration.data.data_points[0].bucket_counts), 1, ) self.assertEqual( sum(client_request_size.data.data_points[0].bucket_counts), 1, ) self.assertEqual( sum(client_response_size.data.data_points[0].bucket_counts), 1, ) with request.urlopen(self.URL): metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 3) ( client_duration, client_request_size, client_response_size, ) = metrics[:3] self.assertEqual( sum(client_duration.data.data_points[0].bucket_counts), 2, ) self.assertEqual( sum(client_request_size.data.data_points[0].bucket_counts), 2, ) self.assertEqual( sum(client_response_size.data.data_points[0].bucket_counts), 2, ) URLLibInstrumentor().uninstrument() with request.urlopen(self.URL): metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 3) ( client_duration, client_request_size, client_response_size, ) = metrics[:3] self.assertEqual(len(metrics), 3) self.assertEqual( sum(client_duration.data.data_points[0].bucket_counts), 2, ) self.assertEqual( sum(client_request_size.data.data_points[0].bucket_counts), 2, ) self.assertEqual( sum(client_response_size.data.data_points[0].bucket_counts), 2, ) test_urllib_integration.py000066400000000000000000000730321527070072300410310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import abc import urllib from unittest import mock from unittest.mock import patch from urllib import request from urllib.error import HTTPError from urllib.request import OpenerDirector from mocket import Mocket, Mocketizer from mocket.mocks.mockhttp import Entry import opentelemetry.instrumentation.urllib # pylint: disable=no-name-in-module,import-error from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.urllib import ( # pylint: disable=no-name-in-module,import-error URLLibInstrumentor, ) from opentelemetry.instrumentation.utils import ( suppress_http_instrumentation, suppress_instrumentation, ) from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.sdk import resources from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_METHOD, HTTP_STATUS_CODE, HTTP_URL, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, ) from opentelemetry.semconv.attributes.url_attributes import URL_FULL from opentelemetry.test.mock_textmap import MockTextMapPropagator from opentelemetry.test.test_base import TestBase from opentelemetry.trace import StatusCode from opentelemetry.util.http import get_excluded_urls # pylint: disable=too-many-public-methods class URLLibIntegrationTestBase(abc.ABC): # pylint: disable=no-member URL = "http://mock/status/200" URL_TIMEOUT = "http://mock/timeout/0" URL_EXCEPTION = "http://mock/exception/0" # pylint: disable=invalid-name def setUp(self): super().setUp() test_name = "" if hasattr(self, "_testMethodName"): test_name = self._testMethodName sem_conv_mode = "default" if "new_semconv" in test_name: sem_conv_mode = "http" elif "both_semconv" in test_name: sem_conv_mode = "http/dup" self.env_patch = mock.patch.dict( "os.environ", { "OTEL_PYTHON_URLLIB_EXCLUDED_URLS": "http://localhost/env_excluded_arg/123,env_excluded_noarg", OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) _OpenTelemetrySemanticConventionStability._initialized = False self.env_patch.start() self.exclude_patch = mock.patch( "opentelemetry.instrumentation.urllib._excluded_urls_from_env", get_excluded_urls("URLLIB"), ) self.exclude_patch.start() URLLibInstrumentor().instrument() self.mocketizer = Mocketizer(strict_mode=True) self.mocketizer.enter() Entry.single_register(Entry.GET, self.URL, body=b"Hello!") Entry.single_register( Entry.GET, self.URL_TIMEOUT, exception=TimeoutError(), ) Entry.single_register( Entry.GET, self.URL_EXCEPTION, exception=Exception("test"), # pylint: disable=broad-exception-raised ) Entry.single_register( Entry.GET, "http://mock/status/500", status=500, ) # pylint: disable=invalid-name def tearDown(self): super().tearDown() URLLibInstrumentor().uninstrument() self.mocketizer.exit() def assert_span(self, exporter=None, num_spans=1): if exporter is None: exporter = self.memory_exporter span_list = exporter.get_finished_spans() self.assertEqual(num_spans, len(span_list)) if num_spans == 0: return None if num_spans == 1: return span_list[0] return span_list @staticmethod @abc.abstractmethod def perform_request( url: str, opener: OpenerDirector = None, headers: list[tuple[str, str]] | None = None, ): pass def test_basic(self): result = self.perform_request(self.URL) self.assertEqual(result.read(), b"Hello!") span = self.assert_span() self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "GET") self.assertEqual( span.attributes, { HTTP_METHOD: "GET", HTTP_URL: self.URL, HTTP_STATUS_CODE: 200, }, ) self.assertIs(span.status.status_code, trace.StatusCode.UNSET) self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.urllib) def test_basic_new_semconv(self): result = self.perform_request(self.URL) self.assertEqual(result.read(), b"Hello!") span = self.assert_span() self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "GET") self.assertEqual( span.attributes, { HTTP_REQUEST_METHOD: "GET", URL_FULL: self.URL, HTTP_RESPONSE_STATUS_CODE: 200, }, ) self.assertIs(span.status.status_code, trace.StatusCode.UNSET) self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.urllib) def test_basic_both_semconv(self): result = self.perform_request(self.URL) self.assertEqual(result.read(), b"Hello!") span = self.assert_span() self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "GET") self.assertEqual( span.attributes, { HTTP_METHOD: "GET", HTTP_URL: self.URL, HTTP_STATUS_CODE: 200, HTTP_REQUEST_METHOD: "GET", URL_FULL: self.URL, HTTP_RESPONSE_STATUS_CODE: 200, }, ) self.assertIs(span.status.status_code, trace.StatusCode.UNSET) self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.urllib) def test_excluded_urls_explicit(self): url_201 = "http://mock/status/201" Entry.single_register( Entry.GET, url_201, status=201, ) URLLibInstrumentor().uninstrument() URLLibInstrumentor().instrument(excluded_urls=".*/201") self.perform_request(self.URL) self.perform_request(url_201) self.assert_span(num_spans=1) def test_excluded_urls_from_env(self): url = "http://localhost/env_excluded_arg/123" Entry.single_register( Entry.GET, url, status=200, ) URLLibInstrumentor().uninstrument() URLLibInstrumentor().instrument() self.perform_request(self.URL) self.perform_request(url) self.assert_span(num_spans=1) def test_not_foundbasic(self): url_404 = "http://mock/status/404/" Entry.single_register( Entry.GET, url_404, status=404, ) exception = None try: self.perform_request(url_404) except Exception as err: # pylint: disable=broad-except exception = err code = exception.code self.assertEqual(code, 404) span = self.assert_span() self.assertEqual(span.attributes.get(HTTP_STATUS_CODE), 404) self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) def test_not_foundbasic_new_semconv(self): url_404 = "http://mock/status/404/" Entry.single_register( Entry.GET, url_404, status=404, ) exception = None try: self.perform_request(url_404) except Exception as err: # pylint: disable=broad-except exception = err code = exception.code self.assertEqual(code, 404) span = self.assert_span() self.assertEqual(span.attributes.get(HTTP_RESPONSE_STATUS_CODE), 404) self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) def test_not_foundbasic_both_semconv(self): url_404 = "http://mock/status/404/" Entry.single_register( Entry.GET, url_404, status=404, ) exception = None try: self.perform_request(url_404) except Exception as err: # pylint: disable=broad-except exception = err code = exception.code self.assertEqual(code, 404) span = self.assert_span() self.assertEqual(span.attributes.get(HTTP_STATUS_CODE), 404) self.assertEqual(span.attributes.get(HTTP_RESPONSE_STATUS_CODE), 404) self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) @staticmethod def mock_get_code(*args, **kwargs): return None @patch("http.client.HTTPResponse.getcode", new=mock_get_code) def test_response_code_none(self): result = self.perform_request(self.URL) self.assertEqual(result.read(), b"Hello!") span = self.assert_span() self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "GET") self.assertEqual( span.attributes, { HTTP_METHOD: "GET", HTTP_URL: self.URL, }, ) self.assertIs(span.status.status_code, trace.StatusCode.UNSET) self.assertEqualSpanInstrumentationScope(span, opentelemetry.instrumentation.urllib) def test_uninstrument(self): URLLibInstrumentor().uninstrument() result = self.perform_request(self.URL) self.assertEqual(result.read(), b"Hello!") self.assert_span(num_spans=0) # instrument again to avoid annoying warning message URLLibInstrumentor().instrument() def test_uninstrument_session(self): clienr1 = urllib.request.build_opener() URLLibInstrumentor().uninstrument_opener(clienr1) result = self.perform_request(self.URL, clienr1) self.assertEqual(result.read(), b"Hello!") self.assert_span(num_spans=0) # Test that other sessions as well as global requests is still # instrumented opener2 = urllib.request.build_opener() result = self.perform_request(self.URL, opener2) self.assertEqual(result.read(), b"Hello!") self.assert_span() self.memory_exporter.clear() result = self.perform_request(self.URL) self.assertEqual(result.read(), b"Hello!") self.assert_span() def test_suppress_instrumentation(self): with suppress_instrumentation(): result = self.perform_request(self.URL) self.assertEqual(result.read(), b"Hello!") self.assert_span(num_spans=0) def test_suppress_http_instrumentation(self): with suppress_http_instrumentation(): result = self.perform_request(self.URL) self.assertEqual(result.read(), b"Hello!") self.assert_span(num_spans=0) def test_not_recording(self): with mock.patch("opentelemetry.trace.INVALID_SPAN") as mock_span: URLLibInstrumentor().uninstrument() URLLibInstrumentor().instrument(tracer_provider=trace.NoOpTracerProvider()) mock_span.is_recording.return_value = False result = self.perform_request(self.URL) self.assertEqual(result.read(), b"Hello!") self.assert_span(None, 0) self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) def test_distributed_context(self): previous_propagator = get_global_textmap() try: set_global_textmap(MockTextMapPropagator()) result = self.perform_request(self.URL) self.assertEqual(result.read(), b"Hello!") span = self.assert_span() headers_ = dict(Mocket.last_request().headers) headers = {} for k, v in headers_.items(): headers[k.lower()] = v self.assertIn(MockTextMapPropagator.TRACE_ID_KEY, headers) self.assertEqual( str(span.get_span_context().trace_id), headers[MockTextMapPropagator.TRACE_ID_KEY], ) self.assertIn(MockTextMapPropagator.SPAN_ID_KEY, headers) self.assertEqual( str(span.get_span_context().span_id), headers[MockTextMapPropagator.SPAN_ID_KEY], ) finally: set_global_textmap(previous_propagator) def test_custom_tracer_provider(self): resource = resources.Resource.create({}) result = self.create_tracer_provider(resource=resource) tracer_provider, exporter = result URLLibInstrumentor().uninstrument() URLLibInstrumentor().instrument(tracer_provider=tracer_provider) result = self.perform_request(self.URL) self.assertEqual(result.read(), b"Hello!") span = self.assert_span(exporter=exporter) self.assertIs(span.resource, resource) def test_requests_exception_with_response(self, *_, **__): with self.assertRaises(HTTPError): self.perform_request("http://mock/status/500") span = self.assert_span() self.assertEqual( dict(span.attributes), { HTTP_METHOD: "GET", HTTP_URL: "http://mock/status/500", HTTP_STATUS_CODE: 500, }, ) self.assertEqual(span.status.status_code, StatusCode.ERROR) def test_requests_exception_with_response_new_semconv(self, *_, **__): with self.assertRaises(HTTPError): self.perform_request("http://mock/status/500") span = self.assert_span() self.assertEqual( dict(span.attributes), { HTTP_REQUEST_METHOD: "GET", URL_FULL: "http://mock/status/500", HTTP_RESPONSE_STATUS_CODE: 500, ERROR_TYPE: "HTTPError", }, ) self.assertEqual(span.status.status_code, StatusCode.ERROR) def test_requests_exception_with_response_both_semconv(self, *_, **__): with self.assertRaises(HTTPError): self.perform_request("http://mock/status/500") span = self.assert_span() self.assertEqual( dict(span.attributes), { HTTP_METHOD: "GET", HTTP_URL: "http://mock/status/500", HTTP_STATUS_CODE: 500, HTTP_REQUEST_METHOD: "GET", URL_FULL: "http://mock/status/500", HTTP_RESPONSE_STATUS_CODE: 500, ERROR_TYPE: "HTTPError", }, ) self.assertEqual(span.status.status_code, StatusCode.ERROR) def test_requests_basic_exception(self, *_, **__): with self.assertRaises(Exception): self.perform_request(self.URL_EXCEPTION) span = self.assert_span() self.assertEqual(span.status.status_code, StatusCode.ERROR) def test_requests_timeout_exception(self, *_, **__): with self.assertRaises(Exception): opener = urllib.request.build_opener() opener.open(self.URL_TIMEOUT, timeout=0.0001) span = self.assert_span() self.assertEqual(span.status.status_code, StatusCode.ERROR) def test_remove_sensitive_params(self): url = "http://username:password@mock/status/200" Mocket.reset() Entry.single_register(Entry.GET, url, exception=Exception("test")) with self.assertRaises(Exception): self.perform_request(url) span = self.assert_span() self.assertEqual( span.attributes[HTTP_URL], "http://REDACTED:REDACTED@mock/status/200", ) def test_hooks(self): def request_hook(span, request_obj): span.update_name("name set from hook") def response_hook(span, request_obj, response): span.set_attribute("response_hook_attr", "value") URLLibInstrumentor().uninstrument() URLLibInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook) result = self.perform_request(self.URL) self.assertEqual(result.read(), b"Hello!") span = self.assert_span() self.assertEqual(span.name, "name set from hook") self.assertIn("response_hook_attr", span.attributes) self.assertEqual(span.attributes["response_hook_attr"], "value") def test_no_op_tracer_provider(self): URLLibInstrumentor().uninstrument() tracer_provider = trace.NoOpTracerProvider() URLLibInstrumentor().instrument(tracer_provider=tracer_provider) result = self.perform_request(self.URL) self.assertEqual(result.read(), b"Hello!") self.assert_span(num_spans=0) def test_custom_response_headers_captured(self): URLLibInstrumentor().uninstrument() URLLibInstrumentor().instrument(captured_response_headers=["X-Custom-Header", "X-Another-Header"]) response_headers = { "X-Custom-Header": "custom-value", "X-Another-Header": "another-value", } url = "http://mock/capture_headers" Entry.single_register( Entry.GET, url, body="Hello!", headers=response_headers, ) self.perform_request(url) span = self.assert_span(num_spans=1) self.assertEqual( span.attributes["http.response.header.x_custom_header"], ("custom-value",), ) self.assertEqual( span.attributes["http.response.header.x_another_header"], ("another-value",), ) self.assertNotIn("http.response.header.x_excluded_header", span.attributes) def test_custom_headers_not_captured_when_not_configured(self): """Test that headers are not captured when env vars are not set.""" URLLibInstrumentor().uninstrument() URLLibInstrumentor().instrument() self.perform_request( self.URL, headers=[("X-Request-Header", "request-value")], ) span = self.assert_span(num_spans=1) self.assertNotIn("http.request.header.x_request_header", span.attributes) self.assertNotIn("http.response.header.x_response_header", span.attributes) def test_sensitive_headers_sanitized(self): """Test that sensitive header values are redacted.""" URLLibInstrumentor().uninstrument() URLLibInstrumentor().instrument( captured_request_headers=["Authorization", "X-Api-Key"], captured_response_headers=["Set-Cookie", "X-Secret"], sensitive_headers=[ "Authorization", "X-Api-Key", "Set-Cookie", "X-Secret", ], ) response_headers = { "Set-Cookie": "session=abc123", "X-Secret": "secret", } url = "http://mock/capture_headers" Entry.single_register( Entry.GET, url, body="Hello!", headers=response_headers, ) self.perform_request( url, headers=[ ("Authorization", "Bearer secret-token"), ("X-Api-Key", "secret-key"), ], ) span = self.assert_span(num_spans=1) self.assertEqual( span.attributes["http.request.header.authorization"], ("[REDACTED]",), ) self.assertEqual( span.attributes["http.request.header.x_api_key"], ("[REDACTED]",), ) self.assertEqual( span.attributes["http.response.header.set_cookie"], ("[REDACTED]",), ) self.assertEqual( span.attributes["http.response.header.x_secret"], ("[REDACTED]",), ) def test_custom_headers_with_regex(self): """Test that header capture works with regex patterns.""" URLLibInstrumentor().uninstrument() URLLibInstrumentor().instrument( captured_request_headers=["X-Custom-Request-.*"], captured_response_headers=["X-Custom-Response-.*"], ) response_headers = { "X-Custom-Response-A": "value-A", "X-Custom-Response-B": "value-B", "X-Other-Response-Header": "other-value", } url = "http://mock/capture_headers" Entry.single_register( Entry.GET, url, body="Hello!", headers=response_headers, ) self.perform_request( url, headers=[ ("X-Custom-Request-One", "value-one"), ("X-Custom-Request-Two", "value-two"), ("X-Other-Request-Header", "other-value"), ], ) span = self.assert_span(num_spans=1) self.assertEqual( span.attributes["http.request.header.x_custom_request_one"], ("value-one",), ) self.assertEqual( span.attributes["http.request.header.x_custom_request_two"], ("value-two",), ) self.assertNotIn("http.request.header.x_other_request_header", span.attributes) self.assertEqual( span.attributes["http.response.header.x_custom_response_a"], ("value-A",), ) self.assertEqual( span.attributes["http.response.header.x_custom_response_b"], ("value-B",), ) self.assertNotIn("http.response.header.x_other_response_header", span.attributes) def test_custom_headers_case_insensitive(self): """Test that header capture is case-insensitive.""" URLLibInstrumentor().uninstrument() URLLibInstrumentor().instrument( captured_request_headers=["x-request-header"], captured_response_headers=["x-response-header"], ) response_headers = {"X-ReSPoNse-HeaDER": "custom-value"} url = "http://mock/capture_headers" Entry.single_register( Entry.GET, url, body="Hello!", headers=response_headers, ) self.perform_request( url, headers=[("X-ReQuESt-HeaDER", "custom-value")], ) span = self.assert_span(num_spans=1) self.assertEqual( span.attributes["http.request.header.x_request_header"], ("custom-value",), ) self.assertEqual( span.attributes["http.response.header.x_response_header"], ("custom-value",), ) def test_standard_http_headers_captured(self): """Test that standard HTTP headers can be captured.""" URLLibInstrumentor().uninstrument() URLLibInstrumentor().instrument( captured_request_headers=["Content-Type", "Accept"], captured_response_headers=["Content-Type", "Server"], ) response_headers = { "Content-Type": "text/plain", "Server": "TestServer/1.0", } url = "http://mock/capture_headers" Entry.single_register( Entry.GET, url, body="Hello!", headers=response_headers, ) self.perform_request( url, headers=[ ("Content-Type", "application/json"), ("Accept", "application/json"), ], ) span = self.assert_span(num_spans=1) self.assertEqual( span.attributes["http.request.header.content_type"], ("application/json",), ) self.assertEqual( span.attributes["http.request.header.accept"], ("application/json",), ) self.assertEqual( span.attributes["http.response.header.content_type"], ("text/plain",), ) self.assertEqual( span.attributes["http.response.header.server"], ("TestServer/1.0",), ) def test_capture_all_request_headers(self): """Test that all request headers can be captured with .* pattern.""" URLLibInstrumentor().uninstrument() URLLibInstrumentor().instrument(captured_request_headers=[".*"]) self.perform_request( self.URL, headers=[ ("X-Header-One", "value1"), ("X-Header-Two", "value2"), ("X-Header-Three", "value3"), ], ) span = self.assert_span(num_spans=1) self.assertEqual( span.attributes["http.request.header.x_header_one"], ("value1",), ) self.assertEqual( span.attributes["http.request.header.x_header_two"], ("value2",), ) self.assertEqual( span.attributes["http.request.header.x_header_three"], ("value3",), ) def test_capture_all_response_headers(self): """Test that all response headers can be captured with .* pattern.""" URLLibInstrumentor().uninstrument() URLLibInstrumentor().instrument(captured_response_headers=[".*"]) response_headers = { "X-Response-One": "value1", "X-Response-Two": "value2", "X-Response-Three": "value3", } url = "http://mock/capture_headers" Entry.single_register( Entry.GET, url, body="Hello!", headers=response_headers, ) self.perform_request(url) span = self.assert_span(num_spans=1) self.assertEqual( span.attributes["http.response.header.x_response_one"], ("value1",), ) self.assertEqual( span.attributes["http.response.header.x_response_two"], ("value2",), ) self.assertEqual( span.attributes["http.response.header.x_response_three"], ("value3",), ) def test_sanitize_with_regex_pattern(self): """Test that sanitization works with regex patterns.""" URLLibInstrumentor().uninstrument() URLLibInstrumentor().instrument( captured_request_headers=["X-Test.*"], sensitive_headers=[".*secret.*"], ) self.perform_request( self.URL, headers=[ ("X-Test", "normal-value"), ("X-Test-Secret", "secret-value"), ], ) span = self.assert_span(num_spans=1) self.assertEqual( span.attributes["http.request.header.x_test"], ("normal-value",), ) self.assertEqual( span.attributes["http.request.header.x_test_secret"], ("[REDACTED]",), ) @mock.patch.dict( "os.environ", { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST": "x-request-one,x-request-two", "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE": "x-response-one", "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS": "x-request-two", }, ) def test_capture_and_sanitize_environment_variables(self): URLLibInstrumentor().uninstrument() URLLibInstrumentor().instrument() response_headers = { "X-Response-One": "value1", "X-Response-Two": "value2", } url = "http://mock/capture_headers" Entry.single_register( Entry.GET, url, body="Hello!", headers=response_headers, ) self.perform_request(url, headers=[("x-request-one", "one"), ("x-request-two", "two")]) span = self.assert_span(num_spans=1) self.assertEqual( span.attributes["http.request.header.x_request_one"], ("one",), ) self.assertEqual( span.attributes["http.request.header.x_request_two"], ("[REDACTED]",), ) self.assertEqual( span.attributes["http.response.header.x_response_one"], ("value1",), ) self.assertNotIn( "http.response.header.x_response_two", (span.attributes,), ) def test_only_the_first_header_is_sent_with_duplicated_headers(self): URLLibInstrumentor().uninstrument() URLLibInstrumentor().instrument( captured_request_headers=["X-foo"], ) result = self.perform_request(self.URL, headers=[("X-foo", "foo"), ("X-foo", "bar")]) self.assertEqual(result.read(), b"Hello!") span = self.assert_span() self.assertEqual( span.attributes["http.request.header.x_foo"], ("foo",), ) class TestURLLibIntegration(URLLibIntegrationTestBase, TestBase): @staticmethod def perform_request( url: str, opener: OpenerDirector = None, headers: list[tuple[str, str]] | None = None, ): if not opener: opener = urllib.request.build_opener() if headers: opener.addheaders = headers return opener.open(fullurl=url) def test_invalid_url(self): url = "http://[::1/nope" with self.assertRaises(ValueError): request.Request(url, method="POST") self.assert_span(num_spans=0) def test_if_headers_equals_none(self): result = self.perform_request(self.URL) self.assertEqual(result.read(), b"Hello!") self.assert_span() opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3/000077500000000000000000000000001527070072300325175ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3/LICENSE000066400000000000000000000261351527070072300335330ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3/README.rst000066400000000000000000000053231527070072300342110ustar00rootroot00000000000000OpenTelemetry urllib3 Instrumentation ====================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-urllib3.svg :target: https://pypi.org/project/opentelemetry-instrumentation-urllib3/ This library allows tracing HTTP requests made by the `urllib3 `_ library. Installation ------------ :: pip install opentelemetry-instrumentation-urllib3 Usage ----- .. code-block:: python import urllib3 from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor def strip_query_params(url: str) -> str: return url.split("?")[0] URLLib3Instrumentor().instrument( # Remove all query params from the URL attribute on the span. url_filter=strip_query_params, ) http = urllib3.PoolManager() response = http.request("GET", "https://www.example.org/") Configuration ------------- Request/Response hooks ********************** The urllib3 instrumentation supports extending tracing behavior with the help of request and response hooks. These are functions that are called back by the instrumentation right after a Span is created for a request and right before the span is finished processing a response respectively. The hooks can be configured as follows: .. code:: python from typing import Any from urllib3.connectionpool import HTTPConnectionPool from urllib3.response import HTTPResponse from opentelemetry.instrumentation.urllib3 import RequestInfo, URLLib3Instrumentor from opentelemetry.trace import Span def request_hook( span: Span, pool: HTTPConnectionPool, request_info: RequestInfo, ) -> Any: pass def response_hook( span: Span, pool: HTTPConnectionPool, response: HTTPResponse, ) -> Any: pass URLLib3Instrumentor().instrument( request_hook=request_hook, response_hook=response_hook, ) Exclude lists ************* To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_URLLIB3_EXCLUDED_URLS`` (or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude. For example, :: export OTEL_PYTHON_URLLIB3_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. References ---------- * `OpenTelemetry urllib3 Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ benchmark-requirements.txt000066400000000000000000000002411527070072300376510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3mocket==3.14.1 pytest-benchmark==4.0.0 -e opentelemetry-instrumentation -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-urllib3 benchmarks/000077500000000000000000000000001527070072300345555ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3test_benchmark_sanitize_method.py000066400000000000000000000023431527070072300433700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3/benchmarks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import pytest import urllib3 from mocket import Mocketizer from mocket.mocks.mockhttp import Entry from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) _URL = "http://mock/status/200" @pytest.fixture(name="connection_pool") def _connection_pool_fixture(): exporter = InMemorySpanExporter() provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(exporter)) URLLib3Instrumentor().instrument(tracer_provider=provider) pool = urllib3.HTTPConnectionPool("mock", port=80) with Mocketizer(): Entry.single_register(Entry.GET, _URL, body="Hello!", match_querystring=False) yield pool URLLib3Instrumentor().uninstrument() def test_instrumented_urlopen(benchmark, connection_pool: urllib3.HTTPConnectionPool): def run(): Entry.single_register(Entry.GET, _URL, body="Hello!", match_querystring=False) connection_pool.urlopen("GET", "/status/200") benchmark(run) pyproject.toml000066400000000000000000000032501527070072300353540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-urllib3" dynamic = ["version"] description = "OpenTelemetry urllib3 instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-util-http == 0.66b2", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [ "urllib3 >= 1.0.0, < 3.0.0", ] [project.entry-points.opentelemetry_instrumentor] urllib3 = "opentelemetry.instrumentation.urllib3:URLLib3Instrumentor" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-urllib3" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/urllib3/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3/src/000077500000000000000000000000001527070072300333065ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300361235ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3/srcinstrumentation/000077500000000000000000000000001527070072300413665ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetryurllib3/000077500000000000000000000000001527070072300427425ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation__init__.py000066400000000000000000000574451527070072300450720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This library allows tracing HTTP requests made by the `urllib3 `_ library. Usage ----- .. code-block:: python import urllib3 from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor def strip_query_params(url: str) -> str: return url.split("?")[0] URLLib3Instrumentor().instrument( # Remove all query params from the URL attribute on the span. url_filter=strip_query_params, ) http = urllib3.PoolManager() response = http.request("GET", "https://www.example.org/") Configuration ------------- Request/Response hooks ********************** The urllib3 instrumentation supports extending tracing behavior with the help of request and response hooks. These are functions that are called back by the instrumentation right after a Span is created for a request and right before the span is finished processing a response respectively. The hooks can be configured as follows: .. code:: python from typing import Any from urllib3.connectionpool import HTTPConnectionPool from urllib3.response import HTTPResponse from opentelemetry.instrumentation.urllib3 import RequestInfo, URLLib3Instrumentor from opentelemetry.trace import Span def request_hook( span: Span, pool: HTTPConnectionPool, request_info: RequestInfo, ) -> Any: pass def response_hook( span: Span, pool: HTTPConnectionPool, response: HTTPResponse, ) -> Any: pass URLLib3Instrumentor().instrument( request_hook=request_hook, response_hook=response_hook, ) Exclude lists ************* To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_URLLIB3_EXCLUDED_URLS`` (or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude. For example, :: export OTEL_PYTHON_URLLIB3_EXCLUDED_URLS="client/.*/info,healthcheck" will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. Capture HTTP request and response headers ***************************************** You can configure the agent to capture specified HTTP headers as span attributes, according to the `semantic conventions `_. Request headers *************** To capture HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST`` to a comma delimited list of HTTP header names. For example using the environment variable, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST="content-type,custom_request_header" will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. Request header names in urllib3 are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST="Accept.*,X-.*" Would match all request headers that start with ``Accept`` and ``X-``. To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST=".*" The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a single item list containing all the header values. For example: ``http.request.header.custom_request_header = ["", ""]`` Response headers **************** To capture HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE`` to a comma delimited list of HTTP header names. For example using the environment variable, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE="content-type,custom_response_header" will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. Response header names in urllib3 are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE="Content.*,X-.*" Would match all response headers that start with ``Content`` and ``X-``. To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE=".*" The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a list containing the header values. For example: ``http.response.header.custom_response_header = ["", ""]`` Sanitizing headers ****************** In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be matched in a case-insensitive manner. For example using the environment variable, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. Note: The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. API --- """ import collections.abc import inspect import io import typing from collections.abc import Callable, Collection from dataclasses import dataclass from inspect import BoundArguments from timeit import default_timer import urllib3.connectionpool import wrapt from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, _client_duration_attrs_new, _client_duration_attrs_old, _filter_semconv_duration_attrs, _get_schema_url, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _report_new, _report_old, _set_http_host_client, _set_http_method, _set_http_net_peer_name_client, _set_http_network_protocol_version, _set_http_peer_port_client, _set_http_scheme, _set_http_url, _set_status, _StabilityMode, ) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.urllib3.package import _instruments from opentelemetry.instrumentation.urllib3.version import __version__ from opentelemetry.instrumentation.utils import ( is_http_instrumentation_enabled, suppress_http_instrumentation, unwrap, ) from opentelemetry.metrics import Histogram, get_meter from opentelemetry.propagate import inject from opentelemetry.semconv._incubating.metrics.http_metrics import ( create_http_client_request_body_size, create_http_client_response_body_size, ) from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.semconv.metrics.http_metrics import ( HTTP_CLIENT_REQUEST_DURATION, ) from opentelemetry.trace import Span, SpanKind, Tracer, get_tracer from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, ExcludeList, get_custom_header_attributes, get_custom_headers, get_excluded_urls, normalise_request_header_name, normalise_response_header_name, parse_excluded_urls, sanitize_method, ) from opentelemetry.util.http.httplib import set_ip_on_next_http_connection _excluded_urls_from_env = get_excluded_urls("URLLIB3") @dataclass class RequestInfo: """Arguments that were passed to the ``urlopen()`` call.""" __slots__ = ("method", "url", "headers", "body") # The type annotations here come from ``HTTPConnectionPool.urlopen()``. method: str url: str headers: typing.Mapping[str, str] | None body: bytes | typing.IO[typing.Any] | typing.Iterable[bytes] | str | None _UrlFilterT = Callable[[str], str] | None _RequestHookT = ( Callable[ [ Span, urllib3.connectionpool.HTTPConnectionPool, RequestInfo, ], None, ] | None ) _ResponseHookT = ( Callable[ [ Span, urllib3.connectionpool.HTTPConnectionPool, urllib3.response.HTTPResponse, ], None, ] | None ) class URLLib3Instrumentor(BaseInstrumentor): def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): """Instruments the urllib3 module Args: **kwargs: Optional arguments ``tracer_provider``: a TracerProvider, defaults to global. ``request_hook``: An optional callback that is invoked right after a span is created. ``response_hook``: An optional callback which is invoked right before the span is finished processing a response. ``url_filter``: A callback to process the requested URL prior to adding it as a span attribute. ``excluded_urls``: A string containing a comma-delimited list of regexes used to exclude URLs from tracking ``captured_request_headers``: An optional sequence of header names to capture from the request headers ``captured_response_headers``: An optional sequence of header names to capture from the response headers ``sensitive_headers``: An optional sequence of captured header names to redact """ # initialize semantic conventions opt-in if needed _OpenTelemetrySemanticConventionStability._initialize() sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) schema_url = _get_schema_url(sem_conv_opt_in_mode) tracer_provider = kwargs.get("tracer_provider") tracer = get_tracer( __name__, __version__, tracer_provider, schema_url=schema_url, ) excluded_urls = kwargs.get("excluded_urls") meter_provider = kwargs.get("meter_provider") meter = get_meter( __name__, __version__, meter_provider, schema_url=schema_url, ) duration_histogram_old = None request_size_histogram_old = None response_size_histogram_old = None if _report_old(sem_conv_opt_in_mode): # http.client.duration histogram duration_histogram_old = meter.create_histogram( name=MetricInstruments.HTTP_CLIENT_DURATION, unit="ms", description="Measures the duration of the outbound HTTP request", ) # http.client.request.size histogram request_size_histogram_old = meter.create_histogram( name=MetricInstruments.HTTP_CLIENT_REQUEST_SIZE, unit="By", description="Measures the size of HTTP request messages.", ) # http.client.response.size histogram response_size_histogram_old = meter.create_histogram( name=MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE, unit="By", description="Measures the size of HTTP response messages.", ) duration_histogram_new = None request_size_histogram_new = None response_size_histogram_new = None if _report_new(sem_conv_opt_in_mode): # http.client.request.duration histogram duration_histogram_new = meter.create_histogram( name=HTTP_CLIENT_REQUEST_DURATION, unit="s", description="Duration of HTTP client requests.", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) # http.client.request.body.size histogram request_size_histogram_new = create_http_client_request_body_size(meter) # http.client.response.body.size histogram response_size_histogram_new = create_http_client_response_body_size(meter) _instrument( tracer, duration_histogram_old, duration_histogram_new, request_size_histogram_old, request_size_histogram_new, response_size_histogram_old, response_size_histogram_new, request_hook=kwargs.get("request_hook"), response_hook=kwargs.get("response_hook"), url_filter=kwargs.get("url_filter"), excluded_urls=(_excluded_urls_from_env if excluded_urls is None else parse_excluded_urls(excluded_urls)), sem_conv_opt_in_mode=sem_conv_opt_in_mode, captured_request_headers=kwargs.get( "captured_request_headers", get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST), ), captured_response_headers=kwargs.get( "captured_response_headers", get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE), ), sensitive_headers=kwargs.get( "sensitive_headers", get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS), ), ) def _uninstrument(self, **kwargs): _uninstrument() def _get_span_name(sanitized_method: str) -> str: if sanitized_method == "_OTHER": return "HTTP" return sanitized_method # pylint: disable=too-many-locals,too-many-positional-arguments def _instrument( tracer: Tracer, duration_histogram_old: Histogram, duration_histogram_new: Histogram, request_size_histogram_old: Histogram, request_size_histogram_new: Histogram, response_size_histogram_old: Histogram, response_size_histogram_new: Histogram, request_hook: _RequestHookT = None, response_hook: _ResponseHookT = None, url_filter: _UrlFilterT = None, excluded_urls: ExcludeList = None, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, captured_request_headers: list[str] | None = None, captured_response_headers: list[str] | None = None, sensitive_headers: list[str] | None = None, ): urlopen_signature = inspect.signature(urllib3.connectionpool.HTTPConnectionPool.urlopen) def instrumented_urlopen(wrapped, instance, args, kwargs): if not is_http_instrumentation_enabled(): return wrapped(*args, **kwargs) try: bound_args = urlopen_signature.bind(instance, *args, **kwargs) except TypeError: return wrapped(*args, **kwargs) bound_args.apply_defaults() method = bound_args.arguments.get("method").upper() headers = bound_args.arguments.get("headers") body = bound_args.arguments.get("body") url = _get_url(instance, bound_args, url_filter) if excluded_urls and excluded_urls.url_disabled(url): return wrapped(*args, **kwargs) # avoid modifying original headers on inject headers = headers.copy() if headers is not None else {} sanitized_method = sanitize_method(method) span_name = _get_span_name(sanitized_method) span_attributes = {} _set_http_method( span_attributes, method, sanitized_method, sem_conv_opt_in_mode, ) _set_http_url(span_attributes, url, sem_conv_opt_in_mode) span_attributes.update( get_custom_header_attributes( headers, captured_request_headers, sensitive_headers, normalise_request_header_name, ) ) with ( tracer.start_as_current_span(span_name, kind=SpanKind.CLIENT, attributes=span_attributes) as span, set_ip_on_next_http_connection(span), ): if callable(request_hook): request_hook( span, instance, RequestInfo( method=method, url=url, headers=headers, body=body, ), ) inject(headers) bound_args.arguments["headers"] = headers # TODO: add error handling to also set exception `error.type` in new semconv with suppress_http_instrumentation(): start_time = default_timer() response = wrapped(*bound_args.args[1:], **bound_args.kwargs) duration_s = default_timer() - start_time # set http status code based on semconv metric_attributes = {} _set_status_code_attribute(span, response.status, metric_attributes, sem_conv_opt_in_mode) if callable(response_hook): response_hook(span, instance, response) request_size = _get_body_size(body) response_size = int(response.headers.get("Content-Length", 0)) _set_metric_attributes( metric_attributes, instance, response, method, sanitized_method, sem_conv_opt_in_mode, ) _record_metrics( metric_attributes, duration_histogram_old, duration_histogram_new, request_size_histogram_old, request_size_histogram_new, response_size_histogram_old, response_size_histogram_new, duration_s, request_size, response_size, sem_conv_opt_in_mode, ) if span.is_recording(): span.set_attributes( get_custom_header_attributes( response.headers, captured_response_headers, sensitive_headers, normalise_response_header_name, ) ) return response wrapt.wrap_function_wrapper( urllib3.connectionpool.HTTPConnectionPool, "urlopen", instrumented_urlopen, ) def _get_url( instance: urllib3.connectionpool.HTTPConnectionPool, bound_args: BoundArguments, url_filter: _UrlFilterT, ) -> str: url_or_path = bound_args.arguments.get("url") if not url_or_path.startswith("/"): url = url_or_path else: url = instance.scheme + "://" + instance.host if _should_append_port(instance.scheme, instance.port): url += ":" + str(instance.port) url += url_or_path if url_filter: return url_filter(url) return url def _get_body_size(body: object) -> int | None: if body is None: return 0 # pylint: disable-next=no-member if isinstance(body, collections.abc.Sized): return len(body) if isinstance(body, io.BytesIO): return body.getbuffer().nbytes return None def _should_append_port(scheme: str, port: int | None) -> bool: if not port: return False if scheme == "http" and port == 80: return False if scheme == "https" and port == 443: return False return True def _set_status_code_attribute( span: Span, status_code: int, metric_attributes: dict = None, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ) -> None: status_code_str = str(status_code) try: status_code = int(status_code) except ValueError: status_code = -1 if metric_attributes is None: metric_attributes = {} _set_status( span, metric_attributes, status_code, status_code_str, server_span=False, sem_conv_opt_in_mode=sem_conv_opt_in_mode, ) def _set_metric_attributes( metric_attributes: dict, instance: urllib3.connectionpool.HTTPConnectionPool, response: urllib3.response.HTTPResponse, method: str, sanitized_method: str, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ) -> None: _set_http_host_client(metric_attributes, instance.host, sem_conv_opt_in_mode) _set_http_scheme(metric_attributes, instance.scheme, sem_conv_opt_in_mode) _set_http_method( metric_attributes, method, sanitized_method, sem_conv_opt_in_mode, ) _set_http_net_peer_name_client(metric_attributes, instance.host, sem_conv_opt_in_mode) _set_http_peer_port_client(metric_attributes, instance.port, sem_conv_opt_in_mode) version = response.version if version: http_version = "1.1" if version == 11 else "1.0" _set_http_network_protocol_version(metric_attributes, http_version, sem_conv_opt_in_mode) def _filter_attributes_semconv( metric_attributes, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ): duration_attrs_old = None duration_attrs_new = None if _report_old(sem_conv_opt_in_mode): duration_attrs_old = _filter_semconv_duration_attrs( metric_attributes, _client_duration_attrs_old, _client_duration_attrs_new, _StabilityMode.DEFAULT, ) if _report_new(sem_conv_opt_in_mode): duration_attrs_new = _filter_semconv_duration_attrs( metric_attributes, _client_duration_attrs_old, _client_duration_attrs_new, _StabilityMode.HTTP, ) return (duration_attrs_old, duration_attrs_new) def _record_metrics( metric_attributes: dict, duration_histogram_old: Histogram, duration_histogram_new: Histogram, request_size_histogram_old: Histogram, request_size_histogram_new: Histogram, response_size_histogram_old: Histogram, response_size_histogram_new: Histogram, duration_s: float, request_size: int | None, response_size: int, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ): attrs_old, attrs_new = _filter_attributes_semconv(metric_attributes, sem_conv_opt_in_mode) if duration_histogram_old: # Default behavior is to record the duration in milliseconds duration_histogram_old.record( max(round(duration_s * 1000), 0), attributes=attrs_old, ) if duration_histogram_new: # New semconv record the duration in seconds duration_histogram_new.record( duration_s, attributes=attrs_new, ) if request_size is not None: if request_size_histogram_old: request_size_histogram_old.record(request_size, attributes=attrs_old) if request_size_histogram_new: request_size_histogram_new.record(request_size, attributes=attrs_new) if response_size_histogram_old: response_size_histogram_old.record(response_size, attributes=attrs_old) if response_size_histogram_new: response_size_histogram_new.record(response_size, attributes=attrs_new) def _uninstrument(): unwrap(urllib3.connectionpool.HTTPConnectionPool, "urlopen") package.py000066400000000000000000000002651527070072300447120ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("urllib3 >= 1.0.0, < 3.0.0",) _supports_metrics = True _semconv_status = "migration" version.py000066400000000000000000000001441527070072300450000ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements-0.txt000066400000000000000000000005151527070072300370370ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3asgiref==3.8.1 Deprecated==1.2.14 mocket==3.14.1 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 urllib3==1.26.19 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-urllib3 test-requirements-1.txt000066400000000000000000000005131527070072300370360ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3asgiref==3.8.1 Deprecated==1.2.14 mocket==3.14.1 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 urllib3==2.7.0 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-urllib3 opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3/tests/000077500000000000000000000000001527070072300336615ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300357010ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3/teststest_urllib3_integration.py000066400000000000000000001026401527070072300411750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import typing from unittest import mock import urllib3 import urllib3.exceptions from mocket import Mocket, Mocketizer from mocket.mocks.mockhttp import Entry from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, _StabilityMode, ) from opentelemetry.instrumentation.urllib3 import ( RequestInfo, URLLib3Instrumentor, ) from opentelemetry.instrumentation.utils import ( suppress_http_instrumentation, suppress_instrumentation, ) from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.test.mock_textmap import MockTextMapPropagator from opentelemetry.test.test_base import TestBase from opentelemetry.trace import Span from opentelemetry.util.http import get_excluded_urls # pylint: disable=too-many-public-methods,too-many-lines class TestURLLib3Instrumentor(TestBase): HTTP_URL = "http://mock/status/200" HTTPS_URL = "https://mock/status/200" def setUp(self): super().setUp() test_name = "" if hasattr(self, "_testMethodName"): test_name = self._testMethodName sem_conv_mode = "default" if "new_semconv" in test_name: sem_conv_mode = "http" elif "both_semconv" in test_name: sem_conv_mode = "http/dup" self.env_patch = mock.patch.dict( "os.environ", { "OTEL_PYTHON_URLLIB3_EXCLUDED_URLS": "http://localhost/env_excluded_arg/123,env_excluded_noarg", OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) _OpenTelemetrySemanticConventionStability._initialized = False self.env_patch.start() self.exclude_patch = mock.patch( "opentelemetry.instrumentation.urllib3._excluded_urls_from_env", get_excluded_urls("URLLIB3"), ) self.exclude_patch.start() URLLib3Instrumentor().instrument() self.mocketizer = Mocketizer(strict_mode=True) self.mocketizer.enter() Entry.single_register(Entry.GET, self.HTTP_URL, body="Hello!", match_querystring=False) Entry.single_register(Entry.GET, self.HTTPS_URL, body="Hello!") Entry.single_register(Entry.POST, self.HTTP_URL, body="Hello!") def tearDown(self): super().tearDown() self.env_patch.stop() URLLib3Instrumentor().uninstrument() self.mocketizer.exit() def assert_span(self, exporter=None, num_spans=1): if exporter is None: exporter = self.memory_exporter span_list = exporter.get_finished_spans() self.assertEqual(num_spans, len(span_list)) if num_spans == 0: return None if num_spans == 1: return span_list[0] return span_list def assert_success_span( self, response: urllib3.response.HTTPResponse, url: str, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ): self.assertEqual(b"Hello!", response.data) span = self.assert_span() self.assertIs(trace.SpanKind.CLIENT, span.kind) self.assertEqual("GET", span.name) self.assertEqual(span.status.status_code, trace.status.StatusCode.UNSET) expected_attr_old = { "http.method": "GET", "http.url": url, "http.status_code": 200, } expected_attr_new = { "http.request.method": "GET", "url.full": url, "http.response.status_code": 200, } attributes = { _StabilityMode.DEFAULT: expected_attr_old, _StabilityMode.HTTP: expected_attr_new, _StabilityMode.HTTP_DUP: { **expected_attr_new, **expected_attr_old, }, } self.assertDictEqual(dict(span.attributes), attributes.get(sem_conv_opt_in_mode)) def assert_exception_span( self, url: str, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ): span = self.assert_span() expected_attr_old = { "http.method": "GET", "http.url": url, } expected_attr_new = { "http.request.method": "GET", "url.full": url, # TODO: Add `error.type` attribute when supported } attributes = { _StabilityMode.DEFAULT: expected_attr_old, _StabilityMode.HTTP: expected_attr_new, _StabilityMode.HTTP_DUP: { **expected_attr_new, **expected_attr_old, }, } self.assertDictEqual(dict(span.attributes), attributes.get(sem_conv_opt_in_mode)) self.assertEqual(trace.status.StatusCode.ERROR, span.status.status_code) @staticmethod def perform_request( url: str, headers: typing.Mapping = None, retries: urllib3.Retry = None, method: str = "GET", ) -> urllib3.response.HTTPResponse: if retries is None: retries = urllib3.Retry.from_int(0) pool = urllib3.PoolManager() return pool.request(method, url, headers=headers, retries=retries) def test_basic_http_success(self): response = self.perform_request(self.HTTP_URL) self.assert_success_span( response, self.HTTP_URL, sem_conv_opt_in_mode=_StabilityMode.DEFAULT, ) def test_basic_http_success_new_semconv(self): response = self.perform_request(self.HTTP_URL) self.assert_success_span( response, self.HTTP_URL, sem_conv_opt_in_mode=_StabilityMode.HTTP, ) def test_basic_http_success_both_semconv(self): response = self.perform_request(self.HTTP_URL) self.assert_success_span( response, self.HTTP_URL, sem_conv_opt_in_mode=_StabilityMode.HTTP_DUP, ) def test_basic_http_success_using_connection_pool(self): pool = urllib3.HTTPConnectionPool("mock") response = pool.request("GET", "/status/200") self.assert_success_span(response, self.HTTP_URL) def test_basic_https_success(self): response = self.perform_request(self.HTTPS_URL) self.assert_success_span(response, self.HTTPS_URL) def test_basic_https_success_using_connection_pool(self): pool = urllib3.HTTPSConnectionPool("mock") response = pool.request("GET", "/status/200") self.assert_success_span(response, self.HTTPS_URL) def test_schema_url(self): pool = urllib3.HTTPSConnectionPool("mock") response = pool.request("GET", "/status/200") self.assertEqual(b"Hello!", response.data) span = self.assert_span() self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.11.0", ) def test_schema_url_new_semconv(self): pool = urllib3.HTTPSConnectionPool("mock") response = pool.request("GET", "/status/200") self.assertEqual(b"Hello!", response.data) span = self.assert_span() self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.21.0", ) def test_schema_url_both_semconv(self): pool = urllib3.HTTPSConnectionPool("mock") response = pool.request("GET", "/status/200") self.assertEqual(b"Hello!", response.data) span = self.assert_span() self.assertEqual( span.instrumentation_scope.schema_url, "https://opentelemetry.io/schemas/1.21.0", ) def test_basic_not_found(self): url_404 = "http://mock/status/404" Entry.single_register(Entry.GET, url_404, status=404) response = self.perform_request(url_404) self.assertEqual(404, response.status) span = self.assert_span() self.assertEqual(404, span.attributes.get("http.status_code")) self.assertIs(trace.status.StatusCode.ERROR, span.status.status_code) def test_basic_not_found_new_semconv(self): url_404 = "http://mock/status/404" Entry.single_register(Entry.GET, url_404, status=404) response = self.perform_request(url_404) self.assertEqual(404, response.status) span = self.assert_span() self.assertEqual(404, span.attributes.get("http.response.status_code")) self.assertIs(trace.status.StatusCode.ERROR, span.status.status_code) def test_basic_not_found_both_semconv(self): url_404 = "http://mock/status/404" Entry.single_register(Entry.GET, url_404, status=404) response = self.perform_request(url_404) self.assertEqual(404, response.status) span = self.assert_span() self.assertEqual(404, span.attributes.get("http.response.status_code")) self.assertEqual(404, span.attributes.get("http.status_code")) self.assertIs(trace.status.StatusCode.ERROR, span.status.status_code) @mock.patch( "mocket.mocks.mockhttp.Entry.METHODS", Entry.METHODS + ("NONSTANDARD",), ) def test_nonstandard_http_method(self): Entry.single_register("NONSTANDARD", self.HTTP_URL, body="Hello!", status=405) self.perform_request(self.HTTP_URL, method="NONSTANDARD") span = self.assert_span() self.assertEqual("HTTP", span.name) self.assertEqual(span.attributes.get("http.method"), "_OTHER") self.assertEqual(span.attributes.get("http.status_code"), 405) @mock.patch( "mocket.mocks.mockhttp.Entry.METHODS", Entry.METHODS + ("NONSTANDARD",), ) def test_nonstandard_http_method_new_semconv(self): Entry.single_register("NONSTANDARD", self.HTTP_URL, body="Hello!", status=405) self.perform_request(self.HTTP_URL, method="NONSTANDARD") span = self.assert_span() self.assertEqual("HTTP", span.name) self.assertEqual(span.attributes.get("http.request.method"), "_OTHER") self.assertEqual(span.attributes.get("http.request.method_original"), "NONSTANDARD") self.assertEqual(span.attributes.get("http.response.status_code"), 405) @mock.patch( "mocket.mocks.mockhttp.Entry.METHODS", Entry.METHODS + ("NONSTANDARD",), ) def test_nonstandard_http_method_both_semconv(self): Entry.single_register("NONSTANDARD", self.HTTP_URL, body="Hello!", status=405) self.perform_request(self.HTTP_URL, method="NONSTANDARD") span = self.assert_span() self.assertEqual("HTTP", span.name) self.assertEqual(span.attributes.get("http.method"), "_OTHER") self.assertEqual(span.attributes.get("http.status_code"), 405) self.assertEqual(span.attributes.get("http.request.method"), "_OTHER") self.assertEqual(span.attributes.get("http.request.method_original"), "NONSTANDARD") self.assertEqual(span.attributes.get("http.response.status_code"), 405) def test_basic_http_non_default_port(self): url = "http://mock:666/status/200" Entry.single_register(Entry.GET, url, body="Hello!") response = self.perform_request(url) self.assert_success_span(response, url) def test_basic_http_absolute_url(self): url = "http://mock:666/status/200" Entry.single_register(Entry.GET, url, body="Hello!") pool = urllib3.HTTPConnectionPool("mock", port=666) response = pool.request("GET", url) self.assert_success_span(response, url) def test_url_open_explicit_arg_parameters(self): url = "http://mock:666/status/200" Entry.single_register(Entry.GET, url, body="Hello!") pool = urllib3.HTTPConnectionPool("mock", port=666) response = pool.urlopen(method="GET", url="/status/200") self.assert_success_span(response, url) def test_excluded_urls_explicit(self): url_201 = "http://mock/status/201" Entry.single_register( Entry.GET, url_201, status=201, ) URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument(excluded_urls=".*/201") self.perform_request(self.HTTP_URL) self.perform_request(url_201) self.assert_span(num_spans=1) def test_excluded_urls_from_env(self): url = "http://localhost/env_excluded_arg/123" Entry.single_register( Entry.GET, url, status=200, ) URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument() self.perform_request(self.HTTP_URL) self.perform_request(url) self.assert_span(num_spans=1) def test_uninstrument(self): URLLib3Instrumentor().uninstrument() response = self.perform_request(self.HTTP_URL) self.assertEqual(b"Hello!", response.data) self.assert_span(num_spans=0) # instrument again to avoid warning message on tearDown URLLib3Instrumentor().instrument() def test_suppress_instrumentation(self): suppression_cms = ( suppress_instrumentation, suppress_http_instrumentation, ) for cm in suppression_cms: self.memory_exporter.clear() with self.subTest(cm=cm): with cm(): response = self.perform_request(self.HTTP_URL) self.assertEqual(b"Hello!", response.data) self.assert_span(num_spans=0) def test_context_propagation(self): previous_propagator = get_global_textmap() try: set_global_textmap(MockTextMapPropagator()) response = self.perform_request(self.HTTP_URL) self.assertEqual(b"Hello!", response.data) span = self.assert_span() headers = dict(Mocket.last_request().headers) self.assertIn(MockTextMapPropagator.TRACE_ID_KEY, headers) self.assertEqual( headers[MockTextMapPropagator.TRACE_ID_KEY], str(span.get_span_context().trace_id), ) self.assertIn(MockTextMapPropagator.SPAN_ID_KEY, headers) self.assertEqual( headers[MockTextMapPropagator.SPAN_ID_KEY], str(span.get_span_context().span_id), ) finally: set_global_textmap(previous_propagator) def test_custom_tracer_provider(self): tracer_provider, exporter = self.create_tracer_provider() tracer_provider = mock.Mock(wraps=tracer_provider) URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument(tracer_provider=tracer_provider) response = self.perform_request(self.HTTP_URL) self.assertEqual(b"Hello!", response.data) self.assert_span(exporter=exporter) self.assertEqual(1, tracer_provider.get_tracer.call_count) @mock.patch( "urllib3.connectionpool.HTTPConnectionPool._make_request", side_effect=urllib3.exceptions.ConnectTimeoutError, ) def test_request_exception(self, _): with self.assertRaises(urllib3.exceptions.ConnectTimeoutError): self.perform_request(self.HTTP_URL, retries=urllib3.Retry(connect=False)) self.assert_exception_span(self.HTTP_URL) @mock.patch( "urllib3.connectionpool.HTTPConnectionPool._make_request", side_effect=urllib3.exceptions.ConnectTimeoutError, ) def test_request_exception_new_semconv(self, _): with self.assertRaises(urllib3.exceptions.ConnectTimeoutError): self.perform_request(self.HTTP_URL, retries=urllib3.Retry(connect=False)) self.assert_exception_span(self.HTTP_URL, sem_conv_opt_in_mode=_StabilityMode.HTTP) @mock.patch( "urllib3.connectionpool.HTTPConnectionPool._make_request", side_effect=urllib3.exceptions.ConnectTimeoutError, ) def test_request_exception_both_semconv(self, _): with self.assertRaises(urllib3.exceptions.ConnectTimeoutError): self.perform_request(self.HTTP_URL, retries=urllib3.Retry(connect=False)) self.assert_exception_span(self.HTTP_URL, sem_conv_opt_in_mode=_StabilityMode.HTTP_DUP) @mock.patch( "urllib3.connectionpool.HTTPConnectionPool._make_request", side_effect=urllib3.exceptions.ProtocolError, ) def test_retries_do_not_create_spans(self, _): with self.assertRaises(urllib3.exceptions.MaxRetryError): self.perform_request(self.HTTP_URL, retries=urllib3.Retry(1)) # expect only a single span (retries are ignored) self.assert_exception_span(self.HTTP_URL) def test_url_filter(self): def url_filter(url): return url.split("?")[0] URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument(url_filter=url_filter) response = self.perform_request(self.HTTP_URL + "?e=mcc") self.assert_success_span(response, self.HTTP_URL) def test_credential_removal(self): url = "http://username:password@mock/status/200" response = self.perform_request(url) self.assert_success_span(response, self.HTTP_URL) def test_hooks(self): def request_hook(span, pool, request_info): span.update_name("name set from hook") def response_hook(span, pool, response): span.set_attribute("response_hook_attr", "value") URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument(request_hook=request_hook, response_hook=response_hook) response = self.perform_request(self.HTTP_URL) self.assertEqual(b"Hello!", response.data) span = self.assert_span() self.assertEqual(span.name, "name set from hook") self.assertIn("response_hook_attr", span.attributes) self.assertEqual(span.attributes["response_hook_attr"], "value") def test_request_hook_params(self): def request_hook( span: Span, _pool: urllib3.connectionpool.ConnectionPool, request_info: RequestInfo, ) -> None: span.set_attribute("request_hook_method", request_info.method) span.set_attribute("request_hook_url", request_info.url) span.set_attribute("request_hook_headers", json.dumps(dict(request_info.headers))) span.set_attribute("request_hook_body", request_info.body) URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument( request_hook=request_hook, ) headers = {"header1": "value1", "header2": "value2"} body = "param1=1¶m2=2" pool = urllib3.HTTPConnectionPool("mock") response = pool.request("POST", "/status/200", body=body, headers=headers) self.assertEqual(b"Hello!", response.data) span = self.assert_span() self.assertEqual(span.attributes["request_hook_method"], "POST") self.assertEqual(span.attributes["request_hook_url"], "http://mock/status/200") self.assertIn("request_hook_headers", span.attributes) self.assertEqual(span.attributes["request_hook_headers"], json.dumps(headers)) self.assertIn("request_hook_body", span.attributes) self.assertEqual(span.attributes["request_hook_body"], body) def test_request_positional_body(self): def request_hook( span: Span, _pool: urllib3.connectionpool.ConnectionPool, request_info: RequestInfo, ) -> None: span.set_attribute("request_hook_body", request_info.body) URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument( request_hook=request_hook, ) body = "param1=1¶m2=2" pool = urllib3.HTTPConnectionPool("mock") response = pool.urlopen("POST", "/status/200", body) self.assertEqual(b"Hello!", response.data) span = self.assert_span() self.assertIn("request_hook_body", span.attributes) self.assertEqual(span.attributes["request_hook_body"], body) def test_no_op_tracer_provider(self): URLLib3Instrumentor().uninstrument() tracer_provider = trace.NoOpTracerProvider() URLLib3Instrumentor().instrument(tracer_provider=tracer_provider) response = self.perform_request(self.HTTP_URL) self.assertEqual(b"Hello!", response.data) self.assert_span(num_spans=0) def test_custom_response_headers_captured(self): URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument(captured_response_headers=["X-Custom-Header", "X-Another-Header"]) response_headers = { "X-Custom-Header": "custom-value", "X-Another-Header": "another-value", } url = "http://mock/capture_headers" Entry.single_register( Entry.GET, url, body="Hello!", headers=response_headers, ) self.perform_request(url) span = self.assert_span(num_spans=1) self.assertEqual( span.attributes["http.response.header.x_custom_header"], ("custom-value",), ) self.assertEqual( span.attributes["http.response.header.x_another_header"], ("another-value",), ) self.assertNotIn("http.response.header.x_excluded_header", span.attributes) def test_custom_headers_not_captured_when_not_configured(self): """Test that headers are not captured when env vars are not set.""" URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument() self.perform_request( self.HTTP_URL, headers={"X-Request-Header": "request-value"}, ) span = self.assert_span(num_spans=1) self.assertNotIn("http.request.header.x_request_header", span.attributes) self.assertNotIn("http.response.header.x_response_header", span.attributes) def test_sensitive_headers_sanitized(self): """Test that sensitive header values are redacted.""" URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument( captured_request_headers=["Authorization", "X-Api-Key"], captured_response_headers=["Set-Cookie", "X-Secret"], sensitive_headers=[ "Authorization", "X-Api-Key", "Set-Cookie", "X-Secret", ], ) response_headers = { "Set-Cookie": "session=abc123", "X-Secret": "secret", } url = "http://mock/capture_headers" Entry.single_register( Entry.GET, url, body="Hello!", headers=response_headers, ) self.perform_request( url, headers={ "Authorization": "Bearer secret-token", "X-Api-Key": "secret-key", }, ) span = self.assert_span(num_spans=1) self.assertEqual( span.attributes["http.request.header.authorization"], ("[REDACTED]",), ) self.assertEqual( span.attributes["http.request.header.x_api_key"], ("[REDACTED]",), ) self.assertEqual( span.attributes["http.response.header.set_cookie"], ("[REDACTED]",), ) self.assertEqual( span.attributes["http.response.header.x_secret"], ("[REDACTED]",), ) def test_custom_headers_with_regex(self): """Test that header capture works with regex patterns.""" URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument( captured_request_headers=["X-Custom-Request-.*"], captured_response_headers=["X-Custom-Response-.*"], ) response_headers = { "X-Custom-Response-A": "value-A", "X-Custom-Response-B": "value-B", "X-Other-Response-Header": "other-value", } url = "http://mock/capture_headers" Entry.single_register( Entry.GET, url, body="Hello!", headers=response_headers, ) self.perform_request( url, headers={ "X-Custom-Request-One": "value-one", "X-Custom-Request-Two": "value-two", "X-Other-Request-Header": "other-value", }, ) span = self.assert_span(num_spans=1) self.assertEqual( span.attributes["http.request.header.x_custom_request_one"], ("value-one",), ) self.assertEqual( span.attributes["http.request.header.x_custom_request_two"], ("value-two",), ) self.assertNotIn("http.request.header.x_other_request_header", span.attributes) self.assertEqual( span.attributes["http.response.header.x_custom_response_a"], ("value-A",), ) self.assertEqual( span.attributes["http.response.header.x_custom_response_b"], ("value-B",), ) self.assertNotIn("http.response.header.x_other_response_header", span.attributes) def test_custom_headers_case_insensitive(self): """Test that header capture is case-insensitive.""" URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument( captured_request_headers=["x-request-header"], captured_response_headers=["x-response-header"], ) response_headers = {"X-ReSPoNse-HeaDER": "custom-value"} url = "http://mock/capture_headers" Entry.single_register( Entry.GET, url, body="Hello!", headers=response_headers, ) self.perform_request( url, headers={"X-ReQuESt-HeaDER": "custom-value"}, ) span = self.assert_span(num_spans=1) self.assertEqual( span.attributes["http.request.header.x_request_header"], ("custom-value",), ) self.assertEqual( span.attributes["http.response.header.x_response_header"], ("custom-value",), ) def test_standard_http_headers_captured(self): """Test that standard HTTP headers can be captured.""" URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument( captured_request_headers=["Content-Type", "Accept"], captured_response_headers=["Content-Type", "Server"], ) response_headers = { "Content-Type": "text/plain", "Server": "TestServer/1.0", } url = "http://mock/capture_headers" Entry.single_register( Entry.GET, url, body="Hello!", headers=response_headers, ) self.perform_request( url, headers={ "Content-Type": "application/json", "Accept": "application/json", }, ) span = self.assert_span(num_spans=1) self.assertEqual( span.attributes["http.request.header.content_type"], ("application/json",), ) self.assertEqual( span.attributes["http.request.header.accept"], ("application/json",), ) self.assertEqual( span.attributes["http.response.header.content_type"], ("text/plain",), ) self.assertEqual( span.attributes["http.response.header.server"], ("TestServer/1.0",), ) def test_capture_all_request_headers(self): """Test that all request headers can be captured with .* pattern.""" URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument(captured_request_headers=[".*"]) self.perform_request( self.HTTP_URL, headers={ "X-Header-One": "value1", "X-Header-Two": "value2", "X-Header-Three": "value3", }, ) span = self.assert_span(num_spans=1) self.assertEqual( span.attributes["http.request.header.x_header_one"], ("value1",), ) self.assertEqual( span.attributes["http.request.header.x_header_two"], ("value2",), ) self.assertEqual( span.attributes["http.request.header.x_header_three"], ("value3",), ) def test_capture_all_response_headers(self): """Test that all response headers can be captured with .* pattern.""" URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument(captured_response_headers=[".*"]) response_headers = { "X-Response-One": "value1", "X-Response-Two": "value2", "X-Response-Three": "value3", } url = "http://mock/capture_headers" Entry.single_register( Entry.GET, url, body="Hello!", headers=response_headers, ) self.perform_request(url) span = self.assert_span(num_spans=1) self.assertEqual( span.attributes["http.response.header.x_response_one"], ("value1",), ) self.assertEqual( span.attributes["http.response.header.x_response_two"], ("value2",), ) self.assertEqual( span.attributes["http.response.header.x_response_three"], ("value3",), ) def test_sanitize_with_regex_pattern(self): """Test that sanitization works with regex patterns.""" URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument( captured_request_headers=["X-Test.*"], sensitive_headers=[".*secret.*"], ) self.perform_request( self.HTTP_URL, headers={ "X-Test": "normal-value", "X-Test-Secret": "secret-value", }, ) span = self.assert_span(num_spans=1) self.assertEqual( span.attributes["http.request.header.x_test"], ("normal-value",), ) self.assertEqual( span.attributes["http.request.header.x_test_secret"], ("[REDACTED]",), ) @mock.patch.dict( "os.environ", { "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST": "x-request-one,x-request-two", "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE": "x-response-one", "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS": "x-request-two", }, ) def test_capture_and_sanitize_environment_variables(self): URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument() response_headers = { "X-Response-One": "value1", "X-Response-Two": "value2", } url = "http://mock/capture_headers" Entry.single_register( Entry.GET, url, body="Hello!", headers=response_headers, ) self.perform_request(url, headers={"x-request-one": "one", "x-request-two": "two"}) span = self.assert_span(num_spans=1) self.assertEqual( span.attributes["http.request.header.x_request_one"], ("one",), ) self.assertEqual( span.attributes["http.request.header.x_request_two"], ("[REDACTED]",), ) self.assertEqual( span.attributes["http.response.header.x_response_one"], ("value1",), ) self.assertNotIn( "http.response.header.x_response_two", span.attributes, ) def test_urlopen_positional_headers(self): URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument(captured_request_headers=["X-Test"]) url = "http://mock/status/200" Entry.single_register(Entry.GET, url, body="Hello!") pool = urllib3.HTTPConnectionPool("mock") headers = {"X-Test": "Value"} response = pool.urlopen("GET", "/status/200", None, headers) self.assertEqual(b"Hello!", response.data) span = self.assert_span() self.assertEqual(span.attributes["http.request.header.x_test"], ("Value",)) def test_urlopen_all_positional(self): URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument(captured_request_headers=["X-Test"]) url = "http://mock/status/200" Entry.single_register(Entry.GET, url, body="Hello!") pool = urllib3.HTTPConnectionPool("mock") response = pool.urlopen( "GET", "/status/200", None, # body {"X-Test": "Value"}, # headers None, # retries True, # redirect True, # assert_same_host urllib3.util.Timeout(5), # timeout ) self.assertEqual(b"Hello!", response.data) span = self.assert_span() self.assertEqual(span.attributes["http.request.header.x_test"], ("Value",)) def test_urlopen_mixed_args(self): URLLib3Instrumentor().uninstrument() URLLib3Instrumentor().instrument(captured_request_headers=["X-Test"]) url = "http://mock/status/200" Entry.single_register(Entry.GET, url, body="Hello!") pool = urllib3.HTTPConnectionPool("mock") response = pool.urlopen("GET", "/status/200", headers={"X-Test": "Value"}, body=None) self.assertEqual(b"Hello!", response.data) span = self.assert_span() self.assertEqual(span.attributes["http.request.header.x_test"], ("Value",)) test_urllib3_ip_support.py000066400000000000000000000051551527070072300410610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import urllib3 import urllib3.exceptions from opentelemetry import trace from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor from opentelemetry.test.httptest import HttpTestBase from opentelemetry.test.test_base import TestBase from opentelemetry.util.http.httplib import HttpClientInstrumentor class TestURLLib3InstrumentorWithRealSocket(HttpTestBase, TestBase): def setUp(self): super().setUp() self.assert_ip = self.server.server_address[0] self.http_host = ":".join(map(str, self.server.server_address[:2])) self.http_url_base = "http://" + self.http_host self.http_url = self.http_url_base + "/status/200" HttpClientInstrumentor().instrument() URLLib3Instrumentor().instrument() def tearDown(self): super().tearDown() HttpClientInstrumentor().uninstrument() URLLib3Instrumentor().uninstrument() @staticmethod def perform_request(url: str) -> urllib3.response.HTTPResponse: with urllib3.PoolManager() as pool: resp = pool.request("GET", url) resp.close() return resp def test_basic_http_success(self): response = self.perform_request(self.http_url) self.assert_success_span(response, self.http_url) def test_basic_http_success_using_connection_pool(self): with urllib3.HTTPConnectionPool(self.http_host, timeout=3) as pool: response = pool.request("GET", "/status/200") self.assert_success_span(response, self.http_url) # Test that when re-using an existing connection, everything still works. # Especially relevant for IP capturing. response = pool.request("GET", "/status/200") self.assert_success_span(response, self.http_url) def assert_span(self, num_spans=1): span_list = self.memory_exporter.get_finished_spans() self.assertEqual(num_spans, len(span_list)) if num_spans == 0: return None self.memory_exporter.clear() if num_spans == 1: return span_list[0] return span_list def assert_success_span(self, response: urllib3.response.HTTPResponse, url: str): self.assertEqual(b"Hello!", response.data) span = self.assert_span() self.assertIs(trace.SpanKind.CLIENT, span.kind) self.assertEqual("GET", span.name) attributes = { "http.status_code": 200, "net.peer.ip": self.assert_ip, } self.assertGreaterEqual(span.attributes.items(), attributes.items()) test_urllib3_metrics.py000066400000000000000000000507741527070072300403320ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-urllib3/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import io from timeit import default_timer from unittest import mock import urllib3 import urllib3.exceptions from mocket import Mocketizer from mocket.mocks.mockhttp import Entry from urllib3 import encode_multipart_formdata from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor from opentelemetry.test.httptest import HttpTestBase from opentelemetry.test.test_base import TestBase SCOPE = "opentelemetry.instrumentation.urllib3" class TestURLLib3InstrumentorMetric(HttpTestBase, TestBase): HTTP_URL = "http://mock/status/200" def setUp(self): super().setUp() test_name = "" if hasattr(self, "_testMethodName"): test_name = self._testMethodName sem_conv_mode = "default" if "new_semconv" in test_name: sem_conv_mode = "http" elif "both_semconv" in test_name: sem_conv_mode = "http/dup" self.env_patch = mock.patch.dict( "os.environ", { OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) _OpenTelemetrySemanticConventionStability._initialized = False self.env_patch.start() URLLib3Instrumentor().instrument() self.mocketizer = Mocketizer(strict_mode=True) self.mocketizer.enter() Entry.single_register(Entry.GET, self.HTTP_URL, body="Hello!") Entry.single_register(Entry.POST, self.HTTP_URL, body="Hello!") self.pool = urllib3.PoolManager() def tearDown(self): super().tearDown() self.env_patch.stop() self.pool.clear() URLLib3Instrumentor().uninstrument() self.mocketizer.exit() def test_basic_metrics(self): start_time = default_timer() response = self.pool.request("GET", self.HTTP_URL) duration_ms = max(round((default_timer() - start_time) * 1000), 0) metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 3) ( client_duration, client_request_size, client_response_size, ) = metrics attrs_old = { "http.status_code": 200, "http.host": "mock", "net.peer.port": 80, "net.peer.name": "mock", "http.method": "GET", "http.flavor": "1.1", "http.scheme": "http", } self.assertEqual(client_duration.name, "http.client.duration") self.assert_metric_expected( client_duration, [ self.create_histogram_data_point( count=1, sum_data_point=duration_ms, max_data_point=duration_ms, min_data_point=duration_ms, attributes=attrs_old, ) ], est_value_delta=40, ) self.assertEqual(client_request_size.name, "http.client.request.size") self.assert_metric_expected( client_request_size, [ self.create_histogram_data_point( count=1, sum_data_point=0, max_data_point=0, min_data_point=0, attributes=attrs_old, ) ], ) expected_size = len(response.data) self.assertEqual(client_response_size.name, "http.client.response.size") self.assert_metric_expected( client_response_size, [ self.create_histogram_data_point( count=1, sum_data_point=expected_size, max_data_point=expected_size, min_data_point=expected_size, attributes=attrs_old, ) ], ) def test_basic_metrics_new_semconv(self): start_time = default_timer() response = self.pool.request("GET", self.HTTP_URL) duration_s = max(default_timer() - start_time, 0) metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 3) ( client_request_size, client_duration, client_response_size, ) = metrics attrs_new = { "network.protocol.version": "1.1", "server.address": "mock", "server.port": 80, "http.request.method": "GET", "http.response.status_code": 200, # TODO: add URL_SCHEME to tests when supported in the implementation } self.assertEqual(client_duration.name, "http.client.request.duration") self.assert_metric_expected( client_duration, [ self.create_histogram_data_point( count=1, sum_data_point=duration_s, max_data_point=duration_s, min_data_point=duration_s, attributes=attrs_new, explicit_bounds=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) ], est_value_delta=40 / 1000, ) self.assertEqual(client_request_size.name, "http.client.request.body.size") self.assert_metric_expected( client_request_size, [ self.create_histogram_data_point( count=1, sum_data_point=0, max_data_point=0, min_data_point=0, attributes=attrs_new, ) ], ) expected_size = len(response.data) self.assertEqual(client_response_size.name, "http.client.response.body.size") self.assert_metric_expected( client_response_size, [ self.create_histogram_data_point( count=1, sum_data_point=expected_size, max_data_point=expected_size, min_data_point=expected_size, attributes=attrs_new, ) ], ) def test_basic_metrics_both_semconv(self): start_time = default_timer() response = self.pool.request("GET", self.HTTP_URL) duration_s = max(default_timer() - start_time, 0) duration = max(round(duration_s * 1000), 0) expected_size = len(response.data) metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 6) ( client_duration, client_request_body_size, client_request_duration, client_request_size, client_response_body_size, client_response_size, ) = metrics[:6] attrs_new = { "network.protocol.version": "1.1", "server.address": "mock", "server.port": 80, "http.request.method": "GET", "http.response.status_code": 200, # TODO: add URL_SCHEME to tests when supported in the implementation } attrs_old = { "http.status_code": 200, "http.host": "mock", "net.peer.port": 80, "net.peer.name": "mock", "http.method": "GET", "http.flavor": "1.1", "http.scheme": "http", } # assert new semconv metrics self.assertEqual(client_request_duration.name, "http.client.request.duration") self.assert_metric_expected( client_request_duration, [ self.create_histogram_data_point( count=1, sum_data_point=duration_s, max_data_point=duration_s, min_data_point=duration_s, attributes=attrs_new, explicit_bounds=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) ], est_value_delta=40 / 1000, ) self.assertEqual(client_request_body_size.name, "http.client.request.body.size") self.assert_metric_expected( client_request_body_size, [ self.create_histogram_data_point( count=1, sum_data_point=0, max_data_point=0, min_data_point=0, attributes=attrs_new, ) ], ) self.assertEqual(client_response_body_size.name, "http.client.response.body.size") self.assert_metric_expected( client_response_body_size, [ self.create_histogram_data_point( count=1, sum_data_point=expected_size, max_data_point=expected_size, min_data_point=expected_size, attributes=attrs_new, ) ], ) # assert old semconv metrics self.assertEqual(client_duration.name, "http.client.duration") self.assert_metric_expected( client_duration, [ self.create_histogram_data_point( count=1, sum_data_point=duration, max_data_point=duration, min_data_point=duration, attributes=attrs_old, ) ], est_value_delta=40, ) self.assertEqual(client_request_size.name, "http.client.request.size") self.assert_metric_expected( client_request_size, [ self.create_histogram_data_point( count=1, sum_data_point=0, max_data_point=0, min_data_point=0, attributes=attrs_old, ) ], ) self.assertEqual(client_response_size.name, "http.client.response.size") self.assert_metric_expected( client_response_size, [ self.create_histogram_data_point( count=1, sum_data_point=expected_size, max_data_point=expected_size, min_data_point=expected_size, attributes=attrs_old, ) ], ) @mock.patch( "mocket.mocks.mockhttp.Entry.METHODS", Entry.METHODS + ("NONSTANDARD",), ) def test_basic_metrics_nonstandard_http_method(self): Entry.single_register("NONSTANDARD", self.HTTP_URL, body="", status=405) start_time = default_timer() response = self.pool.request("NONSTANDARD", self.HTTP_URL) duration_ms = max(round((default_timer() - start_time) * 1000), 0) metrics = self.get_sorted_metrics(SCOPE) ( client_duration, client_request_size, client_response_size, ) = metrics attrs_old = { "http.status_code": 405, "http.host": "mock", "net.peer.port": 80, "net.peer.name": "mock", "http.method": "_OTHER", "http.flavor": "1.1", "http.scheme": "http", } self.assertEqual(client_duration.name, "http.client.duration") self.assert_metric_expected( client_duration, [ self.create_histogram_data_point( count=1, sum_data_point=duration_ms, max_data_point=duration_ms, min_data_point=duration_ms, attributes=attrs_old, ) ], est_value_delta=40, ) self.assertEqual(client_request_size.name, "http.client.request.size") self.assert_metric_expected( client_request_size, [ self.create_histogram_data_point( count=1, sum_data_point=0, max_data_point=0, min_data_point=0, attributes=attrs_old, ) ], ) expected_size = len(response.data) self.assertEqual(client_response_size.name, "http.client.response.size") self.assert_metric_expected( client_response_size, [ self.create_histogram_data_point( count=1, sum_data_point=expected_size, max_data_point=expected_size, min_data_point=expected_size, attributes=attrs_old, ) ], ) @mock.patch( "mocket.mocks.mockhttp.Entry.METHODS", Entry.METHODS + ("NONSTANDARD",), ) def test_basic_metrics_nonstandard_http_method_new_semconv(self): Entry.single_register("NONSTANDARD", self.HTTP_URL, body="", status=405) start_time = default_timer() response = self.pool.request("NONSTANDARD", self.HTTP_URL) duration_s = max(default_timer() - start_time, 0) metrics = self.get_sorted_metrics(SCOPE) ( client_request_size, client_duration, client_response_size, ) = metrics attrs_new = { "network.protocol.version": "1.1", "server.address": "mock", "server.port": 80, "http.request.method": "_OTHER", "http.response.status_code": 405, "error.type": "405", # TODO: add URL_SCHEME to tests when supported in the implementation } self.assertEqual(client_duration.name, "http.client.request.duration") self.assert_metric_expected( client_duration, [ self.create_histogram_data_point( count=1, sum_data_point=duration_s, max_data_point=duration_s, min_data_point=duration_s, attributes=attrs_new, explicit_bounds=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) ], est_value_delta=40 / 1000, ) self.assertEqual(client_request_size.name, "http.client.request.body.size") self.assert_metric_expected( client_request_size, [ self.create_histogram_data_point( count=1, sum_data_point=0, max_data_point=0, min_data_point=0, attributes=attrs_new, ) ], ) expected_size = len(response.data) self.assertEqual(client_response_size.name, "http.client.response.body.size") self.assert_metric_expected( client_response_size, [ self.create_histogram_data_point( count=1, sum_data_point=expected_size, max_data_point=expected_size, min_data_point=expected_size, attributes=attrs_new, ) ], ) def test_str_request_body_size_metrics(self): self.pool.request("POST", self.HTTP_URL, body="foobar") metrics = self.get_sorted_metrics(SCOPE) (_, client_request_size, _) = metrics self.assertEqual(client_request_size.name, "http.client.request.size") self.assert_metric_expected( client_request_size, [ self.create_histogram_data_point( count=1, sum_data_point=6, max_data_point=6, min_data_point=6, attributes={ "http.flavor": "1.1", "http.host": "mock", "http.method": "POST", "http.scheme": "http", "http.status_code": 200, "net.peer.name": "mock", "net.peer.port": 80, }, ) ], ) def test_schema_url(self): self.pool.request("POST", self.HTTP_URL, body="foobar") resource_metrics = self.memory_metrics_reader.get_metrics_data().resource_metrics for metrics in resource_metrics: scope_metrics_list = [sm for sm in metrics.scope_metrics if sm.scope.name == SCOPE] for scope_metrics in scope_metrics_list: self.assertEqual( scope_metrics.scope.schema_url, "https://opentelemetry.io/schemas/1.11.0", ) def test_bytes_request_body_size_metrics(self): self.pool.request("POST", self.HTTP_URL, body=b"foobar") metrics = self.get_sorted_metrics(SCOPE) (_, client_request_size, _) = metrics self.assertEqual(client_request_size.name, "http.client.request.size") self.assert_metric_expected( client_request_size, [ self.create_histogram_data_point( count=1, sum_data_point=6, max_data_point=6, min_data_point=6, attributes={ "http.flavor": "1.1", "http.host": "mock", "http.method": "POST", "http.scheme": "http", "http.status_code": 200, "net.peer.name": "mock", "net.peer.port": 80, }, ) ], ) def test_fields_request_body_size_metrics(self): self.pool.request("POST", self.HTTP_URL, fields={"foo": "bar"}) metrics = self.get_sorted_metrics(SCOPE) (_, client_request_size, _) = metrics self.assertEqual(client_request_size.name, "http.client.request.size") expected_value = len(encode_multipart_formdata({"foo": "bar"})[0]) self.assert_metric_expected( client_request_size, [ self.create_histogram_data_point( count=1, sum_data_point=expected_value, max_data_point=expected_value, min_data_point=expected_value, attributes={ "http.flavor": "1.1", "http.host": "mock", "http.method": "POST", "http.scheme": "http", "http.status_code": 200, "net.peer.name": "mock", "net.peer.port": 80, }, ) ], ) def test_bytesio_request_body_size_metrics(self): self.pool.request("POST", self.HTTP_URL, body=io.BytesIO(b"foobar")) metrics = self.get_sorted_metrics(SCOPE) (_, client_request_size, _) = metrics self.assertEqual(client_request_size.name, "http.client.request.size") self.assert_metric_expected( client_request_size, [ self.create_histogram_data_point( count=1, sum_data_point=6, max_data_point=6, min_data_point=6, attributes={ "http.flavor": "1.1", "http.host": "mock", "http.method": "POST", "http.scheme": "http", "http.status_code": 200, "net.peer.name": "mock", "net.peer.port": 80, }, ) ], ) def test_generator_request_body_size_metrics(self): self.pool.request("POST", self.HTTP_URL, body=(b for b in (b"foo", b"bar"))) metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 2) self.assertNotIn("http.client.request.size", [m.name for m in metrics]) def test_metric_uninstrument(self): self.pool.request("GET", self.HTTP_URL) URLLib3Instrumentor().uninstrument() self.pool.request("GET", self.HTTP_URL) metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 3) for metric in metrics: for point in list(metric.data.data_points): self.assertEqual(point.count, 1) # instrument again to avoid warning message on tearDown URLLib3Instrumentor().instrument() opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-valkey-py/000077500000000000000000000000001527070072300330645ustar00rootroot00000000000000CHANGELOG.md000066400000000000000000000010731527070072300346170ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-valkey-py# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Version 0.1b0 (2026-09-11) No significant changes. pyproject.toml000066400000000000000000000042621527070072300357250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-valkey-py[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-valkey-py" dynamic = ["version"] description = "OpenTelemetry Valkey instrumentation" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation ~= 0.66b0.dev", "opentelemetry-semantic-conventions ~= 0.66b0.dev", "wrapt >= 1.0.0, < 3.0.0", ] [project.optional-dependencies] instruments = [ "valkey ~= 6.0", ] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-valkey-py" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/valkey/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] [tool.towncrier] directory = ".changelog" filename = "CHANGELOG.md" start_string = "\n" template = "../../scripts/changelog_template.j2" issue_format = "[#{issue}](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/{issue})" wrap = true issue_pattern = "^(\\d+)" [[tool.towncrier.type]] directory = "added" name = "Added" showcontent = true [[tool.towncrier.type]] directory = "changed" name = "Changed" showcontent = true [[tool.towncrier.type]] directory = "deprecated" name = "Deprecated" showcontent = true [[tool.towncrier.type]] directory = "removed" name = "Removed" showcontent = true [[tool.towncrier.type]] directory = "fixed" name = "Fixed" showcontent = true opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-valkey-py/src/000077500000000000000000000000001527070072300336535ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300364705ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-valkey-py/srcinstrumentation/000077500000000000000000000000001527070072300417335ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-valkey-py/src/opentelemetryvalkey/000077500000000000000000000000001527070072300432265ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-valkey-py/src/opentelemetry/instrumentation__init__.py000066400000000000000000000001141527070072300453330ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-valkey-py/src/opentelemetry/instrumentation/valkey# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 package.py000066400000000000000000000002521527070072300451720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-valkey-py/src/opentelemetry/instrumentation/valkey# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _instruments = ("valkey ~= 6.0",) _supports_metrics = True _semconv_status = "development" version.py000066400000000000000000000001471527070072300452670ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-valkey-py/src/opentelemetry/instrumentation/valkey# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.1b0.dev" opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-wsgi/000077500000000000000000000000001527070072300321145ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-wsgi/LICENSE000066400000000000000000000261351527070072300331300ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-wsgi/README.rst000066400000000000000000000015071527070072300336060ustar00rootroot00000000000000OpenTelemetry WSGI Middleware ============================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-wsgi.svg :target: https://pypi.org/project/opentelemetry-instrumentation-wsgi/ This library provides a WSGI middleware that can be used on any WSGI framework (such as Django / Flask) to track requests timing through OpenTelemetry. Installation ------------ :: pip install opentelemetry-instrumentation-wsgi References ---------- * `OpenTelemetry WSGI Middleware `_ * `OpenTelemetry Project `_ * `WSGI `_ * `OpenTelemetry Python Examples `_ pyproject.toml000066400000000000000000000027451527070072300347610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-wsgi[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation-wsgi" dynamic = ["version"] description = "WSGI Middleware for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-semantic-conventions == 0.66b2", "opentelemetry-util-http == 0.66b2", ] [project.optional-dependencies] instruments = [] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-wsgi" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/wsgi/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-wsgi/src/000077500000000000000000000000001527070072300327035ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300355205ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-wsgi/srcinstrumentation/000077500000000000000000000000001527070072300407635ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetrywsgi/000077500000000000000000000000001527070072300417345ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation__init__.py000066400000000000000000000646601527070072300440610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This library provides a WSGI middleware that can be used on any WSGI framework (such as Django / Flask / Web.py) to track requests timing through OpenTelemetry. Usage (Flask) ------------- .. code-block:: python from flask import Flask from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware app = Flask(__name__) app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) @app.route("/") def hello(): return "Hello!" if __name__ == "__main__": app.run(debug=True) Usage (Django) -------------- Modify the application's ``wsgi.py`` file as shown below. .. code-block:: python import os from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "application.settings") application = get_wsgi_application() application = OpenTelemetryMiddleware(application) Usage (Web.py) -------------- .. code-block:: python import web from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware from cheroot import wsgi urls = ("/", "index") class index: def GET(self): return "Hello, world!" if __name__ == "__main__": app = web.application(urls, globals()) func = app.wsgifunc() func = OpenTelemetryMiddleware(func) server = wsgi.WSGIServer(("localhost", 5100), func, server_name="localhost") server.start() Configuration ------------- Request/Response hooks ********************** This instrumentation supports request and response hooks. These are functions that get called right after a span is created for a request and right before the span is finished for the response. - The client request hook is called with the internal span and an instance of WSGIEnvironment when the method ``receive`` is called. - The client response hook is called with the internal span, the status of the response and a list of key-value (tuples) representing the response headers returned from the response when the method ``send`` is called. For example, .. code-block:: python from opentelemetry.trace import Span from wsgiref.types import WSGIEnvironment, StartResponse from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware def app(environ: WSGIEnvironment, start_response: StartResponse): start_response( "200 OK", [("Content-Type", "text/plain"), ("Content-Length", "13")], ) return [b"Hello, World!"] def request_hook(span: Span, environ: WSGIEnvironment): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_request_hook", "some-value") def response_hook( span: Span, environ: WSGIEnvironment, status: str, response_headers: list[tuple[str, str]], ): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_response_hook", "some-value") OpenTelemetryMiddleware(app, request_hook=request_hook, response_hook=response_hook) Capture HTTP request and response headers ***************************************** You can configure the agent to capture specified HTTP headers as span attributes, according to the `semantic conventions `_. Request headers *************** To capture HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. Request header names in WSGI are case-insensitive and ``-`` characters are replaced by ``_``. So, giving the header name as ``CUStom_Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" Would match all request headers that start with ``Accept`` and ``X-``. To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*" The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a single item list containing all the header values. For example: ``http.request.header.custom_request_header = [","]`` Response headers **************** To capture HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. Response header names in WSGI are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment variable will capture the header named ``custom-header``. Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" Would match all response headers that start with ``Content`` and ``X-``. To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``. :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*" The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a single item list containing all the header values. For example: ``http.response.header.custom_response_header = [","]`` Sanitizing headers ****************** In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be matched in a case-insensitive manner. For example, :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. Note: The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. Sanitizing methods ****************** In order to prevent unbound cardinality for HTTP methods by default nonstandard ones are labeled as ``NONSTANDARD``. To record all of the names set the environment variable ``OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS`` to a value that evaluates to true, e.g. ``1``. API --- """ from __future__ import annotations import functools import wsgiref.util as wsgiref_util from collections.abc import Callable, Iterable from timeit import default_timer from typing import TYPE_CHECKING, Any, TypeVar, cast from opentelemetry import context, trace from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, _filter_semconv_active_request_count_attr, _filter_semconv_duration_attrs, _get_schema_url, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _report_new, _report_old, _server_active_requests_count_attrs_new, _server_active_requests_count_attrs_old, _server_duration_attrs_new, _server_duration_attrs_old, _set_http_flavor_version, _set_http_method, _set_http_net_host, _set_http_net_host_port, _set_http_net_peer_name_server, _set_http_peer_ip_server, _set_http_peer_port_server, _set_http_scheme, _set_http_target, _set_http_user_agent, _set_status, _StabilityMode, ) from opentelemetry.instrumentation.utils import _start_internal_or_server_span from opentelemetry.instrumentation.wsgi.version import __version__ from opentelemetry.metrics import MeterProvider, get_meter from opentelemetry.propagators.textmap import Getter from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_HOST, HTTP_SERVER_NAME, HTTP_URL, ) from opentelemetry.semconv._incubating.attributes.user_agent_attributes import ( USER_AGENT_SYNTHETIC_TYPE, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.semconv.metrics.http_metrics import ( HTTP_SERVER_REQUEST_DURATION, ) from opentelemetry.trace import TracerProvider from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, SanitizeValue, detect_synthetic_user_agent, get_custom_headers, normalise_request_header_name, normalise_response_header_name, normalize_user_agent, redact_url, sanitize_method, ) if TYPE_CHECKING: from wsgiref.types import StartResponse, WSGIApplication, WSGIEnvironment T = TypeVar("T") RequestHook = Callable[[trace.Span, "WSGIEnvironment"], None] ResponseHook = Callable[[trace.Span, "WSGIEnvironment", str, "list[tuple[str, str]]"], None] _HTTP_VERSION_PREFIX = "HTTP/" _CARRIER_KEY_PREFIX = "HTTP_" _CARRIER_KEY_PREFIX_LEN = len(_CARRIER_KEY_PREFIX) class WSGIGetter(Getter[dict[str, Any]]): def get(self, carrier: dict[str, Any], key: str) -> list[str] | None: """Getter implementation to retrieve a HTTP header value from the PEP3333-conforming WSGI environ Args: carrier: WSGI environ object key: header name in environ object Returns: A list with a single string with the header value if it exists, else None. """ environ_key = "HTTP_" + key.upper().replace("-", "_") value = carrier.get(environ_key) if value is not None: return [value] return None def keys(self, carrier: dict[str, Any]): return [ key[_CARRIER_KEY_PREFIX_LEN:].lower().replace("_", "-") for key in carrier if key.startswith(_CARRIER_KEY_PREFIX) ] wsgi_getter = WSGIGetter() # pylint: disable=too-many-branches def collect_request_attributes( environ: WSGIEnvironment, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ): """Collects HTTP request attributes from the PEP3333-conforming WSGI environ and returns a dictionary to be used as span creation attributes. """ result: dict[str, str | None] = {} _set_http_method( result, environ.get("REQUEST_METHOD", ""), sanitize_method(cast(str, environ.get("REQUEST_METHOD", ""))), sem_conv_opt_in_mode, ) # old semconv v1.12.0 server_name = environ.get("SERVER_NAME") if _report_old(sem_conv_opt_in_mode): result[HTTP_SERVER_NAME] = server_name _set_http_scheme( result, environ.get("wsgi.url_scheme"), sem_conv_opt_in_mode, ) host = environ.get("HTTP_HOST") host_port = environ.get("SERVER_PORT") if host: _set_http_net_host(result, host, sem_conv_opt_in_mode) # old semconv v1.12.0 if _report_old(sem_conv_opt_in_mode): result[HTTP_HOST] = host if host_port: _set_http_net_host_port( result, int(host_port), sem_conv_opt_in_mode, ) target = environ.get("RAW_URI") if target is None: # Note: `"" or None is None` target = environ.get("REQUEST_URI") if target: path = environ.get("PATH_INFO") query = environ.get("QUERY_STRING") _set_http_target(result, target, path, query, sem_conv_opt_in_mode) else: # old semconv v1.20.0 if _report_old(sem_conv_opt_in_mode): result[HTTP_URL] = redact_url(wsgiref_util.request_uri(environ)) remote_addr = environ.get("REMOTE_ADDR") if remote_addr: _set_http_peer_ip_server(result, remote_addr, sem_conv_opt_in_mode) peer_port = environ.get("REMOTE_PORT") if peer_port: _set_http_peer_port_server(result, peer_port, sem_conv_opt_in_mode) remote_host = environ.get("REMOTE_HOST") if remote_host and remote_host != remote_addr: _set_http_net_peer_name_server(result, remote_host, sem_conv_opt_in_mode) _apply_user_agent_attributes(result, environ, sem_conv_opt_in_mode) flavor = environ.get("SERVER_PROTOCOL", "") if flavor.upper().startswith(_HTTP_VERSION_PREFIX): flavor = flavor[len(_HTTP_VERSION_PREFIX) :] if flavor: _set_http_flavor_version(result, flavor, sem_conv_opt_in_mode) return result def _apply_user_agent_attributes( result: dict[str, str | None], environ: WSGIEnvironment, sem_conv_opt_in_mode: _StabilityMode, ): user_agent_raw = environ.get("HTTP_USER_AGENT") if not user_agent_raw: return user_agent = normalize_user_agent(user_agent_raw) if not user_agent: return _set_http_user_agent(result, user_agent, sem_conv_opt_in_mode) synthetic_type = detect_synthetic_user_agent(user_agent) if synthetic_type: result[USER_AGENT_SYNTHETIC_TYPE] = synthetic_type def collect_custom_request_headers_attributes(environ: WSGIEnvironment): """Returns custom HTTP request headers which are configured by the user from the PEP3333-conforming WSGI environ to be used as span creation attributes as described in the semantic conventions https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#http-server-span. See also https://peps.python.org/pep-3333/ """ sanitize = SanitizeValue(get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS)) headers = { key[_CARRIER_KEY_PREFIX_LEN:].replace("_", "-"): val for key, val in environ.items() if key.startswith(_CARRIER_KEY_PREFIX) } return sanitize.sanitize_header_values( headers, get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST), normalise_request_header_name, ) def collect_custom_response_headers_attributes( response_headers: list[tuple[str, str]], ): """Returns custom HTTP response headers which are configured by the user from the PEP3333-conforming WSGI environ as described in the semantic conventions https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#http-server-span """ sanitize = SanitizeValue(get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS)) response_headers_dict: dict[str, str] = {} if response_headers: for key, val in response_headers: key = key.lower() if key in response_headers_dict: response_headers_dict[key] += "," + val else: response_headers_dict[key] = val return sanitize.sanitize_header_values( response_headers_dict, get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE), normalise_response_header_name, ) # TODO: Used only on the `opentelemetry-instrumentation-pyramid` package - It can be moved there. def _parse_status_code(resp_status: str) -> int | None: status_code, _ = resp_status.split(" ", 1) try: return int(status_code) except ValueError: return None def _parse_active_request_count_attrs(req_attrs, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT): return _filter_semconv_active_request_count_attr( req_attrs, _server_active_requests_count_attrs_old, _server_active_requests_count_attrs_new, sem_conv_opt_in_mode, ) def _parse_duration_attrs( req_attrs: dict[str, str | None], sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ): return _filter_semconv_duration_attrs( req_attrs, _server_duration_attrs_old, _server_duration_attrs_new, sem_conv_opt_in_mode, ) def add_response_attributes( span: trace.Span, start_response_status: str, response_headers: list[tuple[str, str]], duration_attrs: dict[str, str | None] | None = None, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ): # pylint: disable=unused-argument """Adds HTTP response attributes to span using the arguments passed to a PEP3333-conforming start_response callable. """ status_code_str, _ = start_response_status.split(" ", 1) try: status_code = int(status_code_str) except ValueError: status_code = -1 if duration_attrs is None: duration_attrs = {} _set_status( span, duration_attrs, status_code, status_code_str, server_span=True, sem_conv_opt_in_mode=sem_conv_opt_in_mode, ) def get_default_span_name(environ: WSGIEnvironment) -> str: """ Default span name is the HTTP method and URL path, or just the method. https://github.com/open-telemetry/opentelemetry-specification/pull/3165 https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#name Args: environ: The WSGI environ object. Returns: The span name. """ method = sanitize_method(cast(str, environ.get("REQUEST_METHOD", "")).strip()) if method == "_OTHER": return "HTTP" path = cast(str, environ.get("PATH_INFO", "")).strip() if method and path: return f"{method} {path}" return method class OpenTelemetryMiddleware: """The WSGI application middleware. This class is a PEP 3333 conforming WSGI middleware that starts and annotates spans for any requests it is invoked with. Args: wsgi: The WSGI application callable to forward requests to. request_hook: Optional callback which is called with the server span and WSGI environ object for every incoming request. response_hook: Optional callback which is called with the server span, WSGI environ, status_code and response_headers for every incoming request. tracer_provider: Optional tracer provider to use. If omitted the current globally configured one is used. meter_provider: Optional meter provider to use. If omitted the current globally configured one is used. """ def __init__( self, wsgi: WSGIApplication, request_hook: RequestHook | None = None, response_hook: ResponseHook | None = None, tracer_provider: TracerProvider | None = None, meter_provider: MeterProvider | None = None, ): # initialize semantic conventions opt-in if needed _OpenTelemetrySemanticConventionStability._initialize() sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP, ) self.wsgi = wsgi self.tracer = trace.get_tracer( __name__, __version__, tracer_provider, schema_url=_get_schema_url(sem_conv_opt_in_mode), ) self.meter = get_meter( __name__, __version__, meter_provider, schema_url=_get_schema_url(sem_conv_opt_in_mode), ) self.duration_histogram_old = None if _report_old(sem_conv_opt_in_mode): self.duration_histogram_old = self.meter.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", description="Measures the duration of inbound HTTP requests.", ) self.duration_histogram_new = None if _report_new(sem_conv_opt_in_mode): self.duration_histogram_new = self.meter.create_histogram( name=HTTP_SERVER_REQUEST_DURATION, unit="s", description="Duration of HTTP server requests.", explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) # We don't need a separate active request counter for old/new semantic conventions # because the new attributes are a subset of the old attributes self.active_requests_counter = self.meter.create_up_down_counter( name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, unit="{request}", description="Number of active HTTP server requests.", ) self.request_hook = request_hook self.response_hook = response_hook self._sem_conv_opt_in_mode = sem_conv_opt_in_mode @staticmethod def _create_start_response( span: trace.Span, start_response: StartResponse, response_hook: Callable[[str, list[tuple[str, str]]], None] | None, duration_attrs: dict[str, str | None], sem_conv_opt_in_mode: _StabilityMode, ): @functools.wraps(start_response) def _start_response( status: str, response_headers: list[tuple[str, str]], *args: Any, **kwargs: Any, ): add_response_attributes( span, status, response_headers, duration_attrs, sem_conv_opt_in_mode, ) if span.is_recording() and span.kind == trace.SpanKind.SERVER: custom_attributes = collect_custom_response_headers_attributes(response_headers) if len(custom_attributes) > 0: span.set_attributes(custom_attributes) if response_hook: response_hook(status, response_headers) return start_response(status, response_headers, *args, **kwargs) return _start_response # pylint: disable=too-many-branches # pylint: disable=too-many-locals def __call__(self, environ: WSGIEnvironment, start_response: StartResponse): """The WSGI application Args: environ: A WSGI environment. start_response: The WSGI start_response callable. """ req_attrs = collect_request_attributes(environ, self._sem_conv_opt_in_mode) active_requests_count_attrs = _parse_active_request_count_attrs( req_attrs, self._sem_conv_opt_in_mode, ) span, token = _start_internal_or_server_span( tracer=self.tracer, span_name=get_default_span_name(environ), start_time=None, context_carrier=environ, context_getter=wsgi_getter, attributes=req_attrs, ) if span.is_recording() and span.kind == trace.SpanKind.SERVER: custom_attributes = collect_custom_request_headers_attributes(environ) if len(custom_attributes) > 0: span.set_attributes(custom_attributes) if self.request_hook: self.request_hook(span, environ) response_hook = self.response_hook if response_hook: response_hook = functools.partial(response_hook, span, environ) start = default_timer() self.active_requests_counter.add(1, active_requests_count_attrs) try: with trace.use_span(span): start_response = self._create_start_response( span, start_response, response_hook, req_attrs, self._sem_conv_opt_in_mode, ) iterable = self.wsgi(environ, start_response) return _end_span_after_iterating(iterable, span, token) except Exception as ex: if _report_new(self._sem_conv_opt_in_mode): req_attrs[ERROR_TYPE] = type(ex).__qualname__ if span.is_recording(): span.set_attribute(ERROR_TYPE, type(ex).__qualname__) span.set_status(Status(StatusCode.ERROR, str(ex))) span.end() if token is not None: context.detach(token) raise finally: duration_s = default_timer() - start active_metric_ctx = trace.set_span_in_context(span) if self.duration_histogram_old: duration_attrs_old = _parse_duration_attrs(req_attrs, _StabilityMode.DEFAULT) self.duration_histogram_old.record( max(round(duration_s * 1000), 0), duration_attrs_old, context=active_metric_ctx, ) if self.duration_histogram_new: duration_attrs_new = _parse_duration_attrs(req_attrs, _StabilityMode.HTTP) self.duration_histogram_new.record( max(duration_s, 0), duration_attrs_new, context=active_metric_ctx, ) self.active_requests_counter.add(-1, active_requests_count_attrs) # Put this in a subfunction to not delay the call to the wrapped # WSGI application (instrumentation should change the application # behavior as little as possible). def _end_span_after_iterating(iterable: Iterable[T], span: trace.Span, token: object) -> Iterable[T]: try: with trace.use_span(span): yield from iterable finally: close = getattr(iterable, "close", None) if close: close() span.end() if token is not None: context.detach(token) # TODO: inherit from opentelemetry.instrumentation.propagators.Setter class ResponsePropagationSetter: def set(self, carrier: list[tuple[str, T]], key: str, value: T): # pylint: disable=no-self-use carrier.append((key, value)) default_response_propagation_setter = ResponsePropagationSetter() package.py000066400000000000000000000003221527070072300436760ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations _instruments: tuple[str, ...] = tuple() _supports_metrics = True _semconv_status = "migration" py.typed000066400000000000000000000000001527070072300434210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgiversion.py000066400000000000000000000001441527070072300437720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000004521527070072300362770ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-wsgiasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e util/opentelemetry-util-http -e instrumentation/opentelemetry-instrumentation-wsgi opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-wsgi/tests/000077500000000000000000000000001527070072300332565ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300352760ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-wsgi/teststest_getter.py000066400000000000000000000017011527070072300361010ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-wsgi/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from opentelemetry.instrumentation.wsgi import WSGIGetter class TestGetter(TestCase): def test_get_none(self): getter = WSGIGetter() carrier = {} val = getter.get(carrier, "test") self.assertIsNone(val) def test_get(self): getter = WSGIGetter() carrier = {"HTTP_TEST_KEY": "val"} val = getter.get(carrier, "test-key") self.assertEqual(val, ["val"]) def test_keys(self): getter = WSGIGetter() keys = getter.keys( { "HTTP_TEST_KEY": "val", "HTTP_OTHER_KEY": 42, "NON_HTTP_KEY": "val", } ) self.assertEqual(keys, ["test-key", "other-key"]) def test_keys_empty(self): getter = WSGIGetter() keys = getter.keys({}) self.assertEqual(keys, []) test_wsgi_middleware.py000066400000000000000000001331271527070072300377650ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/instrumentation/opentelemetry-instrumentation-wsgi/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines import sys import unittest import wsgiref.util as wsgiref_util from unittest import mock from urllib.parse import urlsplit import opentelemetry.instrumentation.wsgi as otel_wsgi from opentelemetry import trace as trace_api from opentelemetry.instrumentation._semconv import ( HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, _server_active_requests_count_attrs_new, _server_active_requests_count_attrs_old, _server_duration_attrs_new, _server_duration_attrs_old, _StabilityMode, ) from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, NumberDataPoint, ) from opentelemetry.sdk.resources import Resource from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_FLAVOR, HTTP_HOST, HTTP_METHOD, HTTP_SCHEME, HTTP_SERVER_NAME, HTTP_STATUS_CODE, HTTP_TARGET, HTTP_URL, HTTP_USER_AGENT, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_HOST_NAME, NET_HOST_PORT, ) from opentelemetry.semconv._incubating.attributes.user_agent_attributes import ( USER_AGENT_SYNTHETIC_TYPE, ) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, ) from opentelemetry.semconv.attributes.network_attributes import ( NETWORK_PROTOCOL_VERSION, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.semconv.attributes.url_attributes import ( URL_FULL, URL_PATH, URL_QUERY, URL_SCHEME, ) from opentelemetry.semconv.attributes.user_agent_attributes import ( USER_AGENT_ORIGINAL, ) from opentelemetry.test.test_base import TestBase from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.trace import StatusCode from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS, ) class Response: def __init__(self): self.iter = iter([b"*"]) self.close_calls = 0 def __iter__(self): return self def __next__(self): return next(self.iter) def close(self): self.close_calls += 1 def simple_wsgi(environ, start_response): assert isinstance(environ, dict) start_response("200 OK", [("Content-Type", "text/plain")]) return [b"*"] def create_iter_wsgi(response): def iter_wsgi(environ, start_response): assert isinstance(environ, dict) start_response("200 OK", [("Content-Type", "text/plain")]) return response return iter_wsgi def create_gen_wsgi(response): def gen_wsgi(environ, start_response): result = create_iter_wsgi(response)(environ, start_response) yield from result getattr(result, "close", lambda: None)() return gen_wsgi def error_wsgi(environ, start_response): assert isinstance(environ, dict) exc_info = None try: raise ValueError except ValueError: exc_info = sys.exc_info() start_response("200 OK", [("Content-Type", "text/plain")], exc_info) exc_info = None return [b"*"] def error_wsgi_unhandled(environ, start_response): assert isinstance(environ, dict) raise ValueError def wsgi_with_custom_response_headers(environ, start_response): assert isinstance(environ, dict) start_response( "200 OK", [ ("content-type", "text/plain; charset=utf-8"), ("content-length", "100"), ("my-custom-header", "my-custom-value-1,my-custom-header-2"), ( "my-custom-regex-header-1", "my-custom-regex-value-1,my-custom-regex-value-2", ), ( "My-Custom-Regex-Header-2", "my-custom-regex-value-3,my-custom-regex-value-4", ), ("My-Secret-Header", "My Secret Value"), ], ) return [b"*"] def wsgi_with_repeat_custom_response_headers(environ, start_response): assert isinstance(environ, dict) start_response( "200 OK", [ ("my-custom-header", "my-custom-value-1"), ("my-custom-header", "my-custom-value-2"), ], ) return [b"*"] _expected_metric_names_old = [ "http.server.active_requests", "http.server.duration", ] _expected_metric_names_new = [ "http.server.active_requests", "http.server.request.duration", ] _recommended_metrics_attrs_old = { "http.server.active_requests": _server_active_requests_count_attrs_old, "http.server.duration": _server_duration_attrs_old, } _recommended_metrics_attrs_new = { "http.server.active_requests": _server_active_requests_count_attrs_new, "http.server.request.duration": _server_duration_attrs_new, } _server_active_requests_count_attrs_both = _server_active_requests_count_attrs_old _server_active_requests_count_attrs_both.extend(_server_active_requests_count_attrs_new) _recommended_metrics_attrs_both = { "http.server.active_requests": _server_active_requests_count_attrs_both, "http.server.duration": _server_duration_attrs_old, "http.server.request.duration": _server_duration_attrs_new, } SCOPE = "opentelemetry.instrumentation.wsgi" class TestWsgiApplication(WsgiTestBase): def setUp(self): super().setUp() test_name = "" if hasattr(self, "_testMethodName"): test_name = self._testMethodName sem_conv_mode = "default" if "new_semconv" in test_name: sem_conv_mode = "http" elif "both_semconv" in test_name: sem_conv_mode = "http/dup" self.env_patch = mock.patch.dict( "os.environ", { OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) _OpenTelemetrySemanticConventionStability._initialized = False self.env_patch.start() def validate_response( self, response, error=None, span_name="GET /", http_method="GET", span_attributes=None, response_headers=None, old_sem_conv=True, new_sem_conv=False, ): while True: try: value = next(response) self.assertEqual(value, b"*") except StopIteration: break expected_headers = [("Content-Type", "text/plain")] if response_headers: expected_headers.extend(response_headers) self.assertEqual(self.status, "200 OK") self.assertEqual(self.response_headers, expected_headers) if error: self.assertIs(self.exc_info[0], error) self.assertIsInstance(self.exc_info[1], error) self.assertIsNotNone(self.exc_info[2]) else: self.assertIsNone(self.exc_info) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, span_name) self.assertEqual(span_list[0].kind, trace_api.SpanKind.SERVER) expected_attributes = {} expected_attributes_old = { HTTP_SERVER_NAME: "127.0.0.1", HTTP_SCHEME: "http", NET_HOST_PORT: 80, HTTP_HOST: "127.0.0.1", HTTP_FLAVOR: "1.0", HTTP_URL: "http://127.0.0.1/", HTTP_STATUS_CODE: 200, NET_HOST_NAME: "127.0.0.1", } expected_attributes_new = { SERVER_PORT: 80, SERVER_ADDRESS: "127.0.0.1", NETWORK_PROTOCOL_VERSION: "1.0", HTTP_RESPONSE_STATUS_CODE: 200, URL_SCHEME: "http", } if old_sem_conv: expected_attributes.update(expected_attributes_old) if new_sem_conv: expected_attributes.update(expected_attributes_new) expected_attributes.update(span_attributes or {}) if http_method is not None: if old_sem_conv: expected_attributes[HTTP_METHOD] = http_method if new_sem_conv: expected_attributes[HTTP_REQUEST_METHOD] = http_method self.assertEqual(span_list[0].attributes, expected_attributes) # Helper modeled after ASGI test suite to assert presence of exemplars on histogram metrics def _assert_exemplars_present(self, metric_names, context=""): metrics_data = self.memory_metrics_reader.get_metrics_data() self.assertTrue( len(metrics_data.resource_metrics) > 0, f"No resource metrics collected while checking exemplars ({context})", ) checked = set() for resource_metric in metrics_data.resource_metrics: for scope_metric in resource_metric.scope_metrics: for metric in scope_metric.metrics: if metric.name not in metric_names: continue checked.add(metric.name) # Expect exactly one datapoint per histogram metric in these tests data_points = list(metric.data.data_points) self.assertGreater( len(data_points), 0, f"No data points for {metric.name} while checking exemplars ({context})", ) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertGreater( len(point.exemplars), 0, f"Expected at least one exemplar on histogram data point for {metric.name} ({context}) but none found.", ) # Ensure we actually saw all targeted metrics self.assertSetEqual( set(metric_names), checked, f"Did not observe all targeted metrics when asserting exemplars ({context}). Expected {metric_names} got {checked}", ) def test_basic_wsgi_call(self): app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) response = app(self.environ, self.start_response) self.validate_response(response) def test_basic_wsgi_call_new_semconv(self): app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) response = app(self.environ, self.start_response) self.validate_response(response, old_sem_conv=False, new_sem_conv=True) def test_basic_wsgi_call_both_semconv(self): app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) response = app(self.environ, self.start_response) self.validate_response(response, old_sem_conv=True, new_sem_conv=True) def test_hooks(self): hook_headers = ( "hook_attr", "hello otel", ) def request_hook(span, environ): span.update_name("name from hook") def response_hook(span, environ, status_code, response_headers): span.set_attribute("hook_attr", "hello world") response_headers.append(hook_headers) app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi, request_hook, response_hook) response = app(self.environ, self.start_response) self.validate_response( response, span_name="name from hook", span_attributes={"hook_attr": "hello world"}, response_headers=(hook_headers,), ) def test_wsgi_not_recording(self): mock_tracer = mock.Mock() mock_span = mock.Mock() mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span with mock.patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) # pylint: disable=W0612 response = app(self.environ, self.start_response) # noqa: F841 self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) def test_wsgi_iterable(self): original_response = Response() iter_wsgi = create_iter_wsgi(original_response) app = otel_wsgi.OpenTelemetryMiddleware(iter_wsgi) response = app(self.environ, self.start_response) # Verify that start_response has been called self.assertTrue(self.status) self.validate_response(response) # Verify that close has been called exactly once self.assertEqual(1, original_response.close_calls) def test_wsgi_generator(self): original_response = Response() gen_wsgi = create_gen_wsgi(original_response) app = otel_wsgi.OpenTelemetryMiddleware(gen_wsgi) response = app(self.environ, self.start_response) # Verify that start_response has not been called self.assertIsNone(self.status) self.validate_response(response) # Verify that close has been called exactly once self.assertEqual(original_response.close_calls, 1) def test_wsgi_exc_info(self): app = otel_wsgi.OpenTelemetryMiddleware(error_wsgi) response = app(self.environ, self.start_response) self.validate_response(response, error=ValueError) def test_wsgi_internal_error(self): app = otel_wsgi.OpenTelemetryMiddleware(error_wsgi_unhandled) self.assertRaises(ValueError, app, self.environ, self.start_response) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual( span_list[0].status.status_code, StatusCode.ERROR, ) def test_wsgi_metrics(self): app = otel_wsgi.OpenTelemetryMiddleware(error_wsgi_unhandled) self.assertRaises(ValueError, app, self.environ, self.start_response) self.assertRaises(ValueError, app, self.environ, self.start_response) self.assertRaises(ValueError, app, self.environ, self.start_response) number_data_point_seen = False histogram_data_point_seen = False metrics = self.get_sorted_metrics(SCOPE) self.assertTrue(len(metrics) > 0) for metric in metrics: self.assertIn(metric.name, _expected_metric_names_old) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 3) histogram_data_point_seen = True if isinstance(point, NumberDataPoint): number_data_point_seen = True for attr in point.attributes: self.assertIn( attr, _recommended_metrics_attrs_old[metric.name], ) self.assertTrue(number_data_point_seen and histogram_data_point_seen) def test_wsgi_metrics_exemplars_expected_old_semconv(self): # type: ignore[func-returns-value] """Failing test asserting exemplars should be present for duration histogram (old semconv).""" app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) # generate several requests to increase chance of exemplar sampling for _ in range(5): response = app(self.environ, self.start_response) # exhaust response iterable for _ in response: pass self._assert_exemplars_present({"http.server.duration"}, context="old semconv") def test_wsgi_metrics_exemplars_expected_new_semconv(self): # type: ignore[func-returns-value] """Failing test asserting exemplars should be present for request duration histogram (new semconv).""" app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) for _ in range(5): response = app(self.environ, self.start_response) for _ in response: pass self._assert_exemplars_present({"http.server.request.duration"}, context="new semconv") def test_wsgi_metrics_exemplars_expected_both_semconv(self): # type: ignore[func-returns-value] """Failing test asserting exemplars should be present for both duration histograms when both semconv modes enabled.""" app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) for _ in range(5): response = app(self.environ, self.start_response) for _ in response: pass self._assert_exemplars_present( {"http.server.duration", "http.server.request.duration"}, context="both semconv", ) def test_wsgi_metrics_new_semconv(self): # pylint: disable=too-many-nested-blocks app = otel_wsgi.OpenTelemetryMiddleware(error_wsgi_unhandled) self.assertRaises(ValueError, app, self.environ, self.start_response) self.assertRaises(ValueError, app, self.environ, self.start_response) self.assertRaises(ValueError, app, self.environ, self.start_response) number_data_point_seen = False histogram_data_point_seen = False metrics = self.get_sorted_metrics(SCOPE) self.assertTrue(len(metrics) != 0) for metric in metrics: self.assertIn(metric.name, _expected_metric_names_new) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 3) if metric.name == "http.server.request.duration": self.assertEqual( point.explicit_bounds, HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) histogram_data_point_seen = True if isinstance(point, NumberDataPoint): number_data_point_seen = True for attr in point.attributes: self.assertIn( attr, _recommended_metrics_attrs_new[metric.name], ) self.assertTrue(number_data_point_seen and histogram_data_point_seen) def test_wsgi_metrics_both_semconv(self): # pylint: disable=too-many-nested-blocks app = otel_wsgi.OpenTelemetryMiddleware(error_wsgi_unhandled) self.assertRaises(ValueError, app, self.environ, self.start_response) number_data_point_seen = False histogram_data_point_seen = False metrics = self.get_sorted_metrics(SCOPE) self.assertTrue(len(metrics) != 0) for metric in metrics: if metric.unit == "ms": self.assertEqual(metric.name, "http.server.duration") elif metric.unit == "s": self.assertEqual(metric.name, "http.server.request.duration") else: self.assertEqual(metric.name, "http.server.active_requests") data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) if metric.name == "http.server.request.duration": self.assertEqual( point.explicit_bounds, HTTP_DURATION_HISTOGRAM_BUCKETS_NEW, ) histogram_data_point_seen = True if isinstance(point, NumberDataPoint): number_data_point_seen = True for attr in point.attributes: self.assertIn( attr, _recommended_metrics_attrs_both[metric.name], ) self.assertTrue(number_data_point_seen and histogram_data_point_seen) def test_nonstandard_http_method(self): self.environ["REQUEST_METHOD"] = "NONSTANDARD" app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) response = app(self.environ, self.start_response) self.validate_response(response, span_name="HTTP", http_method="_OTHER") @mock.patch.dict( "os.environ", { OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS: "1", }, ) def test_nonstandard_http_method_allowed(self): self.environ["REQUEST_METHOD"] = "NONSTANDARD" app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) response = app(self.environ, self.start_response) self.validate_response(response, span_name="NONSTANDARD /", http_method="NONSTANDARD") def test_default_span_name_missing_path_info(self): """Test that default span_names with missing path info.""" self.environ.pop("PATH_INFO") method = self.environ.get("REQUEST_METHOD", "").strip() app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) response = app(self.environ, self.start_response) self.validate_response(response, span_name=method) # pylint: disable=too-many-public-methods class TestWsgiAttributes(unittest.TestCase): def setUp(self): self.environ = {} wsgiref_util.setup_testing_defaults(self.environ) self.span = mock.create_autospec(trace_api.Span, spec_set=True) def test_request_attributes(self): self.environ["QUERY_STRING"] = "foo=bar" attrs = otel_wsgi.collect_request_attributes(self.environ) self.assertDictEqual( attrs, { HTTP_METHOD: "GET", HTTP_HOST: "127.0.0.1", HTTP_URL: "http://127.0.0.1/?foo=bar", NET_HOST_PORT: 80, HTTP_SCHEME: "http", HTTP_SERVER_NAME: "127.0.0.1", HTTP_FLAVOR: "1.0", NET_HOST_NAME: "127.0.0.1", }, ) def test_request_attributes_new_semconv(self): self.environ["QUERY_STRING"] = "foo=bar" self.environ["REQUEST_URI"] = "http://127.0.0.1/?foo=bar" attrs = otel_wsgi.collect_request_attributes( self.environ, _StabilityMode.HTTP, ) self.assertDictEqual( attrs, { HTTP_REQUEST_METHOD: "GET", SERVER_ADDRESS: "127.0.0.1", SERVER_PORT: 80, NETWORK_PROTOCOL_VERSION: "1.0", URL_PATH: "/", URL_QUERY: "foo=bar", URL_SCHEME: "http", }, ) def validate_url( self, expected_url, raw=False, has_host=True, old_semconv=True, new_semconv=False, ): parts = urlsplit(expected_url) expected_old = { HTTP_SCHEME: parts.scheme, NET_HOST_PORT: parts.port or (80 if parts.scheme == "http" else 443), HTTP_SERVER_NAME: parts.hostname, # Not true in the general case, but for all tests. } expected_new = { SERVER_PORT: parts.port or (80 if parts.scheme == "http" else 443), SERVER_ADDRESS: parts.hostname, URL_PATH: parts.path, URL_QUERY: parts.query, } if old_semconv: if raw: expected_old[HTTP_TARGET] = expected_url.split(parts.netloc, 1)[1] else: expected_old[HTTP_URL] = expected_url if has_host: expected_old[HTTP_HOST] = parts.hostname if new_semconv: if raw: expected_new[URL_PATH] = expected_url.split(parts.path, 1)[1] if parts.query: expected_new[URL_QUERY] = expected_url.split(parts.query, 1)[1] else: expected_new[URL_FULL] = expected_url if has_host: expected_new[SERVER_ADDRESS] = parts.hostname attrs = otel_wsgi.collect_request_attributes(self.environ) self.assertGreaterEqual(attrs.items(), expected_old.items(), expected_url + " expected.") def test_request_attributes_with_partial_raw_uri(self): self.environ["RAW_URI"] = "/?foo=bar/#top" self.validate_url("http://127.0.0.1/?foo=bar/#top", raw=True) self.validate_url( "http://127.0.0.1/?foo=bar/#top", raw=True, old_semconv=False, new_semconv=True, ) self.validate_url( "http://127.0.0.1/?foo=bar/#top", raw=True, old_semconv=True, new_semconv=True, ) def test_request_attributes_with_partial_raw_uri_and_nonstandard_port( self, ): self.environ["RAW_URI"] = "/?" del self.environ["HTTP_HOST"] self.environ["SERVER_PORT"] = "8080" self.validate_url("http://127.0.0.1:8080/?", raw=True, has_host=False) self.validate_url( "http://127.0.0.1:8080/?", raw=True, has_host=False, old_semconv=False, new_semconv=True, ) self.validate_url( "http://127.0.0.1:8080/?", raw=True, has_host=False, old_semconv=True, new_semconv=True, ) def test_https_uri_port(self): del self.environ["HTTP_HOST"] self.environ["SERVER_PORT"] = "443" self.environ["wsgi.url_scheme"] = "https" self.validate_url("https://127.0.0.1/", has_host=False) self.validate_url( "https://127.0.0.1/", has_host=False, old_semconv=False, new_semconv=True, ) self.validate_url( "https://127.0.0.1/", has_host=False, old_semconv=True, new_semconv=True, ) self.environ["SERVER_PORT"] = "8080" self.validate_url("https://127.0.0.1:8080/", has_host=False) self.validate_url( "https://127.0.0.1:8080/", has_host=False, old_semconv=False, new_semconv=True, ) self.validate_url( "https://127.0.0.1:8080/", has_host=False, old_semconv=True, new_semconv=True, ) self.environ["SERVER_PORT"] = "80" self.validate_url("https://127.0.0.1:80/", has_host=False) self.validate_url( "https://127.0.0.1:80/", has_host=False, old_semconv=False, new_semconv=True, ) self.validate_url( "https://127.0.0.1:80/", has_host=False, old_semconv=True, new_semconv=True, ) def test_http_uri_port(self): del self.environ["HTTP_HOST"] self.environ["SERVER_PORT"] = "80" self.environ["wsgi.url_scheme"] = "http" self.validate_url("http://127.0.0.1/", has_host=False) self.environ["SERVER_PORT"] = "8080" self.validate_url("http://127.0.0.1:8080/", has_host=False) self.environ["SERVER_PORT"] = "443" self.validate_url("http://127.0.0.1:443/", has_host=False) def test_request_attributes_with_nonstandard_port_and_no_host(self): del self.environ["HTTP_HOST"] self.environ["SERVER_PORT"] = "8080" self.validate_url("http://127.0.0.1:8080/", has_host=False) self.environ["SERVER_PORT"] = "443" self.validate_url("http://127.0.0.1:443/", has_host=False) def test_request_attributes_with_conflicting_nonstandard_port(self): self.environ["HTTP_HOST"] += ":8080" # Note that we do not correct SERVER_PORT expected = { HTTP_HOST: "127.0.0.1:8080", HTTP_URL: "http://127.0.0.1:8080/", NET_HOST_PORT: 80, } self.assertGreaterEqual( otel_wsgi.collect_request_attributes(self.environ).items(), expected.items(), ) def test_request_attributes_with_faux_scheme_relative_raw_uri(self): self.environ["RAW_URI"] = "//127.0.0.1/?" self.validate_url("http://127.0.0.1//127.0.0.1/?", raw=True) def test_request_attributes_pathless(self): self.environ["RAW_URI"] = "" self.assertIsNone(otel_wsgi.collect_request_attributes(self.environ).get(HTTP_TARGET)) def test_request_attributes_with_full_request_uri(self): self.environ["HTTP_HOST"] = "127.0.0.1:8080" self.environ["REQUEST_METHOD"] = "CONNECT" self.environ["REQUEST_URI"] = ( "http://docs.python.org:80/3/library/urllib.parse.html?highlight=params#url-parsing" # Might happen in a CONNECT request ) self.environ["PATH_INFO"] = "/3/library/urllib.parse.html" self.environ["QUERY_STRING"] = "highlight=params" expected_old = { HTTP_HOST: "127.0.0.1:8080", HTTP_TARGET: "http://docs.python.org:80/3/library/urllib.parse.html?highlight=params#url-parsing", } expected_new = { URL_PATH: "/3/library/urllib.parse.html", URL_QUERY: "highlight=params", } self.assertGreaterEqual( otel_wsgi.collect_request_attributes(self.environ).items(), expected_old.items(), ) self.assertGreaterEqual( otel_wsgi.collect_request_attributes( self.environ, _StabilityMode.HTTP, ).items(), expected_new.items(), ) def test_request_attributes_with_invalid_request_uri_uses_wsgi_environ( self, ): # Previously raised ValueError when REQUEST_URI was parsed. self.environ["REQUEST_URI"] = "http://example.com[invalid" self.environ["PATH_INFO"] = "/safe/path" self.environ["QUERY_STRING"] = "a=b" expected_old = { HTTP_TARGET: self.environ["REQUEST_URI"], } expected_new = { URL_PATH: "/safe/path", URL_QUERY: "a=b", } self.assertGreaterEqual( otel_wsgi.collect_request_attributes(self.environ).items(), expected_old.items(), ) self.assertGreaterEqual( otel_wsgi.collect_request_attributes( self.environ, _StabilityMode.HTTP, ).items(), expected_new.items(), ) def test_http_user_agent_attribute(self): self.environ["HTTP_USER_AGENT"] = "test-useragent" expected = {HTTP_USER_AGENT: "test-useragent"} expected_new = {USER_AGENT_ORIGINAL: "test-useragent"} self.assertGreaterEqual( otel_wsgi.collect_request_attributes(self.environ).items(), expected.items(), ) self.assertGreaterEqual( otel_wsgi.collect_request_attributes( self.environ, _StabilityMode.HTTP, ).items(), expected_new.items(), ) def test_http_user_agent_bytes_like_attribute(self): self.environ["HTTP_USER_AGENT"] = b"AlwaysOn-Monitor/1.0" attributes = otel_wsgi.collect_request_attributes(self.environ) self.assertEqual(attributes[HTTP_USER_AGENT], "AlwaysOn-Monitor/1.0") self.assertEqual(attributes[USER_AGENT_SYNTHETIC_TYPE], "test") def test_http_user_agent_synthetic_bot_detection(self): """Test that bot user agents are detected as synthetic with type 'bot'""" test_cases = [ "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", "googlebot/1.0", "bingbot/1.0", ] for user_agent in test_cases: with self.subTest(user_agent=user_agent): self.environ["HTTP_USER_AGENT"] = user_agent attributes = otel_wsgi.collect_request_attributes(self.environ) # Should have both the original user agent and synthetic type self.assertIn(HTTP_USER_AGENT, attributes) self.assertEqual(attributes[HTTP_USER_AGENT], user_agent) self.assertIn(USER_AGENT_SYNTHETIC_TYPE, attributes) self.assertEqual(attributes[USER_AGENT_SYNTHETIC_TYPE], "bot") def test_http_user_agent_synthetic_test_detection(self): """Test that test user agents are detected as synthetic with type 'test'""" test_cases = [ "alwayson/1.0", "AlwaysOn/2.0", "test-alwayson-client", ] for user_agent in test_cases: with self.subTest(user_agent=user_agent): self.environ["HTTP_USER_AGENT"] = user_agent attributes = otel_wsgi.collect_request_attributes(self.environ) # Should have both the original user agent and synthetic type self.assertIn(HTTP_USER_AGENT, attributes) self.assertEqual(attributes[HTTP_USER_AGENT], user_agent) self.assertIn(USER_AGENT_SYNTHETIC_TYPE, attributes) self.assertEqual(attributes[USER_AGENT_SYNTHETIC_TYPE], "test") def test_http_user_agent_non_synthetic(self): """Test that normal user agents are not marked as synthetic""" test_cases = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "PostmanRuntime/7.28.4", "curl/7.68.0", ] for user_agent in test_cases: with self.subTest(user_agent=user_agent): self.environ["HTTP_USER_AGENT"] = user_agent attributes = otel_wsgi.collect_request_attributes(self.environ) # Should have the original user agent but no synthetic type self.assertIn(HTTP_USER_AGENT, attributes) self.assertEqual(attributes[HTTP_USER_AGENT], user_agent) self.assertNotIn(USER_AGENT_SYNTHETIC_TYPE, attributes) def test_http_user_agent_synthetic_new_semconv(self): """Test synthetic user agent detection with new semantic conventions""" self.environ["HTTP_USER_AGENT"] = "Mozilla/5.0 (compatible; Googlebot/2.1)" attributes = otel_wsgi.collect_request_attributes( self.environ, _StabilityMode.HTTP, ) # Should have both the new semconv user agent and synthetic type self.assertIn(USER_AGENT_ORIGINAL, attributes) self.assertEqual( attributes[USER_AGENT_ORIGINAL], "Mozilla/5.0 (compatible; Googlebot/2.1)", ) self.assertIn(USER_AGENT_SYNTHETIC_TYPE, attributes) self.assertEqual(attributes[USER_AGENT_SYNTHETIC_TYPE], "bot") def test_response_attributes(self): otel_wsgi.add_response_attributes(self.span, "404 Not Found", {}) otel_wsgi.add_response_attributes( self.span, "404 Not Found", {}, sem_conv_opt_in_mode=_StabilityMode.HTTP, ) expected = (mock.call(HTTP_STATUS_CODE, 404),) expected_new = (mock.call(HTTP_RESPONSE_STATUS_CODE, 404),) self.assertEqual(self.span.set_attribute.call_count, 2) self.span.set_attribute.assert_has_calls(expected, any_order=True) self.span.set_attribute.assert_has_calls(expected_new, any_order=True) def test_response_attributes_noop(self): mock_span = mock.Mock() mock_span.is_recording.return_value = False attrs = {} otel_wsgi.add_response_attributes(mock_span, "404 Not Found", {}, duration_attrs=attrs) self.assertEqual(mock_span.set_attribute.call_count, 0) self.assertEqual(mock_span.is_recording.call_count, 2) self.assertEqual(attrs[HTTP_STATUS_CODE], 404) def test_remove_sensitive_params(self): self.environ["HTTP_HOST"] = "username:password@mock" self.environ["PATH_INFO"] = "/status/200" self.environ["QUERY_STRING"] = "sig=secret" expected = { HTTP_URL: "http://REDACTED:REDACTED@mock/status/200?sig=REDACTED", NET_HOST_PORT: 80, } self.assertGreaterEqual( otel_wsgi.collect_request_attributes(self.environ).items(), expected.items(), ) class TestWsgiMiddlewareWithTracerProvider(WsgiTestBase): def validate_response( self, response, exporter, error=None, span_name="GET /", http_method="GET", ): while True: try: value = next(response) self.assertEqual(value, b"*") except StopIteration: break span_list = exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, span_name) self.assertEqual(span_list[0].kind, trace_api.SpanKind.SERVER) self.assertEqual(span_list[0].resource.attributes["service-key"], "service-value") def test_basic_wsgi_call(self): resource = Resource.create({"service-key": "service-value"}) result = TestBase.create_tracer_provider(resource=resource) tracer_provider, exporter = result app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi, tracer_provider=tracer_provider) response = app(self.environ, self.start_response) self.validate_response(response, exporter) def test_no_op_tracer_provider(self): app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi, tracer_provider=trace_api.NoOpTracerProvider()) response = app(self.environ, self.start_response) while True: try: value = next(response) self.assertEqual(value, b"*") except StopIteration: break span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) class TestWsgiMiddlewareWrappedWithAnotherFramework(WsgiTestBase): def test_mark_span_internal_in_presence_of_span_from_other_framework(self): tracer_provider, exporter = TestBase.create_tracer_provider() tracer = tracer_provider.get_tracer(__name__) with tracer.start_as_current_span("test", kind=trace_api.SpanKind.SERVER) as parent_span: app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi, tracer_provider=tracer_provider) response = app(self.environ, self.start_response) while True: try: value = next(response) self.assertEqual(value, b"*") except StopIteration: break span_list = exporter.get_finished_spans() self.assertEqual(trace_api.SpanKind.INTERNAL, span_list[0].kind) self.assertEqual(trace_api.SpanKind.SERVER, parent_span.kind) # internal span should be child of the parent span we have provided self.assertEqual(parent_span.context.span_id, span_list[0].parent.span_id) class TestAdditionOfCustomRequestResponseHeaders(WsgiTestBase): def setUp(self): super().setUp() self.tracer = self.tracer_provider.get_tracer(__name__) def iterate_response(self, response): while True: try: value = next(response) self.assertEqual(value, b"*") except StopIteration: break @mock.patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", }, ) def test_custom_request_headers_non_recording_span(self): try: tracer_provider = trace_api.NoOpTracerProvider() self.environ.update( { "HTTP_CUSTOM_TEST_HEADER_1": "Test Value 2", "HTTP_CUSTOM_TEST_HEADER_2": "TestValue2,TestValue3", "HTTP_REGEX_TEST_HEADER_1": "Regex Test Value 1", "HTTP_REGEX_TEST_HEADER_2": "RegexTestValue2,RegexTestValue3", "HTTP_MY_SECRET_HEADER": "My Secret Value", } ) app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi, tracer_provider=tracer_provider) response = app(self.environ, self.start_response) self.iterate_response(response) except Exception as exc: # pylint: disable=W0703 self.fail(f"Exception raised with NonRecordingSpan {exc}") @mock.patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", }, ) def test_custom_request_headers_added_in_server_span(self): self.environ.update( { "HTTP_CUSTOM_TEST_HEADER_1": "Test Value 1", "HTTP_CUSTOM_TEST_HEADER_2": "TestValue2,TestValue3", "HTTP_REGEX_TEST_HEADER_1": "Regex Test Value 1", "HTTP_REGEX_TEST_HEADER_2": "RegexTestValue2,RegexTestValue3", "HTTP_MY_SECRET_HEADER": "My Secret Value", } ) app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) response = app(self.environ, self.start_response) self.iterate_response(response) span = self.memory_exporter.get_finished_spans()[0] expected = { "http.request.header.custom_test_header_1": ("Test Value 1",), "http.request.header.custom_test_header_2": ("TestValue2,TestValue3",), "http.request.header.regex_test_header_1": ("Regex Test Value 1",), "http.request.header.regex_test_header_2": ("RegexTestValue2,RegexTestValue3",), "http.request.header.my_secret_header": ("[REDACTED]",), } self.assertSpanHasAttributes(span, expected) @mock.patch.dict( "os.environ", {OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1"}, ) def test_custom_request_headers_not_added_in_internal_span(self): self.environ.update( { "HTTP_CUSTOM_TEST_HEADER_1": "Test Value 1", } ) with self.tracer.start_as_current_span("test", kind=trace_api.SpanKind.SERVER): app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) response = app(self.environ, self.start_response) self.iterate_response(response) span = self.memory_exporter.get_finished_spans()[0] not_expected = { "http.request.header.custom_test_header_1": ("Test Value 1",), } for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) @mock.patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", }, ) def test_custom_response_headers_added_in_server_span(self): app = otel_wsgi.OpenTelemetryMiddleware(wsgi_with_custom_response_headers) response = app(self.environ, self.start_response) self.iterate_response(response) span = self.memory_exporter.get_finished_spans()[0] expected = { "http.response.header.content_type": ("text/plain; charset=utf-8",), "http.response.header.content_length": ("100",), "http.response.header.my_custom_header": ("my-custom-value-1,my-custom-header-2",), "http.response.header.my_custom_regex_header_1": ("my-custom-regex-value-1,my-custom-regex-value-2",), "http.response.header.my_custom_regex_header_2": ("my-custom-regex-value-3,my-custom-regex-value-4",), "http.response.header.my_secret_header": ("[REDACTED]",), } self.assertSpanHasAttributes(span, expected) @mock.patch.dict( "os.environ", {OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "my-custom-header"}, ) def test_custom_response_headers_not_added_in_internal_span(self): with self.tracer.start_as_current_span("test", kind=trace_api.SpanKind.INTERNAL): app = otel_wsgi.OpenTelemetryMiddleware(wsgi_with_custom_response_headers) response = app(self.environ, self.start_response) self.iterate_response(response) span = self.memory_exporter.get_finished_spans()[0] not_expected = { "http.response.header.my_custom_header": ("my-custom-value-1,my-custom-header-2",), } for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) @mock.patch.dict( "os.environ", { OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "my-custom-header", }, ) def test_repeat_custom_response_headers_added_in_server_span(self): app = otel_wsgi.OpenTelemetryMiddleware(wsgi_with_repeat_custom_response_headers) response = app(self.environ, self.start_response) self.iterate_response(response) span = self.memory_exporter.get_finished_spans()[0] expected = { "http.response.header.my_custom_header": ("my-custom-value-1,my-custom-value-2",), } self.assertSpanHasAttributes(span, expected) if __name__ == "__main__": unittest.main() opentelemetry-python-contrib-0.66b2/opamp-gen-requirements.txt000066400000000000000000000005401527070072300247110ustar00rootroot00000000000000# Use caution when bumping this version to ensure compatibility with the currently supported protobuf version. # Pinning this to the oldest grpcio version that supports protobuf 5 helps avoid RuntimeWarning messages # from the generated protobuf code and ensures continued stability for newer grpcio versions. grpcio-tools==1.63.2 mypy-protobuf~=3.5.0 opentelemetry-python-contrib-0.66b2/opamp/000077500000000000000000000000001527070072300206615ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/000077500000000000000000000000001527070072300261435ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/.changelog/000077500000000000000000000000001527070072300301505ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/.changelog/.gitignore000066400000000000000000000000141527070072300321330ustar00rootroot00000000000000!.gitignore opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/.changelog/4897.added000066400000000000000000000001031527070072300315400ustar00rootroot00000000000000`opentelemetry-opamp-client`: allow configuring agent capabilities opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/.changelog/4930.fixed000066400000000000000000000002451527070072300315710ustar00rootroot00000000000000Add `requests` to the `opentelemetry-opamp-client` dependencies, fixing a `ModuleNotFoundError` when importing `opentelemetry._opamp.client` in a fresh environment. opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/CHANGELOG.md000066400000000000000000000022461527070072300277600ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Version 0.3b0 (2026-07-16) ### Added - `opentelemetry-opamp-client`: support non-JSON effective config bodies ([#4536](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4536)) ## Version 0.2b0 (2026-04-01) - Breaking change: callback class `Callbacks` renamed to `OpAMPCallbacks` ([#4355](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4355)) ## Version 0.1b0 (2026-03-23) - Initial implementation ([#3635](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3635)) - Update client to have additional callback methods ([#4322](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4322)) opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/LICENSE000066400000000000000000000261351527070072300271570ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/README.rst000066400000000000000000000013101527070072300276250ustar00rootroot00000000000000OpenTelemetry OpAMP Client ========================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-opamp-client.svg :target: https://pypi.org/project/opentelemetry-opamp-client/ Installation ------------ :: pip install opentelemetry-opamp-client This package provides an HTTP OpAMP client than can be used by OpenTelemetry distributions to provide remote config. References ---------- * `OpenTelemetry OpAMP Client `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/pyproject.toml000066400000000000000000000040261527070072300310610ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-opamp-client" dynamic = ["version"] description = "OpenTelemetry OpAMP client" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "protobuf>=5.0, < 7.0", "requests ~= 2.0", "uuid-utils>=0.11.0, <1" ] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/opamp/opentelemetry-opamp-client" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/_opamp/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] [tool.towncrier] directory = ".changelog" filename = "CHANGELOG.md" start_string = "\n" template = "../../scripts/changelog_template.j2" issue_format = "[#{issue}](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/{issue})" wrap = true issue_pattern = "^(\\d+)" [[tool.towncrier.type]] directory = "added" name = "Added" showcontent = true [[tool.towncrier.type]] directory = "changed" name = "Changed" showcontent = true [[tool.towncrier.type]] directory = "deprecated" name = "Deprecated" showcontent = true [[tool.towncrier.type]] directory = "removed" name = "Removed" showcontent = true [[tool.towncrier.type]] directory = "fixed" name = "Fixed" showcontent = true opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/000077500000000000000000000000001527070072300267325ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/opentelemetry/000077500000000000000000000000001527070072300316265ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/000077500000000000000000000000001527070072300331015ustar00rootroot00000000000000__init__.py000066400000000000000000000064141527070072300351400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ OpenTelemetry Python - OpAMP client ----------------------------------- This package provides a bunch of classes that can be used by OpenTelemetry distributions implementers to implement remote config support via the `OpAMP protocol`_. The client implements the following capabilities: * ReportsStatus * ReportsHeartbeat * AcceptsRemoteConfig * ReportsRemoteConfig * ReportsEffectiveConfig These capabilities are enough to get a remote config from an OpAMP server, parse it, apply it and ack it. While the client supports pluggable transports, only an HTTP backends using the ``requests`` library is implemented. Adding WebSocket support shouldn't be hard but it will require some rework in the OpAMPAgent class. Since OpAMP APIs, config options or environment variables are not standardizes the distros are required to provide code doing so. OTel Python distros would need to provide their own OpAMPCallbacks subclass that implements the actual change of whatever configuration their backends sends. Please note that the API is not finalized yet and so the name is called ``_opamp`` with the underscore. Usage ----- .. code-block:: python import os from opentelemetry._opamp.agent import OpAMPAgent from opentelemetry._opamp.callbacks import OpAMPCallbacks from opentelemetry._opamp.client import OpAMPClient from opentelemetry.sdk._configuration import _OTelSDKConfigurator from opentelemetry.sdk.resources import OTELResourceDetector class MyCallbacks(OpAMPCallbacks): def on_message(self, agent, client, message): if message.remote_config is None: return for ( config_filename, config, ) in message.remote_config.config.config_map.items(): print("do something") class MyOpenTelemetryConfigurator(_OTelSDKConfigurator): def _configure(self, **kwargs): super()._configure(**kwargs) enable_opamp = False endpoint = os.environ.get("OTEL_PYTHON_OPAMP_ENDPOINT") if endpoint: # this is not great but we don't have the calculated resource attributes around # see https://github.com/open-telemetry/opentelemetry-python/pull/4646 for creating # an entry point distros can implement resource = OTELResourceDetector().detect() agent_identifying_attributes = { "service.name": resource.attributes.get("service.name"), } opamp_client = OpAMPClient( endpoint=endpoint, agent_identifying_attributes=agent_identifying_attributes, ) opamp_agent = OpAMPAgent( interval=30, callbacks=MyCallbacks(), client=opamp_client, ) opamp_agent.start() API --- .. _OpAMP protocol: https://opentelemetry.io/docs/specs/opamp/ """ from opentelemetry._opamp.agent import OpAMPAgent from opentelemetry._opamp.callbacks import MessageData, OpAMPCallbacks from opentelemetry._opamp.client import OpAMPClient __all__ = ["MessageData", "OpAMPAgent", "OpAMPCallbacks", "OpAMPClient"] agent.py000066400000000000000000000225061527070072300344770ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import atexit import logging import queue import random import threading from collections.abc import Callable from typing import Any from opentelemetry._opamp.callbacks import MessageData, OpAMPCallbacks from opentelemetry._opamp.client import OpAMPClient from opentelemetry._opamp.proto import opamp_pb2 logger = logging.getLogger(__name__) def _safe_invoke(function: Callable[..., Any], *args: Any) -> None: function_name = "" try: function_name = function.__name__ function(*args) except Exception as exc: # pylint: disable=broad-exception-caught logger.error("Error when invoking function '%s'", function_name, exc_info=exc) class _Job: """ Represents a single request job, with retry/backoff metadata. """ def __init__( self, payload: Any, max_retries: int = 1, initial_backoff: float = 1.0, callback: Callable[..., None] | None = None, ): self.payload = payload self.attempt = 0 self.max_retries = max_retries self.initial_backoff = initial_backoff # callback is called after OpAMP message handler is executed self.callback = callback def should_retry(self) -> bool: """Checks if we should retry again""" return self.attempt <= self.max_retries def delay(self) -> float: """Calculate the delay before next retry""" assert self.attempt > 0 return self.initial_backoff * (2 ** (self.attempt - 1)) * random.uniform(0.8, 1.2) class OpAMPAgent: """ OpAMPAgent handles: - periodic “heartbeat” calls enqueued at a fixed interval - on-demand calls via send() - exponential backoff retry on failures - immediate cancellation of all jobs on shutdown """ def __init__( self, *, interval: float = 30, callbacks: OpAMPCallbacks, max_retries: int = 10, heartbeat_max_retries: int = 1, initial_backoff: float = 1.0, client: OpAMPClient, ): """ :param interval: seconds between heartbeat calls :param callbacks: OpAMPCallbacks instance for receiving client events :param max_retries: how many times to retry a failed job for ad-hoc messages :param heartbeat_max_retries: how many times to retry an heartbeat failed job :param initial_backoff: base seconds for exponential backoff :param client: an OpAMPClient instance """ self._interval = interval self._callbacks = callbacks self._max_retries = max_retries self._heartbeat_max_retries = heartbeat_max_retries self._initial_backoff = initial_backoff self._queue: queue.Queue[_Job] = queue.Queue() self._stop = threading.Event() self._worker = threading.Thread(name="OpAMPAgentWorker", target=self._run_worker, daemon=True) self._scheduler = threading.Thread(name="OpAMPAgentScheduler", target=self._run_scheduler, daemon=True) # start scheduling only after connection with server has been established self._schedule = False self._client = client def _enable_scheduler(self): self._schedule = True logger.debug("Connected with endpoint, enabling heartbeat") def start(self) -> None: """ Starts the scheduler and worker threads. """ self._stop.clear() self._worker.start() self._scheduler.start() atexit.register(self.stop) # enqueue the connection message so we can then enable heartbeat payload = self._client.build_full_state_message() self.send( payload, max_retries=self._max_retries, callback=self._enable_scheduler, ) def send( self, payload: Any, max_retries: int | None = None, callback: Callable[..., None] | None = None, ) -> None: """ Enqueue an on-demand request. """ if not self._worker.is_alive(): logger.warning("Called send() but worker thread is not alive. Worker threads is started with start()") if max_retries is None: max_retries = self._max_retries job = _Job( payload, max_retries=max_retries, initial_backoff=self._initial_backoff, callback=callback, ) self._queue.put(job) logger.debug("On-demand job enqueued: %r", payload) def _run_scheduler(self) -> None: """ After we made a connection, periodically enqueue “heartbeat” jobs until stop is signaled. """ while not self._stop.wait(self._interval): if self._schedule: payload = self._client.build_heartbeat_message() job = _Job( payload=payload, max_retries=self._heartbeat_max_retries, initial_backoff=self._initial_backoff, ) self._queue.put(job) logger.debug("Periodic job enqueued") def _run_worker(self) -> None: """ Worker loop: pull jobs, attempt the message handler, retry on failure with backoff. """ # pylint: disable=broad-exception-caught while not self._stop.is_set(): try: job: _Job = self._queue.get(timeout=1) except queue.Empty: continue message = None while job.should_retry() and not self._stop.is_set(): try: message = self._client.send(job.payload) _safe_invoke(self._callbacks.on_connect, self, self._client) logger.debug("Job succeeded: %r", job.payload) break except Exception as exc: job.attempt += 1 _safe_invoke( self._callbacks.on_connect_failed, self, self._client, exc, ) logger.warning( "Job %r failed attempt %d/%d: %s", job.payload, job.attempt, job.max_retries + 1, exc, ) if not job.should_retry(): logger.error("Job %r dropped after max retries", job.payload) break # exponential backoff with +/- 20% jitter, interruptible by stop event delay = job.delay() logger.debug("Retrying in %.1fs", delay) if self._stop.wait(delay): # stop requested during backoff: abandon job logger.debug("Stop signaled, abandoning job %r", job.payload) break if message is not None: self._process_message(message) try: if job.callback is not None: job.callback() except Exception as exc: logging.warning("Callback for job failed: %s", exc) finally: self._queue.task_done() def _process_message(self, message: opamp_pb2.ServerToAgent) -> None: if message.HasField("error_response"): _safe_invoke( self._callbacks.on_error, self, self._client, message.error_response, ) return if message.flags & opamp_pb2.ServerToAgentFlags_ReportFullState: logger.debug("Server requested full state report") payload = self._client.build_full_state_message() self.send(payload) msg_data = MessageData.from_server_message(message) _safe_invoke( self._callbacks.on_message, self, self._client, msg_data, ) def stop(self, timeout: float | None = None) -> None: """ Signal server we are disconnecting and then threads to exit :param timeout: seconds to wait for each thread to join """ # Before exiting send signal the server we are disconnecting to free our resources # This is not required by the spec but is helpful in practice logger.debug("Stopping OpAMPAgent: sending AgentDisconnect") payload = self._client.build_agent_disconnect_message() try: self._client.send(payload) except Exception: # pylint: disable=broad-exception-caught logger.debug("Stopping OpAMPAgent: failed to send AgentDisconnect message") logger.debug("Stopping OpAMPAgent: signaling threads") # Signal threads to exit self._stop.set() # don't crash if the user calls stop() before start() or calls stop() multiple times try: self._worker.join(timeout=timeout) except RuntimeError as exc: logger.warning("Stopping OpAMPAgent: worker thread failed to join %s", exc) try: self._scheduler.join(timeout=timeout) except RuntimeError as exc: logger.warning("Stopping OpAMPAgent: scheduler thread failed to join %s", exc) logger.debug("OpAMPAgent stopped") callbacks.py000066400000000000000000000047631527070072300353250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from abc import ABC from dataclasses import dataclass from typing import TYPE_CHECKING from opentelemetry._opamp.proto import opamp_pb2 if TYPE_CHECKING: from opentelemetry._opamp.agent import OpAMPAgent from opentelemetry._opamp.client import OpAMPClient @dataclass class MessageData: """Structured view of a ServerToAgent message for callback consumption. Only fields the agent is expected to act on are exposed. Flags and error_response are handled internally by the client before this object reaches the callback. """ remote_config: opamp_pb2.AgentRemoteConfig | None = None @classmethod def from_server_message(cls, message: opamp_pb2.ServerToAgent) -> MessageData: return cls( remote_config=message.remote_config if message.HasField("remote_config") else None, ) class OpAMPCallbacks(ABC): """OpAMP client callbacks with no-op defaults. All methods have no-op defaults so that subclasses only need to override the callbacks they care about. New callbacks can be added in the future without breaking existing subclasses. """ def on_connect(self, agent: OpAMPAgent, client: OpAMPClient) -> None: """Called when the connection is successfully established to the Server. For HTTP clients this is called for any request if the response status is OK. """ def on_connect_failed( self, agent: OpAMPAgent, client: OpAMPClient, error: Exception, ) -> None: """Called when the connection to the Server cannot be established. May also be called if the connection is lost and reconnection attempt fails. """ def on_error( self, agent: OpAMPAgent, client: OpAMPClient, error_response: opamp_pb2.ServerErrorResponse, ) -> None: """Called when the Server reports an error in response to a previously sent request. Useful for logging purposes. The Agent should not attempt to process the error by reconnecting or retrying previous operations. The client handles the UNAVAILABLE case internally by performing retries as necessary. """ def on_message( self, agent: OpAMPAgent, client: OpAMPClient, message: MessageData, ) -> None: """Called when the Agent receives a message that needs processing.""" client.py000066400000000000000000000160271527070072300346600ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Generator, Mapping from logging import getLogger from typing import Any, Final from uuid_utils import uuid7 from opentelemetry._opamp import messages from opentelemetry._opamp.proto import opamp_pb2 from opentelemetry._opamp.transport.base import HttpTransport from opentelemetry._opamp.transport.requests import RequestsTransport from opentelemetry._opamp.version import __version__ from opentelemetry.context import ( _SUPPRESS_INSTRUMENTATION_KEY, attach, detach, set_value, ) from opentelemetry.util.types import AnyValue _logger = getLogger(__name__) _DEFAULT_OPAMP_TIMEOUT_MS = 1_000 _OPAMP_HTTP_HEADERS = { "Content-Type": "application/x-protobuf", "User-Agent": "OTel-OpAMP-Python/" + __version__, } _DEFAULT_CAPABILITIES: Final = ( opamp_pb2.AgentCapabilities.AgentCapabilities_ReportsStatus | opamp_pb2.AgentCapabilities.AgentCapabilities_ReportsHeartbeat | opamp_pb2.AgentCapabilities.AgentCapabilities_AcceptsRemoteConfig | opamp_pb2.AgentCapabilities.AgentCapabilities_ReportsRemoteConfig | opamp_pb2.AgentCapabilities.AgentCapabilities_ReportsEffectiveConfig ) class OpAMPClient: """ OpAMPClient implement the helpers for building and sending messages that the agent will use. """ def __init__( self, *, endpoint: str, headers: Mapping[str, str] | None = None, timeout_millis: int = _DEFAULT_OPAMP_TIMEOUT_MS, agent_identifying_attributes: Mapping[str, AnyValue], agent_non_identifying_attributes: Mapping[str, AnyValue] | None = None, capabilities: int = _DEFAULT_CAPABILITIES, transport: HttpTransport | None = None, # this matches requests but can be mapped to other http libraries APIs tls_certificate: str | bool = True, tls_client_certificate: str | None = None, tls_client_key: str | None = None, ): # ReportsStatus is required by the OpAMP specification: # https://opentelemetry.io/docs/specs/opamp/#agenttoservercapabilities if not capabilities & (opamp_pb2.AgentCapabilities.AgentCapabilities_ReportsStatus): raise ValueError("OpAMP capabilities must include ReportsStatus") self._capabilities = capabilities self._timeout_millis = timeout_millis self._transport = RequestsTransport() if transport is None else transport self._endpoint = endpoint headers = headers or {} self._headers = {**_OPAMP_HTTP_HEADERS, **headers} self._tls_certificate = tls_certificate self._tls_client_certificate = tls_client_certificate self._tls_client_key = tls_client_key self._agent_description = messages.build_agent_description( identifying_attributes=agent_identifying_attributes, non_identifying_attributes=agent_non_identifying_attributes, ) self._sequence_num: int = 0 self._instance_uid: bytes = uuid7().bytes self._remote_config_status: opamp_pb2.RemoteConfigStatus | None = None self._effective_config: opamp_pb2.EffectiveConfig | None = None def build_agent_disconnect_message(self) -> bytes: message = messages.build_agent_disconnect_message( instance_uid=self._instance_uid, sequence_num=self._sequence_num, capabilities=self._capabilities, ) data = messages.encode_message(message) return data def build_heartbeat_message(self) -> bytes: message = messages.build_heartbeat_message( instance_uid=self._instance_uid, sequence_num=self._sequence_num, capabilities=self._capabilities, ) data = messages.encode_message(message) return data def update_effective_config( self, effective_config: Mapping[str, Any], content_type: str, ) -> opamp_pb2.EffectiveConfig: self._effective_config = messages.build_effective_config_message( config=effective_config, content_type=content_type ) return self._effective_config def update_remote_config_status( self, remote_config_hash: bytes, status: opamp_pb2.RemoteConfigStatuses.ValueType, error_message: str = "", ) -> opamp_pb2.RemoteConfigStatus | None: status_changed = ( not self._remote_config_status or self._remote_config_status.last_remote_config_hash != remote_config_hash or self._remote_config_status.status != status or self._remote_config_status.error_message != error_message ) # if the status changed update we return the RemoteConfigStatus message so that we can send it to the server if status_changed: _logger.debug( "Update remote config status changed for %s", remote_config_hash, ) self._remote_config_status = messages.build_remote_config_status_message( last_remote_config_hash=remote_config_hash, status=status, error_message=error_message, ) return self._remote_config_status return None def build_remote_config_status_response_message(self, remote_config_status: opamp_pb2.RemoteConfigStatus) -> bytes: message = messages.build_remote_config_status_response_message( instance_uid=self._instance_uid, sequence_num=self._sequence_num, capabilities=self._capabilities, remote_config_status=remote_config_status, ) data = messages.encode_message(message) return data def build_full_state_message(self) -> bytes: message = messages.build_full_state_message( instance_uid=self._instance_uid, agent_description=self._agent_description, remote_config_status=self._remote_config_status, sequence_num=self._sequence_num, effective_config=self._effective_config, capabilities=self._capabilities, ) data = messages.encode_message(message) return data def send(self, data: bytes): token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) try: response = self._transport.send( url=self._endpoint, headers=self._headers, data=data, timeout_millis=self._timeout_millis, tls_certificate=self._tls_certificate, tls_client_certificate=self._tls_client_certificate, tls_client_key=self._tls_client_key, ) return response finally: self._sequence_num += 1 detach(token) @staticmethod def decode_remote_config( remote_config: opamp_pb2.AgentRemoteConfig, ) -> Generator[tuple[str, Mapping[str, AnyValue]]]: for config_file, config in messages.decode_remote_config(remote_config): yield config_file, config exceptions.py000066400000000000000000000003661527070072300355620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 class OpAMPTimeoutError(Exception): pass class OpAMPRemoteConfigParseException(Exception): pass class OpAMPRemoteConfigDecodeException(Exception): pass messages.py000066400000000000000000000144201527070072300352040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=no-name-in-module from __future__ import annotations import json from collections.abc import Generator, Mapping from logging import getLogger from typing import Any from opentelemetry._opamp.exceptions import ( OpAMPRemoteConfigDecodeException, OpAMPRemoteConfigParseException, ) from opentelemetry._opamp.proto import opamp_pb2 from opentelemetry._opamp.proto.anyvalue_pb2 import ( AnyValue as PB2AnyValue, ) from opentelemetry._opamp.proto.anyvalue_pb2 import ( KeyValue as PB2KeyValue, ) from opentelemetry.util.types import AnyValue _logger = getLogger(__name__) def decode_message(data: bytes) -> opamp_pb2.ServerToAgent: message = opamp_pb2.ServerToAgent() message.ParseFromString(data) return message def _encode_value(value: AnyValue) -> PB2AnyValue: if value is None: return PB2AnyValue() if isinstance(value, bool): return PB2AnyValue(bool_value=value) if isinstance(value, int): return PB2AnyValue(int_value=value) if isinstance(value, float): return PB2AnyValue(double_value=value) if isinstance(value, str): return PB2AnyValue(string_value=value) if isinstance(value, bytes): return PB2AnyValue(bytes_value=value) # TODO: handle sequence and mapping? raise ValueError(f"Invalid type {type(value)} of value {value}") def _encode_attributes(attributes: Mapping[str, AnyValue]): return [PB2KeyValue(key=key, value=_encode_value(value)) for key, value in attributes.items()] def build_agent_description( identifying_attributes: Mapping[str, AnyValue], non_identifying_attributes: Mapping[str, AnyValue] | None = None, ) -> opamp_pb2.AgentDescription: identifying_attrs = _encode_attributes(identifying_attributes) non_identifying_attrs = _encode_attributes(non_identifying_attributes) if non_identifying_attributes else None return opamp_pb2.AgentDescription( identifying_attributes=identifying_attrs, non_identifying_attributes=non_identifying_attrs, ) def build_heartbeat_message(instance_uid: bytes, sequence_num: int, capabilities: int) -> opamp_pb2.AgentToServer: command = opamp_pb2.AgentToServer( instance_uid=instance_uid, sequence_num=sequence_num, capabilities=capabilities, ) return command def build_agent_disconnect_message( instance_uid: bytes, sequence_num: int, capabilities: int ) -> opamp_pb2.AgentToServer: command = opamp_pb2.AgentToServer( instance_uid=instance_uid, sequence_num=sequence_num, agent_disconnect=opamp_pb2.AgentDisconnect(), capabilities=capabilities, ) return command def build_remote_config_status_message( last_remote_config_hash: bytes, status: opamp_pb2.RemoteConfigStatuses.ValueType, error_message: str = "", ) -> opamp_pb2.RemoteConfigStatus: return opamp_pb2.RemoteConfigStatus( last_remote_config_hash=last_remote_config_hash, status=status, error_message=error_message, ) def build_remote_config_status_response_message( instance_uid: bytes, sequence_num: int, capabilities: int, remote_config_status: opamp_pb2.RemoteConfigStatus, ) -> opamp_pb2.AgentToServer: command = opamp_pb2.AgentToServer( instance_uid=instance_uid, sequence_num=sequence_num, remote_config_status=remote_config_status, capabilities=capabilities, ) return command def build_effective_config_message(config: Mapping[str, Any], content_type: str): agent_config_map = opamp_pb2.AgentConfigMap() for filename, value in config.items(): body = encode_effective_config_body(content_type, value) if body is None: _logger.warning( "Skipping effective config entry %s with content type %s because the value cannot be encoded", filename, content_type, ) continue config_entry = agent_config_map.config_map[filename] config_entry.body = body config_entry.content_type = content_type return opamp_pb2.EffectiveConfig( config_map=agent_config_map, ) def encode_effective_config_body(content_type: str, value: Any) -> bytes | None: if content_type == "application/json": try: return json.dumps(value).encode("utf-8") except (TypeError, ValueError): _logger.warning( "Failed to encode effective config body as JSON", exc_info=True, ) return None if isinstance(value, str): return value.encode("utf-8") if isinstance(value, bytes): return value return None def build_full_state_message( instance_uid: bytes, sequence_num: int, agent_description: opamp_pb2.AgentDescription, capabilities: int, remote_config_status: opamp_pb2.RemoteConfigStatus | None, effective_config: opamp_pb2.EffectiveConfig | None, ) -> opamp_pb2.AgentToServer: command = opamp_pb2.AgentToServer( instance_uid=instance_uid, sequence_num=sequence_num, agent_description=agent_description, remote_config_status=remote_config_status, effective_config=effective_config, capabilities=capabilities, ) return command def encode_message(data: opamp_pb2.AgentToServer) -> bytes: return data.SerializeToString() def decode_remote_config( remote_config: opamp_pb2.AgentRemoteConfig, ) -> Generator[tuple[str, Mapping[str, AnyValue]]]: for ( config_file_name, config_file, ) in remote_config.config.config_map.items(): if config_file.content_type in ("application/json", "text/json"): try: body = config_file.body.decode() config_data = json.loads(body) except (UnicodeDecodeError, json.JSONDecodeError) as exc: raise OpAMPRemoteConfigDecodeException( f"Failed to decode {config_file_name} with content type {config_file.content_type}: {exc}" ) yield config_file_name, config_data else: raise OpAMPRemoteConfigParseException( f"Cannot parse {config_file_name} with content type {config_file.content_type}" ) opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/proto/000077500000000000000000000000001527070072300342445ustar00rootroot00000000000000anyvalue_pb2.py000066400000000000000000000041401527070072300371250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/proto# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: anyvalue.proto # Protobuf Python Version: 5.26.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0e\x61nyvalue.proto\x12\x0bopamp.proto\"\xe8\x01\n\x08\x41nyValue\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12.\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32\x17.opamp.proto.ArrayValueH\x00\x12\x31\n\x0ckvlist_value\x18\x06 \x01(\x0b\x32\x19.opamp.proto.KeyValueListH\x00\x12\x15\n\x0b\x62ytes_value\x18\x07 \x01(\x0cH\x00\x42\x07\n\x05value\"3\n\nArrayValue\x12%\n\x06values\x18\x01 \x03(\x0b\x32\x15.opamp.proto.AnyValue\"5\n\x0cKeyValueList\x12%\n\x06values\x18\x01 \x03(\x0b\x32\x15.opamp.proto.KeyValue\"=\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b\x32\x15.opamp.proto.AnyValueB.Z,github.com/open-telemetry/opamp-go/protobufsb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'anyvalue_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'Z,github.com/open-telemetry/opamp-go/protobufs' _globals['_ANYVALUE']._serialized_start=32 _globals['_ANYVALUE']._serialized_end=264 _globals['_ARRAYVALUE']._serialized_start=266 _globals['_ARRAYVALUE']._serialized_end=317 _globals['_KEYVALUELIST']._serialized_start=319 _globals['_KEYVALUELIST']._serialized_end=372 _globals['_KEYVALUE']._serialized_start=374 _globals['_KEYVALUE']._serialized_end=435 # @@protoc_insertion_point(module_scope) anyvalue_pb2.pyi000066400000000000000000000130021527070072300372730ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/proto""" @generated by mypy-protobuf. Do not edit manually! isort:skip_file This file is copied and modified from https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/common/v1/common.proto Modifications: - Removal of unneeded InstrumentationLibrary and StringKeyValue messages. - Change of go_package to reference a package in this repo. - Removal of gogoproto usage. """ import builtins import collections.abc import google.protobuf.descriptor import google.protobuf.internal.containers import google.protobuf.message import sys if sys.version_info >= (3, 8): import typing as typing_extensions else: import typing_extensions DESCRIPTOR: google.protobuf.descriptor.FileDescriptor @typing_extensions.final class AnyValue(google.protobuf.message.Message): """AnyValue is used to represent any type of attribute value. AnyValue may contain a primitive value such as a string or integer or it may contain an arbitrary nested object containing arrays, key-value lists and primitives. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor STRING_VALUE_FIELD_NUMBER: builtins.int BOOL_VALUE_FIELD_NUMBER: builtins.int INT_VALUE_FIELD_NUMBER: builtins.int DOUBLE_VALUE_FIELD_NUMBER: builtins.int ARRAY_VALUE_FIELD_NUMBER: builtins.int KVLIST_VALUE_FIELD_NUMBER: builtins.int BYTES_VALUE_FIELD_NUMBER: builtins.int string_value: builtins.str bool_value: builtins.bool int_value: builtins.int double_value: builtins.float @property def array_value(self) -> global___ArrayValue: ... @property def kvlist_value(self) -> global___KeyValueList: ... bytes_value: builtins.bytes def __init__( self, *, string_value: builtins.str = ..., bool_value: builtins.bool = ..., int_value: builtins.int = ..., double_value: builtins.float = ..., array_value: global___ArrayValue | None = ..., kvlist_value: global___KeyValueList | None = ..., bytes_value: builtins.bytes = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["array_value", b"array_value", "bool_value", b"bool_value", "bytes_value", b"bytes_value", "double_value", b"double_value", "int_value", b"int_value", "kvlist_value", b"kvlist_value", "string_value", b"string_value", "value", b"value"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["array_value", b"array_value", "bool_value", b"bool_value", "bytes_value", b"bytes_value", "double_value", b"double_value", "int_value", b"int_value", "kvlist_value", b"kvlist_value", "string_value", b"string_value", "value", b"value"]) -> None: ... def WhichOneof(self, oneof_group: typing_extensions.Literal["value", b"value"]) -> typing_extensions.Literal["string_value", "bool_value", "int_value", "double_value", "array_value", "kvlist_value", "bytes_value"] | None: ... global___AnyValue = AnyValue @typing_extensions.final class ArrayValue(google.protobuf.message.Message): """ArrayValue is a list of AnyValue messages. We need ArrayValue as a message since oneof in AnyValue does not allow repeated fields. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor VALUES_FIELD_NUMBER: builtins.int @property def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___AnyValue]: """Array of values. The array may be empty (contain 0 elements).""" def __init__( self, *, values: collections.abc.Iterable[global___AnyValue] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["values", b"values"]) -> None: ... global___ArrayValue = ArrayValue @typing_extensions.final class KeyValueList(google.protobuf.message.Message): """KeyValueList is a list of KeyValue messages. We need KeyValueList as a message since `oneof` in AnyValue does not allow repeated fields. Everywhere else where we need a list of KeyValue messages (e.g. in Span) we use `repeated KeyValue` directly to avoid unnecessary extra wrapping (which slows down the protocol). The 2 approaches are semantically equivalent. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor VALUES_FIELD_NUMBER: builtins.int @property def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___KeyValue]: """A collection of key/value pairs of key-value pairs. The list may be empty (may contain 0 elements). """ def __init__( self, *, values: collections.abc.Iterable[global___KeyValue] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["values", b"values"]) -> None: ... global___KeyValueList = KeyValueList @typing_extensions.final class KeyValue(google.protobuf.message.Message): """KeyValue is a key-value pair that is used to store Span attributes, Link attributes, etc. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor KEY_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int key: builtins.str @property def value(self) -> global___AnyValue: ... def __init__( self, *, key: builtins.str = ..., value: global___AnyValue | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... global___KeyValue = KeyValue opamp_pb2.py000066400000000000000000000465351527070072300364330ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/proto# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opamp.proto # Protobuf Python Version: 5.26.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() from . import anyvalue_pb2 as anyvalue__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bopamp.proto\x12\x0bopamp.proto\x1a\x0e\x61nyvalue.proto\"\xae\x05\n\rAgentToServer\x12\x14\n\x0cinstance_uid\x18\x01 \x01(\x0c\x12\x14\n\x0csequence_num\x18\x02 \x01(\x04\x12\x38\n\x11\x61gent_description\x18\x03 \x01(\x0b\x32\x1d.opamp.proto.AgentDescription\x12\x14\n\x0c\x63\x61pabilities\x18\x04 \x01(\x04\x12,\n\x06health\x18\x05 \x01(\x0b\x32\x1c.opamp.proto.ComponentHealth\x12\x36\n\x10\x65\x66\x66\x65\x63tive_config\x18\x06 \x01(\x0b\x32\x1c.opamp.proto.EffectiveConfig\x12=\n\x14remote_config_status\x18\x07 \x01(\x0b\x32\x1f.opamp.proto.RemoteConfigStatus\x12\x36\n\x10package_statuses\x18\x08 \x01(\x0b\x32\x1c.opamp.proto.PackageStatuses\x12\x36\n\x10\x61gent_disconnect\x18\t \x01(\x0b\x32\x1c.opamp.proto.AgentDisconnect\x12\r\n\x05\x66lags\x18\n \x01(\x04\x12K\n\x1b\x63onnection_settings_request\x18\x0b \x01(\x0b\x32&.opamp.proto.ConnectionSettingsRequest\x12<\n\x13\x63ustom_capabilities\x18\x0c \x01(\x0b\x32\x1f.opamp.proto.CustomCapabilities\x12\x32\n\x0e\x63ustom_message\x18\r \x01(\x0b\x32\x1a.opamp.proto.CustomMessage\x12>\n\x14\x61vailable_components\x18\x0e \x01(\x0b\x32 .opamp.proto.AvailableComponents\"\x11\n\x0f\x41gentDisconnect\"W\n\x19\x43onnectionSettingsRequest\x12:\n\x05opamp\x18\x01 \x01(\x0b\x32+.opamp.proto.OpAMPConnectionSettingsRequest\"^\n\x1eOpAMPConnectionSettingsRequest\x12<\n\x13\x63\x65rtificate_request\x18\x01 \x01(\x0b\x32\x1f.opamp.proto.CertificateRequest\"!\n\x12\x43\x65rtificateRequest\x12\x0b\n\x03\x63sr\x18\x01 \x01(\x0c\"\xbb\x01\n\x13\x41vailableComponents\x12\x44\n\ncomponents\x18\x01 \x03(\x0b\x32\x30.opamp.proto.AvailableComponents.ComponentsEntry\x12\x0c\n\x04hash\x18\x02 \x01(\x0c\x1aP\n\x0f\x43omponentsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12,\n\x05value\x18\x02 \x01(\x0b\x32\x1d.opamp.proto.ComponentDetails:\x02\x38\x01\"\xe1\x01\n\x10\x43omponentDetails\x12\'\n\x08metadata\x18\x01 \x03(\x0b\x32\x15.opamp.proto.KeyValue\x12M\n\x11sub_component_map\x18\x02 \x03(\x0b\x32\x32.opamp.proto.ComponentDetails.SubComponentMapEntry\x1aU\n\x14SubComponentMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12,\n\x05value\x18\x02 \x01(\x0b\x32\x1d.opamp.proto.ComponentDetails:\x02\x38\x01\"\xa1\x04\n\rServerToAgent\x12\x14\n\x0cinstance_uid\x18\x01 \x01(\x0c\x12\x38\n\x0e\x65rror_response\x18\x02 \x01(\x0b\x32 .opamp.proto.ServerErrorResponse\x12\x35\n\rremote_config\x18\x03 \x01(\x0b\x32\x1e.opamp.proto.AgentRemoteConfig\x12\x42\n\x13\x63onnection_settings\x18\x04 \x01(\x0b\x32%.opamp.proto.ConnectionSettingsOffers\x12:\n\x12packages_available\x18\x05 \x01(\x0b\x32\x1e.opamp.proto.PackagesAvailable\x12\r\n\x05\x66lags\x18\x06 \x01(\x04\x12\x14\n\x0c\x63\x61pabilities\x18\x07 \x01(\x04\x12>\n\x14\x61gent_identification\x18\x08 \x01(\x0b\x32 .opamp.proto.AgentIdentification\x12\x32\n\x07\x63ommand\x18\t \x01(\x0b\x32!.opamp.proto.ServerToAgentCommand\x12<\n\x13\x63ustom_capabilities\x18\n \x01(\x0b\x32\x1f.opamp.proto.CustomCapabilities\x12\x32\n\x0e\x63ustom_message\x18\x0b \x01(\x0b\x32\x1a.opamp.proto.CustomMessage\"\xb4\x01\n\x17OpAMPConnectionSettings\x12\x1c\n\x14\x64\x65stination_endpoint\x18\x01 \x01(\t\x12%\n\x07headers\x18\x02 \x01(\x0b\x32\x14.opamp.proto.Headers\x12\x30\n\x0b\x63\x65rtificate\x18\x03 \x01(\x0b\x32\x1b.opamp.proto.TLSCertificate\x12\"\n\x1aheartbeat_interval_seconds\x18\x04 \x01(\x04\"\x94\x01\n\x1bTelemetryConnectionSettings\x12\x1c\n\x14\x64\x65stination_endpoint\x18\x01 \x01(\t\x12%\n\x07headers\x18\x02 \x01(\x0b\x32\x14.opamp.proto.Headers\x12\x30\n\x0b\x63\x65rtificate\x18\x03 \x01(\x0b\x32\x1b.opamp.proto.TLSCertificate\"\x97\x02\n\x17OtherConnectionSettings\x12\x1c\n\x14\x64\x65stination_endpoint\x18\x01 \x01(\t\x12%\n\x07headers\x18\x02 \x01(\x0b\x32\x14.opamp.proto.Headers\x12\x30\n\x0b\x63\x65rtificate\x18\x03 \x01(\x0b\x32\x1b.opamp.proto.TLSCertificate\x12O\n\x0eother_settings\x18\x04 \x03(\x0b\x32\x37.opamp.proto.OtherConnectionSettings.OtherSettingsEntry\x1a\x34\n\x12OtherSettingsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"/\n\x07Headers\x12$\n\x07headers\x18\x01 \x03(\x0b\x32\x13.opamp.proto.Header\"$\n\x06Header\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"D\n\x0eTLSCertificate\x12\x0c\n\x04\x63\x65rt\x18\x01 \x01(\x0c\x12\x13\n\x0bprivate_key\x18\x02 \x01(\x0c\x12\x0f\n\x07\x63\x61_cert\x18\x03 \x01(\x0c\"\xcd\x03\n\x18\x43onnectionSettingsOffers\x12\x0c\n\x04hash\x18\x01 \x01(\x0c\x12\x33\n\x05opamp\x18\x02 \x01(\x0b\x32$.opamp.proto.OpAMPConnectionSettings\x12=\n\x0bown_metrics\x18\x03 \x01(\x0b\x32(.opamp.proto.TelemetryConnectionSettings\x12<\n\nown_traces\x18\x04 \x01(\x0b\x32(.opamp.proto.TelemetryConnectionSettings\x12:\n\x08own_logs\x18\x05 \x01(\x0b\x32(.opamp.proto.TelemetryConnectionSettings\x12V\n\x11other_connections\x18\x06 \x03(\x0b\x32;.opamp.proto.ConnectionSettingsOffers.OtherConnectionsEntry\x1a]\n\x15OtherConnectionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x33\n\x05value\x18\x02 \x01(\x0b\x32$.opamp.proto.OtherConnectionSettings:\x02\x38\x01\"\xbe\x01\n\x11PackagesAvailable\x12>\n\x08packages\x18\x01 \x03(\x0b\x32,.opamp.proto.PackagesAvailable.PackagesEntry\x12\x19\n\x11\x61ll_packages_hash\x18\x02 \x01(\x0c\x1aN\n\rPackagesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12,\n\x05value\x18\x02 \x01(\x0b\x32\x1d.opamp.proto.PackageAvailable:\x02\x38\x01\"\x86\x01\n\x10PackageAvailable\x12&\n\x04type\x18\x01 \x01(\x0e\x32\x18.opamp.proto.PackageType\x12\x0f\n\x07version\x18\x02 \x01(\t\x12+\n\x04\x66ile\x18\x03 \x01(\x0b\x32\x1d.opamp.proto.DownloadableFile\x12\x0c\n\x04hash\x18\x04 \x01(\x0c\"x\n\x10\x44ownloadableFile\x12\x14\n\x0c\x64ownload_url\x18\x01 \x01(\t\x12\x14\n\x0c\x63ontent_hash\x18\x02 \x01(\x0c\x12\x11\n\tsignature\x18\x03 \x01(\x0c\x12%\n\x07headers\x18\x04 \x01(\x0b\x32\x14.opamp.proto.Headers\"\x99\x01\n\x13ServerErrorResponse\x12\x32\n\x04type\x18\x01 \x01(\x0e\x32$.opamp.proto.ServerErrorResponseType\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12,\n\nretry_info\x18\x03 \x01(\x0b\x32\x16.opamp.proto.RetryInfoH\x00\x42\t\n\x07\x44\x65tails\",\n\tRetryInfo\x12\x1f\n\x17retry_after_nanoseconds\x18\x01 \x01(\x04\">\n\x14ServerToAgentCommand\x12&\n\x04type\x18\x01 \x01(\x0e\x32\x18.opamp.proto.CommandType\"\x84\x01\n\x10\x41gentDescription\x12\x35\n\x16identifying_attributes\x18\x01 \x03(\x0b\x32\x15.opamp.proto.KeyValue\x12\x39\n\x1anon_identifying_attributes\x18\x02 \x03(\x0b\x32\x15.opamp.proto.KeyValue\"\xb0\x02\n\x0f\x43omponentHealth\x12\x0f\n\x07healthy\x18\x01 \x01(\x08\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x12\n\nlast_error\x18\x03 \x01(\t\x12\x0e\n\x06status\x18\x04 \x01(\t\x12\x1d\n\x15status_time_unix_nano\x18\x05 \x01(\x06\x12R\n\x14\x63omponent_health_map\x18\x06 \x03(\x0b\x32\x34.opamp.proto.ComponentHealth.ComponentHealthMapEntry\x1aW\n\x17\x43omponentHealthMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12+\n\x05value\x18\x02 \x01(\x0b\x32\x1c.opamp.proto.ComponentHealth:\x02\x38\x01\"B\n\x0f\x45\x66\x66\x65\x63tiveConfig\x12/\n\nconfig_map\x18\x01 \x01(\x0b\x32\x1b.opamp.proto.AgentConfigMap\"\x7f\n\x12RemoteConfigStatus\x12\x1f\n\x17last_remote_config_hash\x18\x01 \x01(\x0c\x12\x31\n\x06status\x18\x02 \x01(\x0e\x32!.opamp.proto.RemoteConfigStatuses\x12\x15\n\rerror_message\x18\x03 \x01(\t\"\xde\x01\n\x0fPackageStatuses\x12<\n\x08packages\x18\x01 \x03(\x0b\x32*.opamp.proto.PackageStatuses.PackagesEntry\x12)\n!server_provided_all_packages_hash\x18\x02 \x01(\x0c\x12\x15\n\rerror_message\x18\x03 \x01(\t\x1aK\n\rPackagesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.opamp.proto.PackageStatus:\x02\x38\x01\"\x93\x02\n\rPackageStatus\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x19\n\x11\x61gent_has_version\x18\x02 \x01(\t\x12\x16\n\x0e\x61gent_has_hash\x18\x03 \x01(\x0c\x12\x1e\n\x16server_offered_version\x18\x04 \x01(\t\x12\x1b\n\x13server_offered_hash\x18\x05 \x01(\x0c\x12.\n\x06status\x18\x06 \x01(\x0e\x32\x1e.opamp.proto.PackageStatusEnum\x12\x15\n\rerror_message\x18\x07 \x01(\t\x12=\n\x10\x64ownload_details\x18\x08 \x01(\x0b\x32#.opamp.proto.PackageDownloadDetails\"U\n\x16PackageDownloadDetails\x12\x18\n\x10\x64ownload_percent\x18\x01 \x01(\x01\x12!\n\x19\x64ownload_bytes_per_second\x18\x02 \x01(\x01\"/\n\x13\x41gentIdentification\x12\x18\n\x10new_instance_uid\x18\x01 \x01(\x0c\"U\n\x11\x41gentRemoteConfig\x12+\n\x06\x63onfig\x18\x01 \x01(\x0b\x32\x1b.opamp.proto.AgentConfigMap\x12\x13\n\x0b\x63onfig_hash\x18\x02 \x01(\x0c\"\xa0\x01\n\x0e\x41gentConfigMap\x12>\n\nconfig_map\x18\x01 \x03(\x0b\x32*.opamp.proto.AgentConfigMap.ConfigMapEntry\x1aN\n\x0e\x43onfigMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12+\n\x05value\x18\x02 \x01(\x0b\x32\x1c.opamp.proto.AgentConfigFile:\x02\x38\x01\"5\n\x0f\x41gentConfigFile\x12\x0c\n\x04\x62ody\x18\x01 \x01(\x0c\x12\x14\n\x0c\x63ontent_type\x18\x02 \x01(\t\"*\n\x12\x43ustomCapabilities\x12\x14\n\x0c\x63\x61pabilities\x18\x01 \x03(\t\"?\n\rCustomMessage\x12\x12\n\ncapability\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c*c\n\x12\x41gentToServerFlags\x12\"\n\x1e\x41gentToServerFlags_Unspecified\x10\x00\x12)\n%AgentToServerFlags_RequestInstanceUid\x10\x01*\x92\x01\n\x12ServerToAgentFlags\x12\"\n\x1eServerToAgentFlags_Unspecified\x10\x00\x12&\n\"ServerToAgentFlags_ReportFullState\x10\x01\x12\x30\n,ServerToAgentFlags_ReportAvailableComponents\x10\x02*\xf7\x02\n\x12ServerCapabilities\x12\"\n\x1eServerCapabilities_Unspecified\x10\x00\x12$\n ServerCapabilities_AcceptsStatus\x10\x01\x12)\n%ServerCapabilities_OffersRemoteConfig\x10\x02\x12-\n)ServerCapabilities_AcceptsEffectiveConfig\x10\x04\x12%\n!ServerCapabilities_OffersPackages\x10\x08\x12,\n(ServerCapabilities_AcceptsPackagesStatus\x10\x10\x12/\n+ServerCapabilities_OffersConnectionSettings\x10 \x12\x37\n3ServerCapabilities_AcceptsConnectionSettingsRequest\x10@*>\n\x0bPackageType\x12\x18\n\x14PackageType_TopLevel\x10\x00\x12\x15\n\x11PackageType_Addon\x10\x01*\x8f\x01\n\x17ServerErrorResponseType\x12#\n\x1fServerErrorResponseType_Unknown\x10\x00\x12&\n\"ServerErrorResponseType_BadRequest\x10\x01\x12\'\n#ServerErrorResponseType_Unavailable\x10\x02*&\n\x0b\x43ommandType\x12\x17\n\x13\x43ommandType_Restart\x10\x00*\xcc\x05\n\x11\x41gentCapabilities\x12!\n\x1d\x41gentCapabilities_Unspecified\x10\x00\x12#\n\x1f\x41gentCapabilities_ReportsStatus\x10\x01\x12)\n%AgentCapabilities_AcceptsRemoteConfig\x10\x02\x12,\n(AgentCapabilities_ReportsEffectiveConfig\x10\x04\x12%\n!AgentCapabilities_AcceptsPackages\x10\x08\x12,\n(AgentCapabilities_ReportsPackageStatuses\x10\x10\x12&\n\"AgentCapabilities_ReportsOwnTraces\x10 \x12\'\n#AgentCapabilities_ReportsOwnMetrics\x10@\x12%\n AgentCapabilities_ReportsOwnLogs\x10\x80\x01\x12\x35\n0AgentCapabilities_AcceptsOpAMPConnectionSettings\x10\x80\x02\x12\x35\n0AgentCapabilities_AcceptsOtherConnectionSettings\x10\x80\x04\x12,\n\'AgentCapabilities_AcceptsRestartCommand\x10\x80\x08\x12$\n\x1f\x41gentCapabilities_ReportsHealth\x10\x80\x10\x12*\n%AgentCapabilities_ReportsRemoteConfig\x10\x80 \x12\'\n\"AgentCapabilities_ReportsHeartbeat\x10\x80@\x12\x32\n,AgentCapabilities_ReportsAvailableComponents\x10\x80\x80\x01*\x9c\x01\n\x14RemoteConfigStatuses\x12\x1e\n\x1aRemoteConfigStatuses_UNSET\x10\x00\x12 \n\x1cRemoteConfigStatuses_APPLIED\x10\x01\x12!\n\x1dRemoteConfigStatuses_APPLYING\x10\x02\x12\x1f\n\x1bRemoteConfigStatuses_FAILED\x10\x03*\xc4\x01\n\x11PackageStatusEnum\x12\x1f\n\x1bPackageStatusEnum_Installed\x10\x00\x12$\n PackageStatusEnum_InstallPending\x10\x01\x12 \n\x1cPackageStatusEnum_Installing\x10\x02\x12#\n\x1fPackageStatusEnum_InstallFailed\x10\x03\x12!\n\x1dPackageStatusEnum_Downloading\x10\x04\x42.Z,github.com/open-telemetry/opamp-go/protobufsb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opamp_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'Z,github.com/open-telemetry/opamp-go/protobufs' _globals['_AVAILABLECOMPONENTS_COMPONENTSENTRY']._loaded_options = None _globals['_AVAILABLECOMPONENTS_COMPONENTSENTRY']._serialized_options = b'8\001' _globals['_COMPONENTDETAILS_SUBCOMPONENTMAPENTRY']._loaded_options = None _globals['_COMPONENTDETAILS_SUBCOMPONENTMAPENTRY']._serialized_options = b'8\001' _globals['_OTHERCONNECTIONSETTINGS_OTHERSETTINGSENTRY']._loaded_options = None _globals['_OTHERCONNECTIONSETTINGS_OTHERSETTINGSENTRY']._serialized_options = b'8\001' _globals['_CONNECTIONSETTINGSOFFERS_OTHERCONNECTIONSENTRY']._loaded_options = None _globals['_CONNECTIONSETTINGSOFFERS_OTHERCONNECTIONSENTRY']._serialized_options = b'8\001' _globals['_PACKAGESAVAILABLE_PACKAGESENTRY']._loaded_options = None _globals['_PACKAGESAVAILABLE_PACKAGESENTRY']._serialized_options = b'8\001' _globals['_COMPONENTHEALTH_COMPONENTHEALTHMAPENTRY']._loaded_options = None _globals['_COMPONENTHEALTH_COMPONENTHEALTHMAPENTRY']._serialized_options = b'8\001' _globals['_PACKAGESTATUSES_PACKAGESENTRY']._loaded_options = None _globals['_PACKAGESTATUSES_PACKAGESENTRY']._serialized_options = b'8\001' _globals['_AGENTCONFIGMAP_CONFIGMAPENTRY']._loaded_options = None _globals['_AGENTCONFIGMAP_CONFIGMAPENTRY']._serialized_options = b'8\001' _globals['_AGENTTOSERVERFLAGS']._serialized_start=5585 _globals['_AGENTTOSERVERFLAGS']._serialized_end=5684 _globals['_SERVERTOAGENTFLAGS']._serialized_start=5687 _globals['_SERVERTOAGENTFLAGS']._serialized_end=5833 _globals['_SERVERCAPABILITIES']._serialized_start=5836 _globals['_SERVERCAPABILITIES']._serialized_end=6211 _globals['_PACKAGETYPE']._serialized_start=6213 _globals['_PACKAGETYPE']._serialized_end=6275 _globals['_SERVERERRORRESPONSETYPE']._serialized_start=6278 _globals['_SERVERERRORRESPONSETYPE']._serialized_end=6421 _globals['_COMMANDTYPE']._serialized_start=6423 _globals['_COMMANDTYPE']._serialized_end=6461 _globals['_AGENTCAPABILITIES']._serialized_start=6464 _globals['_AGENTCAPABILITIES']._serialized_end=7180 _globals['_REMOTECONFIGSTATUSES']._serialized_start=7183 _globals['_REMOTECONFIGSTATUSES']._serialized_end=7339 _globals['_PACKAGESTATUSENUM']._serialized_start=7342 _globals['_PACKAGESTATUSENUM']._serialized_end=7538 _globals['_AGENTTOSERVER']._serialized_start=45 _globals['_AGENTTOSERVER']._serialized_end=731 _globals['_AGENTDISCONNECT']._serialized_start=733 _globals['_AGENTDISCONNECT']._serialized_end=750 _globals['_CONNECTIONSETTINGSREQUEST']._serialized_start=752 _globals['_CONNECTIONSETTINGSREQUEST']._serialized_end=839 _globals['_OPAMPCONNECTIONSETTINGSREQUEST']._serialized_start=841 _globals['_OPAMPCONNECTIONSETTINGSREQUEST']._serialized_end=935 _globals['_CERTIFICATEREQUEST']._serialized_start=937 _globals['_CERTIFICATEREQUEST']._serialized_end=970 _globals['_AVAILABLECOMPONENTS']._serialized_start=973 _globals['_AVAILABLECOMPONENTS']._serialized_end=1160 _globals['_AVAILABLECOMPONENTS_COMPONENTSENTRY']._serialized_start=1080 _globals['_AVAILABLECOMPONENTS_COMPONENTSENTRY']._serialized_end=1160 _globals['_COMPONENTDETAILS']._serialized_start=1163 _globals['_COMPONENTDETAILS']._serialized_end=1388 _globals['_COMPONENTDETAILS_SUBCOMPONENTMAPENTRY']._serialized_start=1303 _globals['_COMPONENTDETAILS_SUBCOMPONENTMAPENTRY']._serialized_end=1388 _globals['_SERVERTOAGENT']._serialized_start=1391 _globals['_SERVERTOAGENT']._serialized_end=1936 _globals['_OPAMPCONNECTIONSETTINGS']._serialized_start=1939 _globals['_OPAMPCONNECTIONSETTINGS']._serialized_end=2119 _globals['_TELEMETRYCONNECTIONSETTINGS']._serialized_start=2122 _globals['_TELEMETRYCONNECTIONSETTINGS']._serialized_end=2270 _globals['_OTHERCONNECTIONSETTINGS']._serialized_start=2273 _globals['_OTHERCONNECTIONSETTINGS']._serialized_end=2552 _globals['_OTHERCONNECTIONSETTINGS_OTHERSETTINGSENTRY']._serialized_start=2500 _globals['_OTHERCONNECTIONSETTINGS_OTHERSETTINGSENTRY']._serialized_end=2552 _globals['_HEADERS']._serialized_start=2554 _globals['_HEADERS']._serialized_end=2601 _globals['_HEADER']._serialized_start=2603 _globals['_HEADER']._serialized_end=2639 _globals['_TLSCERTIFICATE']._serialized_start=2641 _globals['_TLSCERTIFICATE']._serialized_end=2709 _globals['_CONNECTIONSETTINGSOFFERS']._serialized_start=2712 _globals['_CONNECTIONSETTINGSOFFERS']._serialized_end=3173 _globals['_CONNECTIONSETTINGSOFFERS_OTHERCONNECTIONSENTRY']._serialized_start=3080 _globals['_CONNECTIONSETTINGSOFFERS_OTHERCONNECTIONSENTRY']._serialized_end=3173 _globals['_PACKAGESAVAILABLE']._serialized_start=3176 _globals['_PACKAGESAVAILABLE']._serialized_end=3366 _globals['_PACKAGESAVAILABLE_PACKAGESENTRY']._serialized_start=3288 _globals['_PACKAGESAVAILABLE_PACKAGESENTRY']._serialized_end=3366 _globals['_PACKAGEAVAILABLE']._serialized_start=3369 _globals['_PACKAGEAVAILABLE']._serialized_end=3503 _globals['_DOWNLOADABLEFILE']._serialized_start=3505 _globals['_DOWNLOADABLEFILE']._serialized_end=3625 _globals['_SERVERERRORRESPONSE']._serialized_start=3628 _globals['_SERVERERRORRESPONSE']._serialized_end=3781 _globals['_RETRYINFO']._serialized_start=3783 _globals['_RETRYINFO']._serialized_end=3827 _globals['_SERVERTOAGENTCOMMAND']._serialized_start=3829 _globals['_SERVERTOAGENTCOMMAND']._serialized_end=3891 _globals['_AGENTDESCRIPTION']._serialized_start=3894 _globals['_AGENTDESCRIPTION']._serialized_end=4026 _globals['_COMPONENTHEALTH']._serialized_start=4029 _globals['_COMPONENTHEALTH']._serialized_end=4333 _globals['_COMPONENTHEALTH_COMPONENTHEALTHMAPENTRY']._serialized_start=4246 _globals['_COMPONENTHEALTH_COMPONENTHEALTHMAPENTRY']._serialized_end=4333 _globals['_EFFECTIVECONFIG']._serialized_start=4335 _globals['_EFFECTIVECONFIG']._serialized_end=4401 _globals['_REMOTECONFIGSTATUS']._serialized_start=4403 _globals['_REMOTECONFIGSTATUS']._serialized_end=4530 _globals['_PACKAGESTATUSES']._serialized_start=4533 _globals['_PACKAGESTATUSES']._serialized_end=4755 _globals['_PACKAGESTATUSES_PACKAGESENTRY']._serialized_start=4680 _globals['_PACKAGESTATUSES_PACKAGESENTRY']._serialized_end=4755 _globals['_PACKAGESTATUS']._serialized_start=4758 _globals['_PACKAGESTATUS']._serialized_end=5033 _globals['_PACKAGEDOWNLOADDETAILS']._serialized_start=5035 _globals['_PACKAGEDOWNLOADDETAILS']._serialized_end=5120 _globals['_AGENTIDENTIFICATION']._serialized_start=5122 _globals['_AGENTIDENTIFICATION']._serialized_end=5169 _globals['_AGENTREMOTECONFIG']._serialized_start=5171 _globals['_AGENTREMOTECONFIG']._serialized_end=5256 _globals['_AGENTCONFIGMAP']._serialized_start=5259 _globals['_AGENTCONFIGMAP']._serialized_end=5419 _globals['_AGENTCONFIGMAP_CONFIGMAPENTRY']._serialized_start=5341 _globals['_AGENTCONFIGMAP_CONFIGMAPENTRY']._serialized_end=5419 _globals['_AGENTCONFIGFILE']._serialized_start=5421 _globals['_AGENTCONFIGFILE']._serialized_end=5474 _globals['_CUSTOMCAPABILITIES']._serialized_start=5476 _globals['_CUSTOMCAPABILITIES']._serialized_end=5518 _globals['_CUSTOMMESSAGE']._serialized_start=5520 _globals['_CUSTOMMESSAGE']._serialized_end=5583 # @@protoc_insertion_point(module_scope) opamp_pb2.pyi000066400000000000000000002627231527070072300366030ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/proto""" @generated by mypy-protobuf. Do not edit manually! isort:skip_file OpAMP: Open Agent Management Protocol (https://github.com/open-telemetry/opamp-spec)""" import anyvalue_pb2 import builtins import collections.abc import google.protobuf.descriptor import google.protobuf.internal.containers import google.protobuf.internal.enum_type_wrapper import google.protobuf.message import sys import typing if sys.version_info >= (3, 10): import typing as typing_extensions else: import typing_extensions DESCRIPTOR: google.protobuf.descriptor.FileDescriptor class _AgentToServerFlags: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType class _AgentToServerFlagsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_AgentToServerFlags.ValueType], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor AgentToServerFlags_Unspecified: _AgentToServerFlags.ValueType # 0 AgentToServerFlags_RequestInstanceUid: _AgentToServerFlags.ValueType # 1 """AgentToServerFlags is a bit mask. Values below define individual bits. The Agent requests Server go generate a new instance_uid, which will be sent back in ServerToAgent message """ class AgentToServerFlags(_AgentToServerFlags, metaclass=_AgentToServerFlagsEnumTypeWrapper): ... AgentToServerFlags_Unspecified: AgentToServerFlags.ValueType # 0 AgentToServerFlags_RequestInstanceUid: AgentToServerFlags.ValueType # 1 """AgentToServerFlags is a bit mask. Values below define individual bits. The Agent requests Server go generate a new instance_uid, which will be sent back in ServerToAgent message """ global___AgentToServerFlags = AgentToServerFlags class _ServerToAgentFlags: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType class _ServerToAgentFlagsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_ServerToAgentFlags.ValueType], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor ServerToAgentFlags_Unspecified: _ServerToAgentFlags.ValueType # 0 ServerToAgentFlags_ReportFullState: _ServerToAgentFlags.ValueType # 1 """Flags is a bit mask. Values below define individual bits. ReportFullState flag can be used by the Server if the Agent did not include the particular bit of information in the last status report (which is an allowed optimization) but the Server detects that it does not have it (e.g. was restarted and lost state). The detection happens using AgentToServer.sequence_num values. The Server asks the Agent to report full status. """ ServerToAgentFlags_ReportAvailableComponents: _ServerToAgentFlags.ValueType # 2 """ReportAvailableComponents flag can be used by the server if the Agent did not include the full AvailableComponents message, but only the hash. If this flag is specified, the agent will populate available_components.components with a full description of the agent's components. Status: [Development] """ class ServerToAgentFlags(_ServerToAgentFlags, metaclass=_ServerToAgentFlagsEnumTypeWrapper): ... ServerToAgentFlags_Unspecified: ServerToAgentFlags.ValueType # 0 ServerToAgentFlags_ReportFullState: ServerToAgentFlags.ValueType # 1 """Flags is a bit mask. Values below define individual bits. ReportFullState flag can be used by the Server if the Agent did not include the particular bit of information in the last status report (which is an allowed optimization) but the Server detects that it does not have it (e.g. was restarted and lost state). The detection happens using AgentToServer.sequence_num values. The Server asks the Agent to report full status. """ ServerToAgentFlags_ReportAvailableComponents: ServerToAgentFlags.ValueType # 2 """ReportAvailableComponents flag can be used by the server if the Agent did not include the full AvailableComponents message, but only the hash. If this flag is specified, the agent will populate available_components.components with a full description of the agent's components. Status: [Development] """ global___ServerToAgentFlags = ServerToAgentFlags class _ServerCapabilities: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType class _ServerCapabilitiesEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_ServerCapabilities.ValueType], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor ServerCapabilities_Unspecified: _ServerCapabilities.ValueType # 0 """The capabilities field is unspecified.""" ServerCapabilities_AcceptsStatus: _ServerCapabilities.ValueType # 1 """The Server can accept status reports. This bit MUST be set, since all Server MUST be able to accept status reports. """ ServerCapabilities_OffersRemoteConfig: _ServerCapabilities.ValueType # 2 """The Server can offer remote configuration to the Agent.""" ServerCapabilities_AcceptsEffectiveConfig: _ServerCapabilities.ValueType # 4 """The Server can accept EffectiveConfig in AgentToServer.""" ServerCapabilities_OffersPackages: _ServerCapabilities.ValueType # 8 """The Server can offer Packages. Status: [Beta] """ ServerCapabilities_AcceptsPackagesStatus: _ServerCapabilities.ValueType # 16 """The Server can accept Packages status. Status: [Beta] """ ServerCapabilities_OffersConnectionSettings: _ServerCapabilities.ValueType # 32 """The Server can offer connection settings. Status: [Beta] """ ServerCapabilities_AcceptsConnectionSettingsRequest: _ServerCapabilities.ValueType # 64 """The Server can accept ConnectionSettingsRequest and respond with an offer. Status: [Development] """ class ServerCapabilities(_ServerCapabilities, metaclass=_ServerCapabilitiesEnumTypeWrapper): ... ServerCapabilities_Unspecified: ServerCapabilities.ValueType # 0 """The capabilities field is unspecified.""" ServerCapabilities_AcceptsStatus: ServerCapabilities.ValueType # 1 """The Server can accept status reports. This bit MUST be set, since all Server MUST be able to accept status reports. """ ServerCapabilities_OffersRemoteConfig: ServerCapabilities.ValueType # 2 """The Server can offer remote configuration to the Agent.""" ServerCapabilities_AcceptsEffectiveConfig: ServerCapabilities.ValueType # 4 """The Server can accept EffectiveConfig in AgentToServer.""" ServerCapabilities_OffersPackages: ServerCapabilities.ValueType # 8 """The Server can offer Packages. Status: [Beta] """ ServerCapabilities_AcceptsPackagesStatus: ServerCapabilities.ValueType # 16 """The Server can accept Packages status. Status: [Beta] """ ServerCapabilities_OffersConnectionSettings: ServerCapabilities.ValueType # 32 """The Server can offer connection settings. Status: [Beta] """ ServerCapabilities_AcceptsConnectionSettingsRequest: ServerCapabilities.ValueType # 64 """The Server can accept ConnectionSettingsRequest and respond with an offer. Status: [Development] """ global___ServerCapabilities = ServerCapabilities class _PackageType: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType class _PackageTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_PackageType.ValueType], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor PackageType_TopLevel: _PackageType.ValueType # 0 PackageType_Addon: _PackageType.ValueType # 1 class PackageType(_PackageType, metaclass=_PackageTypeEnumTypeWrapper): """The type of the package, either an addon or a top-level package. Status: [Beta] """ PackageType_TopLevel: PackageType.ValueType # 0 PackageType_Addon: PackageType.ValueType # 1 global___PackageType = PackageType class _ServerErrorResponseType: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType class _ServerErrorResponseTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_ServerErrorResponseType.ValueType], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor ServerErrorResponseType_Unknown: _ServerErrorResponseType.ValueType # 0 """Unknown error. Something went wrong, but it is not known what exactly. The Agent SHOULD NOT retry the message. The error_message field may contain a description of the problem. """ ServerErrorResponseType_BadRequest: _ServerErrorResponseType.ValueType # 1 """The AgentToServer message was malformed. The Agent SHOULD NOT retry the message. """ ServerErrorResponseType_Unavailable: _ServerErrorResponseType.ValueType # 2 """The Server is overloaded and unable to process the request. The Agent should retry the message later. retry_info field may be optionally set with additional information about retrying. """ class ServerErrorResponseType(_ServerErrorResponseType, metaclass=_ServerErrorResponseTypeEnumTypeWrapper): ... ServerErrorResponseType_Unknown: ServerErrorResponseType.ValueType # 0 """Unknown error. Something went wrong, but it is not known what exactly. The Agent SHOULD NOT retry the message. The error_message field may contain a description of the problem. """ ServerErrorResponseType_BadRequest: ServerErrorResponseType.ValueType # 1 """The AgentToServer message was malformed. The Agent SHOULD NOT retry the message. """ ServerErrorResponseType_Unavailable: ServerErrorResponseType.ValueType # 2 """The Server is overloaded and unable to process the request. The Agent should retry the message later. retry_info field may be optionally set with additional information about retrying. """ global___ServerErrorResponseType = ServerErrorResponseType class _CommandType: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType class _CommandTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_CommandType.ValueType], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor CommandType_Restart: _CommandType.ValueType # 0 """The Agent should restart. This request will be ignored if the Agent does not support restart. """ class CommandType(_CommandType, metaclass=_CommandTypeEnumTypeWrapper): """Status: [Beta]""" CommandType_Restart: CommandType.ValueType # 0 """The Agent should restart. This request will be ignored if the Agent does not support restart. """ global___CommandType = CommandType class _AgentCapabilities: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType class _AgentCapabilitiesEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_AgentCapabilities.ValueType], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor AgentCapabilities_Unspecified: _AgentCapabilities.ValueType # 0 """The capabilities field is unspecified.""" AgentCapabilities_ReportsStatus: _AgentCapabilities.ValueType # 1 """The Agent can report status. This bit MUST be set, since all Agents MUST report status. """ AgentCapabilities_AcceptsRemoteConfig: _AgentCapabilities.ValueType # 2 """The Agent can accept remote configuration from the Server.""" AgentCapabilities_ReportsEffectiveConfig: _AgentCapabilities.ValueType # 4 """The Agent will report EffectiveConfig in AgentToServer.""" AgentCapabilities_AcceptsPackages: _AgentCapabilities.ValueType # 8 """The Agent can accept package offers. Status: [Beta] """ AgentCapabilities_ReportsPackageStatuses: _AgentCapabilities.ValueType # 16 """The Agent can report package status. Status: [Beta] """ AgentCapabilities_ReportsOwnTraces: _AgentCapabilities.ValueType # 32 """The Agent can report own trace to the destination specified by the Server via ConnectionSettingsOffers.own_traces field. Status: [Beta] """ AgentCapabilities_ReportsOwnMetrics: _AgentCapabilities.ValueType # 64 """The Agent can report own metrics to the destination specified by the Server via ConnectionSettingsOffers.own_metrics field. Status: [Beta] """ AgentCapabilities_ReportsOwnLogs: _AgentCapabilities.ValueType # 128 """The Agent can report own logs to the destination specified by the Server via ConnectionSettingsOffers.own_logs field. Status: [Beta] """ AgentCapabilities_AcceptsOpAMPConnectionSettings: _AgentCapabilities.ValueType # 256 """The can accept connections settings for OpAMP via ConnectionSettingsOffers.opamp field. Status: [Beta] """ AgentCapabilities_AcceptsOtherConnectionSettings: _AgentCapabilities.ValueType # 512 """The can accept connections settings for other destinations via ConnectionSettingsOffers.other_connections field. Status: [Beta] """ AgentCapabilities_AcceptsRestartCommand: _AgentCapabilities.ValueType # 1024 """The Agent can accept restart requests. Status: [Beta] """ AgentCapabilities_ReportsHealth: _AgentCapabilities.ValueType # 2048 """The Agent will report Health via AgentToServer.health field.""" AgentCapabilities_ReportsRemoteConfig: _AgentCapabilities.ValueType # 4096 """The Agent will report RemoteConfig status via AgentToServer.remote_config_status field.""" AgentCapabilities_ReportsHeartbeat: _AgentCapabilities.ValueType # 8192 """The Agent can report heartbeats. This is specified by the ServerToAgent.OpAMPConnectionSettings.heartbeat_interval_seconds field. If this capability is true, but the Server does not set a heartbeat_interval_seconds field, the Agent should use its own configured interval, which by default will be 30s. The Server may not know the configured interval and should not make assumptions about it. Status: [Development] """ AgentCapabilities_ReportsAvailableComponents: _AgentCapabilities.ValueType # 16384 """The agent will report AvailableComponents via the AgentToServer.available_components field. Status: [Development] Add new capabilities here, continuing with the least significant unused bit. """ class AgentCapabilities(_AgentCapabilities, metaclass=_AgentCapabilitiesEnumTypeWrapper): ... AgentCapabilities_Unspecified: AgentCapabilities.ValueType # 0 """The capabilities field is unspecified.""" AgentCapabilities_ReportsStatus: AgentCapabilities.ValueType # 1 """The Agent can report status. This bit MUST be set, since all Agents MUST report status. """ AgentCapabilities_AcceptsRemoteConfig: AgentCapabilities.ValueType # 2 """The Agent can accept remote configuration from the Server.""" AgentCapabilities_ReportsEffectiveConfig: AgentCapabilities.ValueType # 4 """The Agent will report EffectiveConfig in AgentToServer.""" AgentCapabilities_AcceptsPackages: AgentCapabilities.ValueType # 8 """The Agent can accept package offers. Status: [Beta] """ AgentCapabilities_ReportsPackageStatuses: AgentCapabilities.ValueType # 16 """The Agent can report package status. Status: [Beta] """ AgentCapabilities_ReportsOwnTraces: AgentCapabilities.ValueType # 32 """The Agent can report own trace to the destination specified by the Server via ConnectionSettingsOffers.own_traces field. Status: [Beta] """ AgentCapabilities_ReportsOwnMetrics: AgentCapabilities.ValueType # 64 """The Agent can report own metrics to the destination specified by the Server via ConnectionSettingsOffers.own_metrics field. Status: [Beta] """ AgentCapabilities_ReportsOwnLogs: AgentCapabilities.ValueType # 128 """The Agent can report own logs to the destination specified by the Server via ConnectionSettingsOffers.own_logs field. Status: [Beta] """ AgentCapabilities_AcceptsOpAMPConnectionSettings: AgentCapabilities.ValueType # 256 """The can accept connections settings for OpAMP via ConnectionSettingsOffers.opamp field. Status: [Beta] """ AgentCapabilities_AcceptsOtherConnectionSettings: AgentCapabilities.ValueType # 512 """The can accept connections settings for other destinations via ConnectionSettingsOffers.other_connections field. Status: [Beta] """ AgentCapabilities_AcceptsRestartCommand: AgentCapabilities.ValueType # 1024 """The Agent can accept restart requests. Status: [Beta] """ AgentCapabilities_ReportsHealth: AgentCapabilities.ValueType # 2048 """The Agent will report Health via AgentToServer.health field.""" AgentCapabilities_ReportsRemoteConfig: AgentCapabilities.ValueType # 4096 """The Agent will report RemoteConfig status via AgentToServer.remote_config_status field.""" AgentCapabilities_ReportsHeartbeat: AgentCapabilities.ValueType # 8192 """The Agent can report heartbeats. This is specified by the ServerToAgent.OpAMPConnectionSettings.heartbeat_interval_seconds field. If this capability is true, but the Server does not set a heartbeat_interval_seconds field, the Agent should use its own configured interval, which by default will be 30s. The Server may not know the configured interval and should not make assumptions about it. Status: [Development] """ AgentCapabilities_ReportsAvailableComponents: AgentCapabilities.ValueType # 16384 """The agent will report AvailableComponents via the AgentToServer.available_components field. Status: [Development] Add new capabilities here, continuing with the least significant unused bit. """ global___AgentCapabilities = AgentCapabilities class _RemoteConfigStatuses: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType class _RemoteConfigStatusesEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_RemoteConfigStatuses.ValueType], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor RemoteConfigStatuses_UNSET: _RemoteConfigStatuses.ValueType # 0 """The value of status field is not set.""" RemoteConfigStatuses_APPLIED: _RemoteConfigStatuses.ValueType # 1 """Remote config was successfully applied by the Agent.""" RemoteConfigStatuses_APPLYING: _RemoteConfigStatuses.ValueType # 2 """Agent is currently applying the remote config that it received earlier.""" RemoteConfigStatuses_FAILED: _RemoteConfigStatuses.ValueType # 3 """Agent tried to apply the config received earlier, but it failed. See error_message for more details. """ class RemoteConfigStatuses(_RemoteConfigStatuses, metaclass=_RemoteConfigStatusesEnumTypeWrapper): ... RemoteConfigStatuses_UNSET: RemoteConfigStatuses.ValueType # 0 """The value of status field is not set.""" RemoteConfigStatuses_APPLIED: RemoteConfigStatuses.ValueType # 1 """Remote config was successfully applied by the Agent.""" RemoteConfigStatuses_APPLYING: RemoteConfigStatuses.ValueType # 2 """Agent is currently applying the remote config that it received earlier.""" RemoteConfigStatuses_FAILED: RemoteConfigStatuses.ValueType # 3 """Agent tried to apply the config received earlier, but it failed. See error_message for more details. """ global___RemoteConfigStatuses = RemoteConfigStatuses class _PackageStatusEnum: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType class _PackageStatusEnumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_PackageStatusEnum.ValueType], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor PackageStatusEnum_Installed: _PackageStatusEnum.ValueType # 0 """Package is successfully installed by the Agent. The error_message field MUST NOT be set. """ PackageStatusEnum_InstallPending: _PackageStatusEnum.ValueType # 1 """Installation of this package has not yet started.""" PackageStatusEnum_Installing: _PackageStatusEnum.ValueType # 2 """Agent is currently installing the package. server_offered_hash field MUST be set to indicate the version that the Agent is installing. The error_message field MUST NOT be set. """ PackageStatusEnum_InstallFailed: _PackageStatusEnum.ValueType # 3 """Agent tried to install the package but installation failed. server_offered_hash field MUST be set to indicate the version that the Agent tried to install. The error_message may also contain more details about the failure. """ PackageStatusEnum_Downloading: _PackageStatusEnum.ValueType # 4 """Agent is currently downloading the package. server_offered_hash field MUST be set to indicate the version that the Agent is installing. The error_message field MUST NOT be set. Status: [Development] """ class PackageStatusEnum(_PackageStatusEnum, metaclass=_PackageStatusEnumEnumTypeWrapper): """The status of this package. Status: [Beta] """ PackageStatusEnum_Installed: PackageStatusEnum.ValueType # 0 """Package is successfully installed by the Agent. The error_message field MUST NOT be set. """ PackageStatusEnum_InstallPending: PackageStatusEnum.ValueType # 1 """Installation of this package has not yet started.""" PackageStatusEnum_Installing: PackageStatusEnum.ValueType # 2 """Agent is currently installing the package. server_offered_hash field MUST be set to indicate the version that the Agent is installing. The error_message field MUST NOT be set. """ PackageStatusEnum_InstallFailed: PackageStatusEnum.ValueType # 3 """Agent tried to install the package but installation failed. server_offered_hash field MUST be set to indicate the version that the Agent tried to install. The error_message may also contain more details about the failure. """ PackageStatusEnum_Downloading: PackageStatusEnum.ValueType # 4 """Agent is currently downloading the package. server_offered_hash field MUST be set to indicate the version that the Agent is installing. The error_message field MUST NOT be set. Status: [Development] """ global___PackageStatusEnum = PackageStatusEnum @typing_extensions.final class AgentToServer(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor INSTANCE_UID_FIELD_NUMBER: builtins.int SEQUENCE_NUM_FIELD_NUMBER: builtins.int AGENT_DESCRIPTION_FIELD_NUMBER: builtins.int CAPABILITIES_FIELD_NUMBER: builtins.int HEALTH_FIELD_NUMBER: builtins.int EFFECTIVE_CONFIG_FIELD_NUMBER: builtins.int REMOTE_CONFIG_STATUS_FIELD_NUMBER: builtins.int PACKAGE_STATUSES_FIELD_NUMBER: builtins.int AGENT_DISCONNECT_FIELD_NUMBER: builtins.int FLAGS_FIELD_NUMBER: builtins.int CONNECTION_SETTINGS_REQUEST_FIELD_NUMBER: builtins.int CUSTOM_CAPABILITIES_FIELD_NUMBER: builtins.int CUSTOM_MESSAGE_FIELD_NUMBER: builtins.int AVAILABLE_COMPONENTS_FIELD_NUMBER: builtins.int instance_uid: builtins.bytes """Globally unique identifier of the running instance of the Agent. SHOULD remain unchanged for the lifetime of the Agent process. MUST be 16 bytes long and SHOULD be generated using the UUID v7 spec. """ sequence_num: builtins.int """The sequence number is incremented by 1 for every AgentToServer sent by the Agent. This allows the Server to detect that it missed a message when it notices that the sequence_num is not exactly by 1 greater than the previously received one. """ @property def agent_description(self) -> global___AgentDescription: """Data that describes the Agent, its type, where it runs, etc. May be omitted if nothing changed since last AgentToServer message. """ capabilities: builtins.int """Bitmask of flags defined by AgentCapabilities enum. All bits that are not defined in AgentCapabilities enum MUST be set to 0 by the Agent. This allows extending the protocol and the AgentCapabilities enum in the future such that old Agents automatically report that they don't support the new capability. This field MUST be always set. """ @property def health(self) -> global___ComponentHealth: """The current health of the Agent and sub-components. The top-level ComponentHealth represents the health of the Agent overall. May be omitted if nothing changed since last AgentToServer message. Status: [Beta] """ @property def effective_config(self) -> global___EffectiveConfig: """The current effective configuration of the Agent. The effective configuration is the one that is currently used by the Agent. The effective configuration may be different from the remote configuration received from the Server earlier, e.g. because the Agent uses a local configuration instead (or in addition). This field SHOULD be unset if the effective config is unchanged since the last AgentToServer message. """ @property def remote_config_status(self) -> global___RemoteConfigStatus: """The status of the remote config that was previously received from the Server. This field SHOULD be unset if the remote config status is unchanged since the last AgentToServer message. """ @property def package_statuses(self) -> global___PackageStatuses: """The list of the Agent packages, including package statuses. This field SHOULD be unset if this information is unchanged since the last AgentToServer message for this Agent was sent in the stream. Status: [Beta] """ @property def agent_disconnect(self) -> global___AgentDisconnect: """AgentDisconnect MUST be set in the last AgentToServer message sent from the Agent to the Server. """ flags: builtins.int """Bit flags as defined by AgentToServerFlags bit masks.""" @property def connection_settings_request(self) -> global___ConnectionSettingsRequest: """A request to create connection settings. This field is set for flows where the Agent initiates the creation of connection settings. Status: [Development] """ @property def custom_capabilities(self) -> global___CustomCapabilities: """A message indicating custom capabilities supported by the Agent. Status: [Development] """ @property def custom_message(self) -> global___CustomMessage: """A custom message sent from an Agent to the Server. Status: [Development] """ @property def available_components(self) -> global___AvailableComponents: """A message indicating the components that are available for configuration on the agent. Status: [Development] """ def __init__( self, *, instance_uid: builtins.bytes = ..., sequence_num: builtins.int = ..., agent_description: global___AgentDescription | None = ..., capabilities: builtins.int = ..., health: global___ComponentHealth | None = ..., effective_config: global___EffectiveConfig | None = ..., remote_config_status: global___RemoteConfigStatus | None = ..., package_statuses: global___PackageStatuses | None = ..., agent_disconnect: global___AgentDisconnect | None = ..., flags: builtins.int = ..., connection_settings_request: global___ConnectionSettingsRequest | None = ..., custom_capabilities: global___CustomCapabilities | None = ..., custom_message: global___CustomMessage | None = ..., available_components: global___AvailableComponents | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["agent_description", b"agent_description", "agent_disconnect", b"agent_disconnect", "available_components", b"available_components", "connection_settings_request", b"connection_settings_request", "custom_capabilities", b"custom_capabilities", "custom_message", b"custom_message", "effective_config", b"effective_config", "health", b"health", "package_statuses", b"package_statuses", "remote_config_status", b"remote_config_status"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["agent_description", b"agent_description", "agent_disconnect", b"agent_disconnect", "available_components", b"available_components", "capabilities", b"capabilities", "connection_settings_request", b"connection_settings_request", "custom_capabilities", b"custom_capabilities", "custom_message", b"custom_message", "effective_config", b"effective_config", "flags", b"flags", "health", b"health", "instance_uid", b"instance_uid", "package_statuses", b"package_statuses", "remote_config_status", b"remote_config_status", "sequence_num", b"sequence_num"]) -> None: ... global___AgentToServer = AgentToServer @typing_extensions.final class AgentDisconnect(google.protobuf.message.Message): """AgentDisconnect is the last message sent from the Agent to the Server. The Server SHOULD forget the association of the Agent instance with the message stream. If the message stream is closed in the transport layer then the Server SHOULD forget association of all Agent instances that were previously established for this message stream using AgentConnect message, even if the corresponding AgentDisconnect message were not explicitly received from the Agent. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor def __init__( self, ) -> None: ... global___AgentDisconnect = AgentDisconnect @typing_extensions.final class ConnectionSettingsRequest(google.protobuf.message.Message): """ConnectionSettingsRequest is a request from the Agent to the Server to create and respond with an offer of connection settings for the Agent. Status: [Development] """ DESCRIPTOR: google.protobuf.descriptor.Descriptor OPAMP_FIELD_NUMBER: builtins.int @property def opamp(self) -> global___OpAMPConnectionSettingsRequest: """Request for OpAMP connection settings. If this field is unset then the ConnectionSettingsRequest message is empty and is not actionable for the Server. """ def __init__( self, *, opamp: global___OpAMPConnectionSettingsRequest | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["opamp", b"opamp"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["opamp", b"opamp"]) -> None: ... global___ConnectionSettingsRequest = ConnectionSettingsRequest @typing_extensions.final class OpAMPConnectionSettingsRequest(google.protobuf.message.Message): """OpAMPConnectionSettingsRequest is a request for the Server to produce a OpAMPConnectionSettings in its response. Status: [Development] """ DESCRIPTOR: google.protobuf.descriptor.Descriptor CERTIFICATE_REQUEST_FIELD_NUMBER: builtins.int @property def certificate_request(self) -> global___CertificateRequest: """A request to create a client certificate. This is used to initiate a Client Signing Request (CSR) flow. Required. """ def __init__( self, *, certificate_request: global___CertificateRequest | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["certificate_request", b"certificate_request"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["certificate_request", b"certificate_request"]) -> None: ... global___OpAMPConnectionSettingsRequest = OpAMPConnectionSettingsRequest @typing_extensions.final class CertificateRequest(google.protobuf.message.Message): """Status: [Development]""" DESCRIPTOR: google.protobuf.descriptor.Descriptor CSR_FIELD_NUMBER: builtins.int csr: builtins.bytes """PEM-encoded Client Certificate Signing Request (CSR), signed by client's private key. The Server SHOULD validate the request and SHOULD respond with a OpAMPConnectionSettings where the certificate.cert contains the issued certificate. """ def __init__( self, *, csr: builtins.bytes = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["csr", b"csr"]) -> None: ... global___CertificateRequest = CertificateRequest @typing_extensions.final class AvailableComponents(google.protobuf.message.Message): """AvailableComponents contains metadata relating to the components included within the agent. status: [Development] """ DESCRIPTOR: google.protobuf.descriptor.Descriptor @typing_extensions.final class ComponentsEntry(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor KEY_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int key: builtins.str @property def value(self) -> global___ComponentDetails: ... def __init__( self, *, key: builtins.str = ..., value: global___ComponentDetails | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... COMPONENTS_FIELD_NUMBER: builtins.int HASH_FIELD_NUMBER: builtins.int @property def components(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___ComponentDetails]: """A map of a unique component ID to details about the component. This may be omitted from the message if the server has not explicitly requested it be sent by setting the ReportAvailableComponents flag in the previous ServerToAgent message. """ hash: builtins.bytes """Agent-calculated hash of the components. This hash should be included in every AvailableComponents message. """ def __init__( self, *, components: collections.abc.Mapping[builtins.str, global___ComponentDetails] | None = ..., hash: builtins.bytes = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["components", b"components", "hash", b"hash"]) -> None: ... global___AvailableComponents = AvailableComponents @typing_extensions.final class ComponentDetails(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @typing_extensions.final class SubComponentMapEntry(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor KEY_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int key: builtins.str @property def value(self) -> global___ComponentDetails: ... def __init__( self, *, key: builtins.str = ..., value: global___ComponentDetails | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... METADATA_FIELD_NUMBER: builtins.int SUB_COMPONENT_MAP_FIELD_NUMBER: builtins.int @property def metadata(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[anyvalue_pb2.KeyValue]: """Extra key/value pairs that may be used to describe the component. The key/value pairs are according to semantic conventions, see: https://opentelemetry.io/docs/specs/semconv/ For example, you may use the "code" semantic conventions to report the location of the code for a specific component: https://opentelemetry.io/docs/specs/semconv/attributes-registry/code/ Or you may use the "vcs" semantic conventions to report the repository the component may be a part of: https://opentelemetry.io/docs/specs/semconv/attributes-registry/vcs/ """ @property def sub_component_map(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___ComponentDetails]: """A map of component ID to sub components details. It can nest as deeply as needed to describe the underlying system. """ def __init__( self, *, metadata: collections.abc.Iterable[anyvalue_pb2.KeyValue] | None = ..., sub_component_map: collections.abc.Mapping[builtins.str, global___ComponentDetails] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["metadata", b"metadata", "sub_component_map", b"sub_component_map"]) -> None: ... global___ComponentDetails = ComponentDetails @typing_extensions.final class ServerToAgent(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor INSTANCE_UID_FIELD_NUMBER: builtins.int ERROR_RESPONSE_FIELD_NUMBER: builtins.int REMOTE_CONFIG_FIELD_NUMBER: builtins.int CONNECTION_SETTINGS_FIELD_NUMBER: builtins.int PACKAGES_AVAILABLE_FIELD_NUMBER: builtins.int FLAGS_FIELD_NUMBER: builtins.int CAPABILITIES_FIELD_NUMBER: builtins.int AGENT_IDENTIFICATION_FIELD_NUMBER: builtins.int COMMAND_FIELD_NUMBER: builtins.int CUSTOM_CAPABILITIES_FIELD_NUMBER: builtins.int CUSTOM_MESSAGE_FIELD_NUMBER: builtins.int instance_uid: builtins.bytes """Agent instance uid. MUST match the instance_uid field in AgentToServer message. Used for multiplexing messages from/to multiple agents using one message stream. """ @property def error_response(self) -> global___ServerErrorResponse: """error_response is set if the Server wants to indicate that something went wrong during processing of an AgentToServer message. If error_response is set then all other fields below must be unset and vice versa, if any of the fields below is set then error_response must be unset. """ @property def remote_config(self) -> global___AgentRemoteConfig: """remote_config field is set when the Server has a remote config offer for the Agent.""" @property def connection_settings(self) -> global___ConnectionSettingsOffers: """This field is set when the Server wants the Agent to change one or more of its client connection settings (destination, headers, certificate, etc). Status: [Beta] """ @property def packages_available(self) -> global___PackagesAvailable: """This field is set when the Server has packages to offer to the Agent. Status: [Beta] """ flags: builtins.int """Bit flags as defined by ServerToAgentFlags bit masks.""" capabilities: builtins.int """Bitmask of flags defined by ServerCapabilities enum. All bits that are not defined in ServerCapabilities enum MUST be set to 0 by the Server. This allows extending the protocol and the ServerCapabilities enum in the future such that old Servers automatically report that they don't support the new capability. This field MUST be set in the first ServerToAgent sent by the Server and MAY be omitted in subsequent ServerToAgent messages by setting it to UnspecifiedServerCapability value. """ @property def agent_identification(self) -> global___AgentIdentification: """Properties related to identification of the Agent, which can be overridden by the Server if needed. """ @property def command(self) -> global___ServerToAgentCommand: """Allows the Server to instruct the Agent to perform a command, e.g. RESTART. This field should not be specified with fields other than instance_uid and capabilities. If specified, other fields will be ignored and the command will be performed. Status: [Beta] """ @property def custom_capabilities(self) -> global___CustomCapabilities: """A message indicating custom capabilities supported by the Server. Status: [Development] """ @property def custom_message(self) -> global___CustomMessage: """A custom message sent from the Server to an Agent. Status: [Development] """ def __init__( self, *, instance_uid: builtins.bytes = ..., error_response: global___ServerErrorResponse | None = ..., remote_config: global___AgentRemoteConfig | None = ..., connection_settings: global___ConnectionSettingsOffers | None = ..., packages_available: global___PackagesAvailable | None = ..., flags: builtins.int = ..., capabilities: builtins.int = ..., agent_identification: global___AgentIdentification | None = ..., command: global___ServerToAgentCommand | None = ..., custom_capabilities: global___CustomCapabilities | None = ..., custom_message: global___CustomMessage | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["agent_identification", b"agent_identification", "command", b"command", "connection_settings", b"connection_settings", "custom_capabilities", b"custom_capabilities", "custom_message", b"custom_message", "error_response", b"error_response", "packages_available", b"packages_available", "remote_config", b"remote_config"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["agent_identification", b"agent_identification", "capabilities", b"capabilities", "command", b"command", "connection_settings", b"connection_settings", "custom_capabilities", b"custom_capabilities", "custom_message", b"custom_message", "error_response", b"error_response", "flags", b"flags", "instance_uid", b"instance_uid", "packages_available", b"packages_available", "remote_config", b"remote_config"]) -> None: ... global___ServerToAgent = ServerToAgent @typing_extensions.final class OpAMPConnectionSettings(google.protobuf.message.Message): """The OpAMPConnectionSettings message is a collection of fields which comprise an offer from the Server to the Agent to use the specified settings for OpAMP connection. Status: [Beta] """ DESCRIPTOR: google.protobuf.descriptor.Descriptor DESTINATION_ENDPOINT_FIELD_NUMBER: builtins.int HEADERS_FIELD_NUMBER: builtins.int CERTIFICATE_FIELD_NUMBER: builtins.int HEARTBEAT_INTERVAL_SECONDS_FIELD_NUMBER: builtins.int destination_endpoint: builtins.str """OpAMP Server URL This MUST be a WebSocket or HTTP URL and MUST be non-empty, for example: "wss://example.com:4318/v1/opamp" """ @property def headers(self) -> global___Headers: """Optional headers to use when connecting. Typically used to set access tokens or other authorization headers. For HTTP-based protocols the Agent should set these in the request headers. For example: key="Authorization", Value="Basic YWxhZGRpbjpvcGVuc2VzYW1l". """ @property def certificate(self) -> global___TLSCertificate: """The Agent should use the offered certificate to connect to the destination from now on. If the Agent is able to validate and connect using the offered certificate the Agent SHOULD forget any previous client certificates for this connection. This field is optional: if omitted the client SHOULD NOT use a client-side certificate. This field can be used to perform a client certificate revocation/rotation. """ heartbeat_interval_seconds: builtins.int """The Agent MUST periodically send an AgentToServer message if the AgentCapabilities_ReportsHeartbeat capability is true. At a minimum the instance_uid field MUST be set. An HTTP Client MUST use the value as polling interval, if heartbeat_interval_seconds is non-zero. A heartbeat is used to keep the connection active and inform the server that the Agent is still alive and active. If this field has no value or is set to 0, the Agent should not send any heartbeats. Status: [Development] """ def __init__( self, *, destination_endpoint: builtins.str = ..., headers: global___Headers | None = ..., certificate: global___TLSCertificate | None = ..., heartbeat_interval_seconds: builtins.int = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["certificate", b"certificate", "headers", b"headers"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["certificate", b"certificate", "destination_endpoint", b"destination_endpoint", "headers", b"headers", "heartbeat_interval_seconds", b"heartbeat_interval_seconds"]) -> None: ... global___OpAMPConnectionSettings = OpAMPConnectionSettings @typing_extensions.final class TelemetryConnectionSettings(google.protobuf.message.Message): """The TelemetryConnectionSettings message is a collection of fields which comprise an offer from the Server to the Agent to use the specified settings for a network connection to report own telemetry. Status: [Beta] """ DESCRIPTOR: google.protobuf.descriptor.Descriptor DESTINATION_ENDPOINT_FIELD_NUMBER: builtins.int HEADERS_FIELD_NUMBER: builtins.int CERTIFICATE_FIELD_NUMBER: builtins.int destination_endpoint: builtins.str """The value MUST be a full URL an OTLP/HTTP/Protobuf receiver with path. Schema SHOULD begin with "https://", for example "https://example.com:4318/v1/metrics" The Agent MAY refuse to send the telemetry if the URL begins with "http://". """ @property def headers(self) -> global___Headers: """Optional headers to use when connecting. Typically used to set access tokens or other authorization headers. For HTTP-based protocols the Agent should set these in the request headers. For example: key="Authorization", Value="Basic YWxhZGRpbjpvcGVuc2VzYW1l". """ @property def certificate(self) -> global___TLSCertificate: """The Agent should use the offered certificate to connect to the destination from now on. If the Agent is able to validate and connect using the offered certificate the Agent SHOULD forget any previous client certificates for this connection. This field is optional: if omitted the client SHOULD NOT use a client-side certificate. This field can be used to perform a client certificate revocation/rotation. """ def __init__( self, *, destination_endpoint: builtins.str = ..., headers: global___Headers | None = ..., certificate: global___TLSCertificate | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["certificate", b"certificate", "headers", b"headers"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["certificate", b"certificate", "destination_endpoint", b"destination_endpoint", "headers", b"headers"]) -> None: ... global___TelemetryConnectionSettings = TelemetryConnectionSettings @typing_extensions.final class OtherConnectionSettings(google.protobuf.message.Message): """The OtherConnectionSettings message is a collection of fields which comprise an offer from the Server to the Agent to use the specified settings for a network connection. It is not required that all fields in this message are specified. The Server may specify only some of the fields, in which case it means that the Server offers the Agent to change only those fields, while keeping the rest of the fields unchanged. For example the Server may send a ConnectionSettings message with only the certificate field set, while all other fields are unset. This means that the Server wants the Agent to use a new certificate and continue sending to the destination it is currently sending using the current header and other settings. For fields which reference other messages the field is considered unset when the reference is unset. For primitive field (string) we rely on the "flags" to describe that the field is not set (this is done to overcome the limitation of old protoc compilers don't generate methods that allow to check for the presence of the field. Status: [Beta] """ DESCRIPTOR: google.protobuf.descriptor.Descriptor @typing_extensions.final class OtherSettingsEntry(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor KEY_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int key: builtins.str value: builtins.str def __init__( self, *, key: builtins.str = ..., value: builtins.str = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... DESTINATION_ENDPOINT_FIELD_NUMBER: builtins.int HEADERS_FIELD_NUMBER: builtins.int CERTIFICATE_FIELD_NUMBER: builtins.int OTHER_SETTINGS_FIELD_NUMBER: builtins.int destination_endpoint: builtins.str """A URL, host:port or some other destination specifier.""" @property def headers(self) -> global___Headers: """Optional headers to use when connecting. Typically used to set access tokens or other authorization headers. For HTTP-based protocols the Agent should set these in the request headers. For example: key="Authorization", Value="Basic YWxhZGRpbjpvcGVuc2VzYW1l". """ @property def certificate(self) -> global___TLSCertificate: """The Agent should use the offered certificate to connect to the destination from now on. If the Agent is able to validate and connect using the offered certificate the Agent SHOULD forget any previous client certificates for this connection. This field is optional: if omitted the client SHOULD NOT use a client-side certificate. This field can be used to perform a client certificate revocation/rotation. """ @property def other_settings(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: """Other connection settings. These are Agent-specific and are up to the Agent interpret. """ def __init__( self, *, destination_endpoint: builtins.str = ..., headers: global___Headers | None = ..., certificate: global___TLSCertificate | None = ..., other_settings: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["certificate", b"certificate", "headers", b"headers"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["certificate", b"certificate", "destination_endpoint", b"destination_endpoint", "headers", b"headers", "other_settings", b"other_settings"]) -> None: ... global___OtherConnectionSettings = OtherConnectionSettings @typing_extensions.final class Headers(google.protobuf.message.Message): """Status: [Beta]""" DESCRIPTOR: google.protobuf.descriptor.Descriptor HEADERS_FIELD_NUMBER: builtins.int @property def headers(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Header]: ... def __init__( self, *, headers: collections.abc.Iterable[global___Header] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["headers", b"headers"]) -> None: ... global___Headers = Headers @typing_extensions.final class Header(google.protobuf.message.Message): """Status: [Beta]""" DESCRIPTOR: google.protobuf.descriptor.Descriptor KEY_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int key: builtins.str value: builtins.str def __init__( self, *, key: builtins.str = ..., value: builtins.str = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... global___Header = Header @typing_extensions.final class TLSCertificate(google.protobuf.message.Message): """Status: [Beta] The (cert,private_key) pair should be issued and signed by a Certificate Authority (CA) that the destination Server recognizes. It is highly recommended that the private key of the CA certificate is NOT stored on the destination Server otherwise compromising the Server will allow a malicious actor to issue valid Server certificates which will be automatically trusted by all agents and will allow the actor to trivially MITM Agent-to-Server traffic of all servers that use this CA certificate for their Server-side certificates. Alternatively the certificate may be self-signed, assuming the Server can verify the certificate. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor CERT_FIELD_NUMBER: builtins.int PRIVATE_KEY_FIELD_NUMBER: builtins.int CA_CERT_FIELD_NUMBER: builtins.int cert: builtins.bytes """PEM-encoded certificate. Required.""" private_key: builtins.bytes """PEM-encoded private key of the certificate. Required.""" ca_cert: builtins.bytes """PEM-encoded certificate of the signing CA. Optional. MUST be specified if the certificate is CA-signed. Can be stored by TLS-terminating intermediary proxies in order to verify the connecting client's certificate in the future. It is not recommended that the Agent accepts this CA as an authority for any purposes. """ def __init__( self, *, cert: builtins.bytes = ..., private_key: builtins.bytes = ..., ca_cert: builtins.bytes = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["ca_cert", b"ca_cert", "cert", b"cert", "private_key", b"private_key"]) -> None: ... global___TLSCertificate = TLSCertificate @typing_extensions.final class ConnectionSettingsOffers(google.protobuf.message.Message): """Status: [Beta]""" DESCRIPTOR: google.protobuf.descriptor.Descriptor @typing_extensions.final class OtherConnectionsEntry(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor KEY_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int key: builtins.str @property def value(self) -> global___OtherConnectionSettings: ... def __init__( self, *, key: builtins.str = ..., value: global___OtherConnectionSettings | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... HASH_FIELD_NUMBER: builtins.int OPAMP_FIELD_NUMBER: builtins.int OWN_METRICS_FIELD_NUMBER: builtins.int OWN_TRACES_FIELD_NUMBER: builtins.int OWN_LOGS_FIELD_NUMBER: builtins.int OTHER_CONNECTIONS_FIELD_NUMBER: builtins.int hash: builtins.bytes """Hash of all settings, including settings that may be omitted from this message because they are unchanged. """ @property def opamp(self) -> global___OpAMPConnectionSettings: """Settings to connect to the OpAMP Server. If this field is not set then the Agent should assume that the settings are unchanged and should continue using existing settings. The Agent MUST verify the offered connection settings by actually connecting before accepting the setting to ensure it does not loose access to the OpAMP Server due to invalid settings. """ @property def own_metrics(self) -> global___TelemetryConnectionSettings: """Settings to connect to an OTLP metrics backend to send Agent's own metrics to. If this field is not set then the Agent should assume that the settings are unchanged. Once accepted the Agent should periodically send to the specified destination its own metrics, i.e. metrics of the Agent process and any custom metrics that describe the Agent state. All attributes specified in the identifying_attributes field in AgentDescription message SHOULD be also specified in the Resource of the reported OTLP metrics. Attributes specified in the non_identifying_attributes field in AgentDescription message may be also specified in the Resource of the reported OTLP metrics, in which case they SHOULD have exactly the same values. Process metrics MUST follow the conventions for processes: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/process-metrics.md """ @property def own_traces(self) -> global___TelemetryConnectionSettings: """Similar to own_metrics, but for traces.""" @property def own_logs(self) -> global___TelemetryConnectionSettings: """Similar to own_metrics, but for logs.""" @property def other_connections(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___OtherConnectionSettings]: """Another set of connection settings, with a string name associated with each. How the Agent uses these is Agent-specific. Typically the name represents the name of the destination to connect to (as it is known to the Agent). If this field is not set then the Agent should assume that the other_connections settings are unchanged. """ def __init__( self, *, hash: builtins.bytes = ..., opamp: global___OpAMPConnectionSettings | None = ..., own_metrics: global___TelemetryConnectionSettings | None = ..., own_traces: global___TelemetryConnectionSettings | None = ..., own_logs: global___TelemetryConnectionSettings | None = ..., other_connections: collections.abc.Mapping[builtins.str, global___OtherConnectionSettings] | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["opamp", b"opamp", "own_logs", b"own_logs", "own_metrics", b"own_metrics", "own_traces", b"own_traces"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["hash", b"hash", "opamp", b"opamp", "other_connections", b"other_connections", "own_logs", b"own_logs", "own_metrics", b"own_metrics", "own_traces", b"own_traces"]) -> None: ... global___ConnectionSettingsOffers = ConnectionSettingsOffers @typing_extensions.final class PackagesAvailable(google.protobuf.message.Message): """List of packages that the Server offers to the Agent. Status: [Beta] """ DESCRIPTOR: google.protobuf.descriptor.Descriptor @typing_extensions.final class PackagesEntry(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor KEY_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int key: builtins.str @property def value(self) -> global___PackageAvailable: ... def __init__( self, *, key: builtins.str = ..., value: global___PackageAvailable | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... PACKAGES_FIELD_NUMBER: builtins.int ALL_PACKAGES_HASH_FIELD_NUMBER: builtins.int @property def packages(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___PackageAvailable]: """Map of packages. Keys are package names, values are the packages available for download.""" all_packages_hash: builtins.bytes """Aggregate hash of all remotely installed packages. The Agent SHOULD include this value in subsequent PackageStatuses messages. This in turn allows the management Server to identify that a different set of packages is available for the Agent and specify the available packages in the next ServerToAgent message. This field MUST be always set if the management Server supports packages of agents. The hash is calculated as an aggregate of all packages names and content. """ def __init__( self, *, packages: collections.abc.Mapping[builtins.str, global___PackageAvailable] | None = ..., all_packages_hash: builtins.bytes = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["all_packages_hash", b"all_packages_hash", "packages", b"packages"]) -> None: ... global___PackagesAvailable = PackagesAvailable @typing_extensions.final class PackageAvailable(google.protobuf.message.Message): """Each Agent is composed of one or more packages. A package has a name and content stored in a file. The content of the files, functionality provided by the packages, how they are stored and used by the Agent side is Agent type-specific and is outside the concerns of the OpAMP protocol. If the Agent does not have an installed package with the specified name then it SHOULD download it from the specified URL and install it. If the Agent already has an installed package with the specified name but with a different hash then the Agent SHOULD download and install the package again, since it is a different version of the same package. If the Agent has an installed package with the specified name and the same hash then the Agent does not need to do anything, it already has the right version of the package. Status: [Beta] """ DESCRIPTOR: google.protobuf.descriptor.Descriptor TYPE_FIELD_NUMBER: builtins.int VERSION_FIELD_NUMBER: builtins.int FILE_FIELD_NUMBER: builtins.int HASH_FIELD_NUMBER: builtins.int type: global___PackageType.ValueType version: builtins.str """The package version that is available on the Server side. The Agent may for example use this information to avoid downloading a package that was previously already downloaded and failed to install. """ @property def file(self) -> global___DownloadableFile: """The downloadable file of the package.""" hash: builtins.bytes """The hash of the package. SHOULD be calculated based on all other fields of the PackageAvailable message and content of the file of the package. The hash is used by the Agent to determine if the package it has is different from the package the Server is offering. """ def __init__( self, *, type: global___PackageType.ValueType = ..., version: builtins.str = ..., file: global___DownloadableFile | None = ..., hash: builtins.bytes = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["file", b"file"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["file", b"file", "hash", b"hash", "type", b"type", "version", b"version"]) -> None: ... global___PackageAvailable = PackageAvailable @typing_extensions.final class DownloadableFile(google.protobuf.message.Message): """Status: [Beta]""" DESCRIPTOR: google.protobuf.descriptor.Descriptor DOWNLOAD_URL_FIELD_NUMBER: builtins.int CONTENT_HASH_FIELD_NUMBER: builtins.int SIGNATURE_FIELD_NUMBER: builtins.int HEADERS_FIELD_NUMBER: builtins.int download_url: builtins.str """The URL from which the file can be downloaded using HTTP GET request. The Server at the specified URL SHOULD support range requests to allow for resuming downloads. """ content_hash: builtins.bytes """The hash of the file content. Can be used by the Agent to verify that the file was downloaded correctly. """ signature: builtins.bytes """Optional signature of the file content. Can be used by the Agent to verify the authenticity of the downloaded file, for example can be the [detached GPG signature](https://www.gnupg.org/gph/en/manual/x135.html#AEN160). The exact signing and verification method is Agent specific. See https://github.com/open-telemetry/opamp-spec/blob/main/specification.md#code-signing for recommendations. """ @property def headers(self) -> global___Headers: """Optional headers to use when downloading a file. Typically used to set access tokens or other authorization headers. For HTTP-based protocols the Agent should set these in the request headers. For example: key="Authorization", Value="Basic YWxhZGRpbjpvcGVuc2VzYW1l". Status: [Development] """ def __init__( self, *, download_url: builtins.str = ..., content_hash: builtins.bytes = ..., signature: builtins.bytes = ..., headers: global___Headers | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["headers", b"headers"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["content_hash", b"content_hash", "download_url", b"download_url", "headers", b"headers", "signature", b"signature"]) -> None: ... global___DownloadableFile = DownloadableFile @typing_extensions.final class ServerErrorResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor TYPE_FIELD_NUMBER: builtins.int ERROR_MESSAGE_FIELD_NUMBER: builtins.int RETRY_INFO_FIELD_NUMBER: builtins.int type: global___ServerErrorResponseType.ValueType error_message: builtins.str """Error message in the string form, typically human readable.""" @property def retry_info(self) -> global___RetryInfo: """Additional information about retrying if type==UNAVAILABLE.""" def __init__( self, *, type: global___ServerErrorResponseType.ValueType = ..., error_message: builtins.str = ..., retry_info: global___RetryInfo | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["Details", b"Details", "retry_info", b"retry_info"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["Details", b"Details", "error_message", b"error_message", "retry_info", b"retry_info", "type", b"type"]) -> None: ... def WhichOneof(self, oneof_group: typing_extensions.Literal["Details", b"Details"]) -> typing_extensions.Literal["retry_info"] | None: ... global___ServerErrorResponse = ServerErrorResponse @typing_extensions.final class RetryInfo(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor RETRY_AFTER_NANOSECONDS_FIELD_NUMBER: builtins.int retry_after_nanoseconds: builtins.int def __init__( self, *, retry_after_nanoseconds: builtins.int = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["retry_after_nanoseconds", b"retry_after_nanoseconds"]) -> None: ... global___RetryInfo = RetryInfo @typing_extensions.final class ServerToAgentCommand(google.protobuf.message.Message): """ServerToAgentCommand is sent from the Server to the Agent to request that the Agent perform a command. Status: [Beta] """ DESCRIPTOR: google.protobuf.descriptor.Descriptor TYPE_FIELD_NUMBER: builtins.int type: global___CommandType.ValueType def __init__( self, *, type: global___CommandType.ValueType = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["type", b"type"]) -> None: ... global___ServerToAgentCommand = ServerToAgentCommand @typing_extensions.final class AgentDescription(google.protobuf.message.Message): """////////////////////////////////////////////////////////////////////////////////// Status reporting """ DESCRIPTOR: google.protobuf.descriptor.Descriptor IDENTIFYING_ATTRIBUTES_FIELD_NUMBER: builtins.int NON_IDENTIFYING_ATTRIBUTES_FIELD_NUMBER: builtins.int @property def identifying_attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[anyvalue_pb2.KeyValue]: """Attributes that identify the Agent. Keys/values are according to OpenTelemetry semantic conventions, see: https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/resource/semantic_conventions For standalone running Agents (such as OpenTelemetry Collector) the following attributes SHOULD be specified: - service.name should be set to a reverse FQDN that uniquely identifies the Agent type, e.g. "io.opentelemetry.collector" - service.namespace if it is used in the environment where the Agent runs. - service.version should be set to version number of the Agent build. - service.instance.id should be set. It may be set equal to the Agent's instance uid (equal to ServerToAgent.instance_uid field) or any other value that uniquely identifies the Agent in combination with other attributes. - any other attributes that are necessary for uniquely identifying the Agent's own telemetry. The Agent SHOULD also include these attributes in the Resource of its own telemetry. The combination of identifying attributes SHOULD be sufficient to uniquely identify the Agent's own telemetry in the destination system to which the Agent sends its own telemetry. """ @property def non_identifying_attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[anyvalue_pb2.KeyValue]: """Attributes that do not necessarily identify the Agent but help describe where it runs. The following attributes SHOULD be included: - os.type, os.version - to describe where the Agent runs. - host.* to describe the host the Agent runs on. - cloud.* to describe the cloud where the host is located. - any other relevant Resource attributes that describe this Agent and the environment it runs in. - any user-defined attributes that the end user would like to associate with this Agent. """ def __init__( self, *, identifying_attributes: collections.abc.Iterable[anyvalue_pb2.KeyValue] | None = ..., non_identifying_attributes: collections.abc.Iterable[anyvalue_pb2.KeyValue] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["identifying_attributes", b"identifying_attributes", "non_identifying_attributes", b"non_identifying_attributes"]) -> None: ... global___AgentDescription = AgentDescription @typing_extensions.final class ComponentHealth(google.protobuf.message.Message): """The health of the Agent and sub-components Status: [Beta] """ DESCRIPTOR: google.protobuf.descriptor.Descriptor @typing_extensions.final class ComponentHealthMapEntry(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor KEY_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int key: builtins.str @property def value(self) -> global___ComponentHealth: ... def __init__( self, *, key: builtins.str = ..., value: global___ComponentHealth | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... HEALTHY_FIELD_NUMBER: builtins.int START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int LAST_ERROR_FIELD_NUMBER: builtins.int STATUS_FIELD_NUMBER: builtins.int STATUS_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int COMPONENT_HEALTH_MAP_FIELD_NUMBER: builtins.int healthy: builtins.bool """Set to true if the component is up and healthy.""" start_time_unix_nano: builtins.int """Timestamp since the component is up, i.e. when the component was started. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. If the component is not running MUST be set to 0. """ last_error: builtins.str """Human-readable error message if the component is in erroneous state. SHOULD be set when healthy==false. """ status: builtins.str """Component status represented as a string. The status values are defined by agent-specific semantics and not at the protocol level. """ status_time_unix_nano: builtins.int """The time when the component status was observed. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. """ @property def component_health_map(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___ComponentHealth]: """A map to store more granular, sub-component health. It can nest as deeply as needed to describe the underlying system. """ def __init__( self, *, healthy: builtins.bool = ..., start_time_unix_nano: builtins.int = ..., last_error: builtins.str = ..., status: builtins.str = ..., status_time_unix_nano: builtins.int = ..., component_health_map: collections.abc.Mapping[builtins.str, global___ComponentHealth] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["component_health_map", b"component_health_map", "healthy", b"healthy", "last_error", b"last_error", "start_time_unix_nano", b"start_time_unix_nano", "status", b"status", "status_time_unix_nano", b"status_time_unix_nano"]) -> None: ... global___ComponentHealth = ComponentHealth @typing_extensions.final class EffectiveConfig(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor CONFIG_MAP_FIELD_NUMBER: builtins.int @property def config_map(self) -> global___AgentConfigMap: """The effective config of the Agent.""" def __init__( self, *, config_map: global___AgentConfigMap | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["config_map", b"config_map"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["config_map", b"config_map"]) -> None: ... global___EffectiveConfig = EffectiveConfig @typing_extensions.final class RemoteConfigStatus(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor LAST_REMOTE_CONFIG_HASH_FIELD_NUMBER: builtins.int STATUS_FIELD_NUMBER: builtins.int ERROR_MESSAGE_FIELD_NUMBER: builtins.int last_remote_config_hash: builtins.bytes """The hash of the remote config that was last received by this Agent in the AgentRemoteConfig.config_hash field. The Server SHOULD compare this hash with the config hash it has for the Agent and if the hashes are different the Server MUST include the remote_config field in the response in the ServerToAgent message. """ status: global___RemoteConfigStatuses.ValueType error_message: builtins.str """Optional error message if status==FAILED.""" def __init__( self, *, last_remote_config_hash: builtins.bytes = ..., status: global___RemoteConfigStatuses.ValueType = ..., error_message: builtins.str = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["error_message", b"error_message", "last_remote_config_hash", b"last_remote_config_hash", "status", b"status"]) -> None: ... global___RemoteConfigStatus = RemoteConfigStatus @typing_extensions.final class PackageStatuses(google.protobuf.message.Message): """The PackageStatuses message describes the status of all packages that the Agent has or was offered. Status: [Beta] """ DESCRIPTOR: google.protobuf.descriptor.Descriptor @typing_extensions.final class PackagesEntry(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor KEY_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int key: builtins.str @property def value(self) -> global___PackageStatus: ... def __init__( self, *, key: builtins.str = ..., value: global___PackageStatus | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... PACKAGES_FIELD_NUMBER: builtins.int SERVER_PROVIDED_ALL_PACKAGES_HASH_FIELD_NUMBER: builtins.int ERROR_MESSAGE_FIELD_NUMBER: builtins.int @property def packages(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___PackageStatus]: """A map of PackageStatus messages, where the keys are package names. The key MUST match the name field of PackageStatus message. """ server_provided_all_packages_hash: builtins.bytes """The aggregate hash of all packages that this Agent previously received from the Server via PackagesAvailable message. The Server SHOULD compare this hash to the aggregate hash of all packages that it has for this Agent and if the hashes are different the Server SHOULD send an PackagesAvailable message to the Agent. """ error_message: builtins.str """This field is set if the Agent encountered an error when processing the PackagesAvailable message and that error is not related to any particular single package. The field must be unset is there were no processing errors. """ def __init__( self, *, packages: collections.abc.Mapping[builtins.str, global___PackageStatus] | None = ..., server_provided_all_packages_hash: builtins.bytes = ..., error_message: builtins.str = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["error_message", b"error_message", "packages", b"packages", "server_provided_all_packages_hash", b"server_provided_all_packages_hash"]) -> None: ... global___PackageStatuses = PackageStatuses @typing_extensions.final class PackageStatus(google.protobuf.message.Message): """The status of a single package. Status: [Beta] """ DESCRIPTOR: google.protobuf.descriptor.Descriptor NAME_FIELD_NUMBER: builtins.int AGENT_HAS_VERSION_FIELD_NUMBER: builtins.int AGENT_HAS_HASH_FIELD_NUMBER: builtins.int SERVER_OFFERED_VERSION_FIELD_NUMBER: builtins.int SERVER_OFFERED_HASH_FIELD_NUMBER: builtins.int STATUS_FIELD_NUMBER: builtins.int ERROR_MESSAGE_FIELD_NUMBER: builtins.int DOWNLOAD_DETAILS_FIELD_NUMBER: builtins.int name: builtins.str """Package name. MUST be always set and MUST match the key in the packages field of PackageStatuses message. """ agent_has_version: builtins.str """The version of the package that the Agent has. MUST be set if the Agent has this package. MUST be empty if the Agent does not have this package. This may be the case for example if the package was offered by the Server but failed to install and the Agent did not have this package previously. """ agent_has_hash: builtins.bytes """The hash of the package that the Agent has. MUST be set if the Agent has this package. MUST be empty if the Agent does not have this package. This may be the case for example if the package was offered by the Server but failed to install and the Agent did not have this package previously. """ server_offered_version: builtins.str """The version of the package that the Server offered to the Agent. MUST be set if the installation of the package is initiated by an earlier offer from the Server to install this package. MUST be empty if the Agent has this package but it was installed locally and was not offered by the Server. Note that it is possible for both agent_has_version and server_offered_version fields to be set and to have different values. This is for example possible if the Agent already has a version of the package successfully installed, the Server offers a different version, but the Agent fails to install that version. """ server_offered_hash: builtins.bytes """The hash of the package that the Server offered to the Agent. MUST be set if the installation of the package is initiated by an earlier offer from the Server to install this package. MUST be empty if the Agent has this package but it was installed locally and was not offered by the Server. Note that it is possible for both agent_has_hash and server_offered_hash fields to be set and to have different values. This is for example possible if the Agent already has a version of the package successfully installed, the Server offers a different version, but the Agent fails to install that version. """ status: global___PackageStatusEnum.ValueType error_message: builtins.str """Error message if the status is erroneous.""" @property def download_details(self) -> global___PackageDownloadDetails: """Optional details that may be of interest to a user. Should only be set if status is Downloading. Status: [Development] """ def __init__( self, *, name: builtins.str = ..., agent_has_version: builtins.str = ..., agent_has_hash: builtins.bytes = ..., server_offered_version: builtins.str = ..., server_offered_hash: builtins.bytes = ..., status: global___PackageStatusEnum.ValueType = ..., error_message: builtins.str = ..., download_details: global___PackageDownloadDetails | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["download_details", b"download_details"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["agent_has_hash", b"agent_has_hash", "agent_has_version", b"agent_has_version", "download_details", b"download_details", "error_message", b"error_message", "name", b"name", "server_offered_hash", b"server_offered_hash", "server_offered_version", b"server_offered_version", "status", b"status"]) -> None: ... global___PackageStatus = PackageStatus @typing_extensions.final class PackageDownloadDetails(google.protobuf.message.Message): """Additional details that an agent can use to describe an in-progress package download. Status: [Development] """ DESCRIPTOR: google.protobuf.descriptor.Descriptor DOWNLOAD_PERCENT_FIELD_NUMBER: builtins.int DOWNLOAD_BYTES_PER_SECOND_FIELD_NUMBER: builtins.int download_percent: builtins.float """The package download progress as a percentage.""" download_bytes_per_second: builtins.float """The current package download rate in bytes per second.""" def __init__( self, *, download_percent: builtins.float = ..., download_bytes_per_second: builtins.float = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["download_bytes_per_second", b"download_bytes_per_second", "download_percent", b"download_percent"]) -> None: ... global___PackageDownloadDetails = PackageDownloadDetails @typing_extensions.final class AgentIdentification(google.protobuf.message.Message): """Properties related to identification of the Agent, which can be overridden by the Server if needed """ DESCRIPTOR: google.protobuf.descriptor.Descriptor NEW_INSTANCE_UID_FIELD_NUMBER: builtins.int new_instance_uid: builtins.bytes """When new_instance_uid is set, Agent MUST update instance_uid to the value provided and use it for all further communication. MUST be 16 bytes long and SHOULD be generated using the UUID v7 spec. """ def __init__( self, *, new_instance_uid: builtins.bytes = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["new_instance_uid", b"new_instance_uid"]) -> None: ... global___AgentIdentification = AgentIdentification @typing_extensions.final class AgentRemoteConfig(google.protobuf.message.Message): """/////////////////////////////////////////////////////////////////////////////////// Config messages /////////////////////////////////////////////////////////////////////////////////// """ DESCRIPTOR: google.protobuf.descriptor.Descriptor CONFIG_FIELD_NUMBER: builtins.int CONFIG_HASH_FIELD_NUMBER: builtins.int @property def config(self) -> global___AgentConfigMap: """Agent config offered by the management Server to the Agent instance. SHOULD NOT be set if the config for this Agent has not changed since it was last requested (i.e. AgentConfigRequest.last_remote_config_hash field is equal to AgentConfigResponse.config_hash field). """ config_hash: builtins.bytes """Hash of "config". The Agent SHOULD include this value in subsequent RemoteConfigStatus messages in the last_remote_config_hash field. This in turn allows the management Server to identify that a new config is available for the Agent. This field MUST be always set if the management Server supports remote configuration of agents. Management Server must choose a hashing function that guarantees lack of hash collisions in practice. """ def __init__( self, *, config: global___AgentConfigMap | None = ..., config_hash: builtins.bytes = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["config", b"config"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["config", b"config", "config_hash", b"config_hash"]) -> None: ... global___AgentRemoteConfig = AgentRemoteConfig @typing_extensions.final class AgentConfigMap(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @typing_extensions.final class ConfigMapEntry(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor KEY_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int key: builtins.str @property def value(self) -> global___AgentConfigFile: ... def __init__( self, *, key: builtins.str = ..., value: global___AgentConfigFile | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... CONFIG_MAP_FIELD_NUMBER: builtins.int @property def config_map(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___AgentConfigFile]: """Map of configs. Keys are config file names or config section names. The configuration is assumed to be a collection of one or more named config files or sections. For agents that use a single config file or section the map SHOULD contain a single entry and the key may be an empty string. """ def __init__( self, *, config_map: collections.abc.Mapping[builtins.str, global___AgentConfigFile] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["config_map", b"config_map"]) -> None: ... global___AgentConfigMap = AgentConfigMap @typing_extensions.final class AgentConfigFile(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor BODY_FIELD_NUMBER: builtins.int CONTENT_TYPE_FIELD_NUMBER: builtins.int body: builtins.bytes """Config file or section body. The content, format and encoding depends on the Agent type. The content_type field may optionally describe the MIME type of the body. """ content_type: builtins.str """Optional MIME Content-Type that describes what's in the body field, for example "text/yaml". """ def __init__( self, *, body: builtins.bytes = ..., content_type: builtins.str = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["body", b"body", "content_type", b"content_type"]) -> None: ... global___AgentConfigFile = AgentConfigFile @typing_extensions.final class CustomCapabilities(google.protobuf.message.Message): """/////////////////////////////////////////////////////////////////////////////////// Custom messages /////////////////////////////////////////////////////////////////////////////////// """ DESCRIPTOR: google.protobuf.descriptor.Descriptor CAPABILITIES_FIELD_NUMBER: builtins.int @property def capabilities(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: """A list of custom capabilities that are supported. Each capability is a reverse FQDN with optional version information that uniquely identifies the custom capability and should match a capability specified in a supported CustomMessage. Status: [Development] """ def __init__( self, *, capabilities: collections.abc.Iterable[builtins.str] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["capabilities", b"capabilities"]) -> None: ... global___CustomCapabilities = CustomCapabilities @typing_extensions.final class CustomMessage(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor CAPABILITY_FIELD_NUMBER: builtins.int TYPE_FIELD_NUMBER: builtins.int DATA_FIELD_NUMBER: builtins.int capability: builtins.str """A reverse FQDN that uniquely identifies the capability and matches one of the capabilities in the CustomCapabilities message. Status: [Development] """ type: builtins.str """Type of message within the capability. The capability defines the types of custom messages that are used to implement the capability. The type must only be unique within the capability. Status: [Development] """ data: builtins.bytes """Binary data of the message. The capability must specify the format of the contents of the data for each custom message type it defines. Status: [Development] """ def __init__( self, *, capability: builtins.str = ..., type: builtins.str = ..., data: builtins.bytes = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["capability", b"capability", "data", b"data", "type", b"type"]) -> None: ... global___CustomMessage = CustomMessage transport/000077500000000000000000000000001527070072300350565ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/opentelemetry/_opampbase.py000066400000000000000000000012151527070072300363410ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import abc from collections.abc import Mapping from opentelemetry._opamp.proto import opamp_pb2 base_headers = { "Content-Type": "application/x-protobuf", } class HttpTransport(abc.ABC): @abc.abstractmethod def send( self, *, url: str, headers: Mapping[str, str], data: bytes, timeout_millis: int, tls_certificate: str | bool, tls_client_certificate: str | None = None, tls_client_key: str | None = None, ) -> opamp_pb2.ServerToAgent: pass exceptions.py000066400000000000000000000001701527070072300376070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 class OpAMPException(Exception): pass requests.py000066400000000000000000000031151527070072300373030ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging from collections.abc import Mapping import requests from opentelemetry._opamp import messages from opentelemetry._opamp.transport.base import HttpTransport, base_headers from opentelemetry._opamp.transport.exceptions import OpAMPException logger = logging.getLogger(__name__) class RequestsTransport(HttpTransport): def __init__(self, session: requests.Session | None = None): self.session = requests.Session() if session is None else session def send( self, *, url: str, headers: Mapping[str, str], data: bytes, timeout_millis: int, tls_certificate: str | bool, tls_client_certificate: str | None = None, tls_client_key: str | None = None, ): headers = {**base_headers, **headers} timeout: float = timeout_millis / 1e3 client_cert = ( (tls_client_certificate, tls_client_key) if tls_client_certificate and tls_client_key else tls_client_certificate ) try: response = self.session.post( url, headers=headers, data=data, timeout=timeout, verify=tls_certificate, cert=client_cert, ) response.raise_for_status() except Exception as exc: raise OpAMPException(str(exc)) from exc message = messages.decode_message(response.content) return message version.py000066400000000000000000000001471527070072300350630ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.4b0.dev" opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/test-requirements.in000066400000000000000000000005541527070072300321770ustar00rootroot00000000000000colorama>=0.4.6 iniconfig>=2.0.0 packaging>=24.0 pluggy>=1.5.0 protobuf>=5.29.5 pytest>=7.4.4 pytest-vcr>=1.0.2 ; platform_python_implementation !='PyPy' pyyaml>=6.0.2 vcrpy>=7.0.0 ; platform_python_implementation !='PyPy' yarl>=1.20.1 requests>=2.33.1 urllib3>=2.7.0 uuid-utils>=0.11.0 wrapt>=1.16.0 -e opamp/opentelemetry-opamp-client # for pylint tomli>=1.1.0 opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/test-requirements.latest.txt000066400000000000000000000051771527070072300337110ustar00rootroot00000000000000# This file was autogenerated by uv via the following command: # uv pip compile --python 3.10 --universal -c dev-requirements.txt opamp/opentelemetry-opamp-client/test-requirements.in -o opamp/opentelemetry-opamp-client/test-requirements.latest.txt --no-emit-package opentelemetry-api -e opamp/opentelemetry-opamp-client # via -r opamp/opentelemetry-opamp-client/test-requirements.in certifi==2025.7.9 # via requests charset-normalizer==3.4.2 # via requests colorama==0.4.6 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # pytest exceptiongroup==1.3.0 ; python_full_version < '3.11' # via pytest idna==3.10 # via # requests # yarl importlib-metadata==8.7.0 # via opentelemetry-api iniconfig==2.1.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # pytest multidict==6.6.3 # via yarl packaging==25.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # pytest pluggy==1.6.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # pytest propcache==0.3.2 # via yarl protobuf==6.31.1 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # opentelemetry-opamp-client pytest==7.4.4 # via # -c dev-requirements.txt # -r opamp/opentelemetry-opamp-client/test-requirements.in # pytest-vcr pytest-vcr==1.0.2 ; platform_python_implementation != 'PyPy' # via -r opamp/opentelemetry-opamp-client/test-requirements.in pyyaml==6.0.2 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # vcrpy requests==2.33.1 # via # -c dev-requirements.txt # -r opamp/opentelemetry-opamp-client/test-requirements.in tomli==2.2.1 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # pytest typing-extensions==4.14.0 # via # exceptiongroup # multidict # opentelemetry-api urllib3==2.7.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # requests # vcrpy uuid-utils==0.11.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # opentelemetry-opamp-client vcrpy==7.0.0 ; platform_python_implementation != 'PyPy' # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # pytest-vcr wrapt==1.17.2 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # vcrpy yarl==1.20.1 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # vcrpy zipp==3.23.0 # via importlib-metadata # The following packages were excluded from the output: # opentelemetry-api opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt000066400000000000000000000050711527070072300337230ustar00rootroot00000000000000# This file was autogenerated by uv via the following command: # uv pip compile --python 3.10 --universal --resolution lowest -c dev-requirements.txt opamp/opentelemetry-opamp-client/test-requirements.in -o opamp/opentelemetry-opamp-client/test-requirements.lowest.txt --no-emit-package opentelemetry-api -e opamp/opentelemetry-opamp-client # via -r opamp/opentelemetry-opamp-client/test-requirements.in certifi==2023.5.7 # via requests charset-normalizer==2.0.0 # via requests colorama==0.4.6 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # pytest deprecated==1.2.6 # via opentelemetry-api exceptiongroup==1.0.0 ; python_full_version < '3.11' # via pytest idna==2.5 # via # requests # yarl iniconfig==2.0.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # pytest multidict==4.0.0 # via yarl packaging==24.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # pytest pluggy==1.6.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # pytest propcache==0.2.1 # via yarl protobuf==5.29.5 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # opentelemetry-opamp-client pytest==7.4.4 # via # -c dev-requirements.txt # -r opamp/opentelemetry-opamp-client/test-requirements.in # pytest-vcr pytest-vcr==1.0.2 ; platform_python_implementation != 'PyPy' # via -r opamp/opentelemetry-opamp-client/test-requirements.in pyyaml==6.0.2 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # vcrpy requests==2.33.1 # via # -c dev-requirements.txt # -r opamp/opentelemetry-opamp-client/test-requirements.in setuptools==16.0 # via opentelemetry-api tomli==1.1.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # pytest urllib3==2.7.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # requests # vcrpy uuid-utils==0.11.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # opentelemetry-opamp-client vcrpy==7.0.0 ; platform_python_implementation != 'PyPy' # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # pytest-vcr wrapt==1.16.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # deprecated # vcrpy yarl==1.20.1 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # vcrpy # The following packages were excluded from the output: # opentelemetry-api opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/tests/000077500000000000000000000000001527070072300273055ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/tests/opamp/000077500000000000000000000000001527070072300304215ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/tests/opamp/README.md000066400000000000000000000012371527070072300317030ustar00rootroot00000000000000## How to record tests cassettes ### Run the server Checkout [opentelemetry-go](https://github.com/open-telemetry/opentelemetry-go) and build it with: ``` make ``` Then run the example server with: ``` ./internal/examples/server/bin/server ``` ### Record the cassettes We are using `vcr.py` for recording HTTP responses from the server, it works by serializing the response in a yaml file and then replying it. `vcr.py` will record a new cassettes only if it does not find it so to redo a recording you need to delete the recording in the `cassettes/` directory. Because of vcr.py issues with older urllib3 versions tests are skipped with Python older than 3.10. opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/tests/opamp/cassettes/000077500000000000000000000000001527070072300324175ustar00rootroot00000000000000test_connection_remote_config_status_heartbeat_disconnection.yaml000066400000000000000000000060771527070072300476750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/tests/opamp/cassettesinteractions: - request: body: !!binary | ChABnL5b5k5046/FazbAJ7r4Gj0KFQoMc2VydmljZS5uYW1lEgUKA2ZvbwokChtkZXBsb3ltZW50 LmVudmlyb25tZW50Lm5hbWUSBQoDZm9vIIdg headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '84' Content-Type: - application/x-protobuf User-Agent: - OTel-OpAMP-Python/0.1b0.dev method: POST uri: https://localhost:4320/v1/opamp response: body: string: !!binary | ChABnL5b5k5046/FazbAJ7r4GioKBgoECgASABIg47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZ G3hSuFUwAVIA headers: Content-Length: - '66' Content-Type: - application/x-protobuf Date: - Thu, 05 Mar 2026 14:16:59 GMT status: code: 200 message: OK - request: body: !!binary | ChABnL5b5k5046/FazbAJ7r4EAEgh2A6JAog47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hS uFUQAQ== headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '61' Content-Type: - application/x-protobuf User-Agent: - OTel-OpAMP-Python/0.1b0.dev method: POST uri: https://localhost:4320/v1/opamp response: body: string: !!binary | ChABnL5b5k5046/FazbAJ7r4GioKBgoECgASABIg47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZ G3hSuFVSAA== headers: Content-Length: - '64' Content-Type: - application/x-protobuf Date: - Thu, 05 Mar 2026 14:16:59 GMT status: code: 200 message: OK - request: body: !!binary | ChABnL5b5k5046/FazbAJ7r4EAIgh2A= headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '23' Content-Type: - application/x-protobuf User-Agent: - OTel-OpAMP-Python/0.1b0.dev method: POST uri: https://localhost:4320/v1/opamp response: body: string: !!binary | ChABnL5b5k5046/FazbAJ7r4GioKBgoECgASABIg47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZ G3hSuFVSAA== headers: Content-Length: - '64' Content-Type: - application/x-protobuf Date: - Thu, 05 Mar 2026 14:17:00 GMT status: code: 200 message: OK - request: body: !!binary | ChABnL5b5k5046/FazbAJ7r4EAMgh2BKAA== headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate Connection: - keep-alive Content-Length: - '25' Content-Type: - application/x-protobuf User-Agent: - OTel-OpAMP-Python/0.1b0.dev method: POST uri: https://localhost:4320/v1/opamp response: body: string: !!binary | ChABnL5b5k5046/FazbAJ7r4GioKBgoECgASABIg47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZ G3hSuFVSAA== headers: Content-Length: - '64' Content-Type: - application/x-protobuf Date: - Thu, 05 Mar 2026 14:17:01 GMT status: code: 200 message: OK version: 1 opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/tests/opamp/conftest.py000066400000000000000000000007621527070072300326250ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import pytest @pytest.fixture(scope="module") def vcr_config(): return { "filter_headers": [ ("authorization", "Bearer key"), ], "decode_compressed_response": True, "before_record_response": scrub_response_headers, } def scrub_response_headers(response): """ This scrubs sensitive response headers. Note they are case-sensitive! """ return response opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/tests/opamp/test_agent.py000066400000000000000000000254001527070072300331310ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging from time import sleep from unittest import mock from opentelemetry._opamp.agent import OpAMPAgent, _safe_invoke from opentelemetry._opamp.agent import _Job as Job from opentelemetry._opamp.callbacks import MessageData, OpAMPCallbacks from opentelemetry._opamp.proto import opamp_pb2 class _NoOpCallbacks(OpAMPCallbacks): pass def test_can_instantiate_agent(): agent = OpAMPAgent(interval=30, client=mock.Mock(), callbacks=_NoOpCallbacks()) assert isinstance(agent, OpAMPAgent) def test_can_start_agent(): agent = OpAMPAgent(interval=30, client=mock.Mock(), callbacks=_NoOpCallbacks()) agent.start() agent.stop() def test_agent_start_will_send_connection_and_disconnetion_messages(): client_mock = mock.Mock() mock_message = mock.Mock() mock_message.HasField.return_value = False mock_message.flags = 0 client_mock.send.return_value = mock_message cb = mock.create_autospec(OpAMPCallbacks, instance=True) agent = OpAMPAgent(interval=30, client=client_mock, callbacks=cb) agent.start() # wait for the queue to be consumed sleep(0.1) agent.stop() # one send for connection message, one for disconnect agent message assert client_mock.send.call_count == 2 # connection callback has been called assert agent._schedule is True # on_connect and on_message called for the connection response cb.on_connect.assert_called_once_with(agent, client_mock) cb.on_message.assert_called_once_with(agent, client_mock, MessageData(remote_config=None)) def test_agent_can_call_agent_stop_multiple_times(): agent = OpAMPAgent(interval=30, client=mock.Mock(), callbacks=_NoOpCallbacks()) agent.start() agent.stop() agent.stop() def test_agent_can_call_agent_stop_before_start(): agent = OpAMPAgent(interval=30, client=mock.Mock(), callbacks=_NoOpCallbacks()) agent.stop() def test_agent_send_warns_without_worker_thread(caplog): agent = OpAMPAgent(interval=30, client=mock.Mock(), callbacks=_NoOpCallbacks()) agent.send(payload="payload") assert caplog.record_tuples == [ ( "opentelemetry._opamp.agent", logging.WARNING, "Called send() but worker thread is not alive. Worker threads is started with start()", ) ] def test_agent_retries_before_max_attempts(caplog): caplog.set_level(logging.DEBUG, logger="opentelemetry._opamp.agent") cb = mock.create_autospec(OpAMPCallbacks, instance=True) client_mock = mock.Mock() connection_message = mock.Mock() connection_message.HasField.return_value = False connection_message.flags = 0 server_message = mock.Mock() server_message.HasField.return_value = False server_message.flags = 0 disconnection_message = mock.Mock() client_mock.send.side_effect = [ connection_message, Exception("fail"), server_message, disconnection_message, ] agent = OpAMPAgent( interval=30, client=client_mock, callbacks=cb, initial_backoff=0, ) agent.start() agent.send(payload="payload") # wait for the queue to be consumed sleep(0.1) agent.stop() assert client_mock.send.call_count == 4 assert cb.on_message.call_count == 2 assert cb.on_connect.call_count == 2 assert cb.on_connect_failed.call_count == 1 def test_agent_stops_after_max_attempts(caplog): caplog.set_level(logging.DEBUG, logger="opentelemetry._opamp.agent") cb = mock.create_autospec(OpAMPCallbacks, instance=True) client_mock = mock.Mock() connection_message = mock.Mock() connection_message.HasField.return_value = False connection_message.flags = 0 disconnection_message = mock.Mock() exc1 = Exception("fail1") exc2 = Exception("fail2") client_mock.send.side_effect = [ connection_message, exc1, exc2, disconnection_message, ] agent = OpAMPAgent( interval=30, client=client_mock, callbacks=cb, max_retries=1, initial_backoff=0, ) agent.start() agent.send(payload="payload") # wait for the queue to be consumed sleep(0.1) agent.stop() assert client_mock.send.call_count == 4 assert cb.on_message.call_count == 1 assert cb.on_connect_failed.call_count == 2 cb.on_connect_failed.assert_any_call(agent, client_mock, exc1) cb.on_connect_failed.assert_any_call(agent, client_mock, exc2) def test_agent_send_enqueues_job(): cb = mock.create_autospec(OpAMPCallbacks, instance=True) client_mock = mock.Mock() msg = mock.Mock() msg.HasField.return_value = False msg.flags = 0 client_mock.send.return_value = msg agent = OpAMPAgent(interval=30, client=client_mock, callbacks=cb) agent.start() # wait for the queue to be consumed sleep(0.1) # on_message called for connection message assert cb.on_message.call_count == 1 agent.send(payload="payload") # wait for the queue to be consumed sleep(0.1) agent.stop() # on_message called once for connection and once for our message assert cb.on_message.call_count == 2 def test_on_error_called_without_on_message_for_error_response(): cb = mock.create_autospec(OpAMPCallbacks, instance=True) client_mock = mock.Mock() error_response = opamp_pb2.ServerErrorResponse( error_message="server error", ) server_msg = opamp_pb2.ServerToAgent( error_response=error_response, ) # connection message (no error) conn_msg = opamp_pb2.ServerToAgent() client_mock.send.side_effect = [ conn_msg, # connection server_msg, # message with error_response mock.Mock(), # disconnect ] agent = OpAMPAgent(interval=30, client=client_mock, callbacks=cb) agent.start() agent.send(payload="payload") sleep(0.1) agent.stop() # on_message called only for connection (not for error_response message) assert cb.on_message.call_count == 1 # on_error called for the message with error_response cb.on_error.assert_called_once_with(agent, client_mock, error_response) def test_on_error_not_called_without_error_response(): cb = mock.create_autospec(OpAMPCallbacks, instance=True) client_mock = mock.Mock() server_msg = opamp_pb2.ServerToAgent() client_mock.send.side_effect = [ server_msg, # connection server_msg, # message without error_response mock.Mock(), # disconnect ] agent = OpAMPAgent(interval=30, client=client_mock, callbacks=cb) agent.start() agent.send(payload="payload") sleep(0.1) agent.stop() assert cb.on_message.call_count == 2 cb.on_error.assert_not_called() def test_dispatch_order_with_error(): """Verify that error_response skips on_message: on_connect -> on_error.""" call_order = [] client_mock = mock.Mock() error_response = opamp_pb2.ServerErrorResponse( error_message="err", ) server_msg = opamp_pb2.ServerToAgent( error_response=error_response, ) class OrderTrackingCallbacks(OpAMPCallbacks): def on_connect(self, agent, client): call_order.append("on_connect") def on_message(self, agent, client, message): call_order.append("on_message") def on_error(self, agent, client, error_response): call_order.append("on_error") client_mock.send.side_effect = [ server_msg, # connection message with error mock.Mock(), # disconnect ] agent = OpAMPAgent(interval=30, client=client_mock, callbacks=OrderTrackingCallbacks()) agent.start() sleep(0.1) agent.stop() assert call_order == ["on_connect", "on_error"] def test_dispatch_order_without_error(): """Verify normal dispatch order: on_connect -> on_message.""" call_order = [] client_mock = mock.Mock() server_msg = opamp_pb2.ServerToAgent() class OrderTrackingCallbacks(OpAMPCallbacks): def on_connect(self, agent, client): call_order.append("on_connect") def on_message(self, agent, client, message): call_order.append("on_message") def on_error(self, agent, client, error_response): call_order.append("on_error") client_mock.send.side_effect = [ server_msg, # connection message, no error mock.Mock(), # disconnect ] agent = OpAMPAgent(interval=30, client=client_mock, callbacks=OrderTrackingCallbacks()) agent.start() sleep(0.1) agent.stop() assert call_order == ["on_connect", "on_message"] def test_report_full_state_flag_triggers_full_state_send(): cb = mock.create_autospec(OpAMPCallbacks, instance=True) client_mock = mock.Mock() conn_msg = opamp_pb2.ServerToAgent() flag_msg = opamp_pb2.ServerToAgent( flags=opamp_pb2.ServerToAgentFlags_ReportFullState, ) no_flag_msg = opamp_pb2.ServerToAgent() client_mock.send.side_effect = [ conn_msg, # connection flag_msg, # response with ReportFullState no_flag_msg, # full state response no_flag_msg, # disconnect ] client_mock.build_full_state_message.return_value = b"full-state" agent = OpAMPAgent(interval=30, client=client_mock, callbacks=cb) agent.start() agent.send(payload="payload") sleep(0.2) agent.stop() client_mock.build_full_state_message.assert_called() def test_safe_invoke_logs_error(caplog): caplog.set_level(logging.ERROR, logger="opentelemetry._opamp.agent") def bad_callback(): raise ValueError("boom") _safe_invoke(bad_callback) assert any("Error when invoking function 'bad_callback'" in record.message for record in caplog.records) def test_safe_invoke_does_not_propagate(): def bad_callback(): raise RuntimeError("should not propagate") # Should not raise _safe_invoke(bad_callback) def test_can_instantiate_job(): job = Job(payload="payload") assert isinstance(job, Job) def test_job_should_retry(): job = Job(payload="payload") assert job.attempt == 0 assert job.max_retries == 1 assert job.should_retry() is True job.attempt += 1 assert job.should_retry() is True job.attempt += 1 assert job.should_retry() is False def test_job_delay(): job = Job(payload="payload") assert job.initial_backoff == 1 job.attempt = 1 assert job.initial_backoff * 0.8 <= job.delay() <= job.initial_backoff * 1.2 job.attempt = 2 assert 2 * job.initial_backoff * 0.8 <= job.delay() <= 2 * job.initial_backoff * 1.2 job.attempt = 3 assert (2**2) * job.initial_backoff * 0.8 <= job.delay() <= (2**2) * job.initial_backoff * 1.2 def test_job_delay_has_jitter(): job = Job(payload="payload") job.attempt = 1 assert len({job.delay() for i in range(10)}) > 1 opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/tests/opamp/test_callbacks.py000066400000000000000000000014321527070072300337510ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import mock from opentelemetry._opamp.callbacks import MessageData, OpAMPCallbacks from opentelemetry._opamp.proto import opamp_pb2 def test_subclass_override_subset(): class MyCallbacks(OpAMPCallbacks): def __init__(self): self.connected = False def on_connect(self, agent, client): self.connected = True cb = MyCallbacks() cb.on_connect(mock.Mock(), mock.Mock()) assert cb.connected is True # non-overridden methods still work as no-ops cb.on_connect_failed(mock.Mock(), mock.Mock(), Exception()) cb.on_message(mock.Mock(), mock.Mock(), MessageData()) cb.on_error(mock.Mock(), mock.Mock(), opamp_pb2.ServerErrorResponse()) opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py000066400000000000000000000465401527070072300333210ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=no-name-in-module import json import logging from unittest import mock import pytest from opentelemetry._opamp import messages from opentelemetry._opamp.client import _DEFAULT_CAPABILITIES, OpAMPClient from opentelemetry._opamp.exceptions import ( OpAMPRemoteConfigDecodeException, OpAMPRemoteConfigParseException, ) from opentelemetry._opamp.proto import opamp_pb2 from opentelemetry._opamp.proto.anyvalue_pb2 import ( AnyValue as PB2AnyValue, ) from opentelemetry._opamp.proto.anyvalue_pb2 import ( KeyValue as PB2KeyValue, ) from opentelemetry._opamp.transport.requests import RequestsTransport from opentelemetry._opamp.version import __version__ _EFFECTIVE_CONFIG_CAPABILITIES = ( opamp_pb2.AgentCapabilities_ReportsStatus | opamp_pb2.AgentCapabilities_ReportsEffectiveConfig | opamp_pb2.AgentCapabilities_ReportsHeartbeat ) @pytest.fixture(name="client") def client_fixture(): return OpAMPClient(endpoint="url", agent_identifying_attributes={"foo": "bar"}) def test_can_instantiate_opamp_client_with_defaults(): client = OpAMPClient(endpoint="url", agent_identifying_attributes={"foo": "bar"}) assert client assert client._headers == { "Content-Type": "application/x-protobuf", "User-Agent": "OTel-OpAMP-Python/" + __version__, } assert client._tls_certificate is True assert client._tls_client_certificate is None assert client._tls_client_key is None assert client._timeout_millis == 1_000 assert client._sequence_num == 0 assert client._capabilities == _DEFAULT_CAPABILITIES assert isinstance(client._instance_uid, bytes) assert isinstance(client._agent_description, opamp_pb2.AgentDescription) assert client._agent_description.identifying_attributes == [ PB2KeyValue(key="foo", value=PB2AnyValue(string_value="bar")), ] assert client._agent_description.non_identifying_attributes == [] def test_default_capabilities_remain_unchanged(): assert _DEFAULT_CAPABILITIES == ( opamp_pb2.AgentCapabilities_ReportsStatus | opamp_pb2.AgentCapabilities_ReportsHeartbeat | opamp_pb2.AgentCapabilities_AcceptsRemoteConfig | opamp_pb2.AgentCapabilities_ReportsRemoteConfig | opamp_pb2.AgentCapabilities_ReportsEffectiveConfig ) def test_rejects_capabilities_without_reports_status(): with pytest.raises(ValueError, match="ReportsStatus"): OpAMPClient( endpoint="url", agent_identifying_attributes={"foo": "bar"}, capabilities=opamp_pb2.AgentCapabilities_ReportsHeartbeat, ) def test_can_instantiate_opamp_client_all_params(): transport = RequestsTransport() client = OpAMPClient( endpoint="url", headers={"an": "header"}, timeout_millis=2_000, agent_identifying_attributes={"foo": "bar"}, agent_non_identifying_attributes={"bar": "baz"}, transport=transport, tls_certificate="ca.pem", tls_client_certificate="client.pem", tls_client_key="client-key.pem", ) assert client assert client._headers == { "Content-Type": "application/x-protobuf", "User-Agent": "OTel-OpAMP-Python/" + __version__, "an": "header", } assert client._tls_certificate == "ca.pem" assert client._tls_client_certificate == "client.pem" assert client._tls_client_key == "client-key.pem" assert client._timeout_millis == 2_000 assert client._sequence_num == 0 assert isinstance(client._instance_uid, bytes) assert isinstance(client._agent_description, opamp_pb2.AgentDescription) assert client._agent_description.identifying_attributes == [ PB2KeyValue(key="foo", value=PB2AnyValue(string_value="bar")), ] assert client._agent_description.non_identifying_attributes == [ PB2KeyValue(key="bar", value=PB2AnyValue(string_value="baz")), ] assert client._transport is transport def test_client_headers_override_defaults(): client = OpAMPClient( endpoint="url", agent_identifying_attributes={"foo": "bar"}, headers={"User-Agent": "Custom"}, ) client._transport = mock.Mock() client.send(b"") (send_call,) = client._transport.mock_calls assert send_call == mock.call.send( url="url", headers={ "Content-Type": "application/x-protobuf", "User-Agent": "Custom", }, data=b"", timeout_millis=1000, tls_certificate=True, tls_client_certificate=None, tls_client_key=None, ) def test_can_serialize_agent_identifying_attributes(): client = OpAMPClient( endpoint="url", agent_identifying_attributes={ "string": "s", "bytes": b"b", "none": None, "bool": True, "int": 1, "float": 2.0, }, ) data = client.build_full_state_message() message = opamp_pb2.AgentToServer() message.ParseFromString(data) assert message assert message.instance_uid == client._instance_uid assert message.sequence_num == 0 assert message.agent_description.identifying_attributes == [ PB2KeyValue(key="string", value=PB2AnyValue(string_value="s")), PB2KeyValue(key="bytes", value=PB2AnyValue(bytes_value=b"b")), PB2KeyValue(key="none", value=PB2AnyValue()), PB2KeyValue(key="bool", value=PB2AnyValue(bool_value=True)), PB2KeyValue(key="int", value=PB2AnyValue(int_value=1)), PB2KeyValue(key="float", value=PB2AnyValue(double_value=2.0)), ] assert message.agent_description.non_identifying_attributes == [] assert message.capabilities == _DEFAULT_CAPABILITIES def test_build_agent_disconnect_message(client): data = client.build_agent_disconnect_message() message = opamp_pb2.AgentToServer() message.ParseFromString(data) assert message assert message.instance_uid == client._instance_uid assert message.sequence_num == 0 assert message.agent_disconnect == opamp_pb2.AgentDisconnect() assert message.capabilities == _DEFAULT_CAPABILITIES def test_build_heartbeat_message(client): data = client.build_heartbeat_message() message = opamp_pb2.AgentToServer() message.ParseFromString(data) assert message assert message.instance_uid == client._instance_uid assert message.sequence_num == 0 assert message.capabilities == _DEFAULT_CAPABILITIES @pytest.mark.parametrize( "builder_name", [ "build_full_state_message", "build_heartbeat_message", "build_agent_disconnect_message", ], ) def test_message_uses_custom_capabilities(builder_name): client = OpAMPClient( endpoint="url", agent_identifying_attributes={"foo": "bar"}, capabilities=_EFFECTIVE_CONFIG_CAPABILITIES, ) data = getattr(client, builder_name)() message = opamp_pb2.AgentToServer() message.ParseFromString(data) assert message.capabilities == _EFFECTIVE_CONFIG_CAPABILITIES assert not message.capabilities & opamp_pb2.AgentCapabilities_AcceptsRemoteConfig assert not message.capabilities & opamp_pb2.AgentCapabilities_ReportsRemoteConfig assert not message.capabilities & opamp_pb2.AgentCapabilities_ReportsHealth def test_clients_use_capabilities_independently(): default_client = OpAMPClient( endpoint="url", agent_identifying_attributes={"foo": "bar"}, ) custom_client = OpAMPClient( endpoint="url", agent_identifying_attributes={"foo": "bar"}, capabilities=_EFFECTIVE_CONFIG_CAPABILITIES, ) default_message = opamp_pb2.AgentToServer() default_message.ParseFromString(default_client.build_full_state_message()) custom_message = opamp_pb2.AgentToServer() custom_message.ParseFromString(custom_client.build_full_state_message()) assert default_message.capabilities == _DEFAULT_CAPABILITIES assert custom_message.capabilities == _EFFECTIVE_CONFIG_CAPABILITIES def test_update_remote_config_status_without_previous_config(client): remote_config_status = client.update_remote_config_status( remote_config_hash=b"12345678", status=opamp_pb2.RemoteConfigStatuses_APPLIED, ) assert remote_config_status is not None assert remote_config_status.last_remote_config_hash == b"12345678" assert remote_config_status.status == opamp_pb2.RemoteConfigStatuses_APPLIED assert remote_config_status.error_message == "" def test_update_remote_config_status_with_same_config(client): remote_config_status = client.update_remote_config_status( remote_config_hash=b"12345678", status=opamp_pb2.RemoteConfigStatuses_APPLIED, ) assert remote_config_status is not None remote_config_status = client.update_remote_config_status( remote_config_hash=b"12345678", status=opamp_pb2.RemoteConfigStatuses_APPLIED, ) assert remote_config_status is None def test_update_remote_config_status_with_diffent_config(client): remote_config_status = client.update_remote_config_status( remote_config_hash=b"12345678", status=opamp_pb2.RemoteConfigStatuses_APPLIED, ) assert remote_config_status is not None # different status remote_config_status = client.update_remote_config_status( remote_config_hash=b"12345678", status=opamp_pb2.RemoteConfigStatuses_FAILED, ) assert remote_config_status is not None # different error message remote_config_status = client.update_remote_config_status( remote_config_hash=b"12345678", status=opamp_pb2.RemoteConfigStatuses_FAILED, error_message="different error message", ) assert remote_config_status is not None # different hash remote_config_status = client.update_remote_config_status( remote_config_hash=b"1234", status=opamp_pb2.RemoteConfigStatuses_FAILED, error_message="different error message", ) assert remote_config_status is not None def test_build_remote_config_status_response_message_no_error_message(client): remote_config_status = messages.build_remote_config_status_message( last_remote_config_hash=b"12345678", status=opamp_pb2.RemoteConfigStatuses_APPLIED, ) data = client.build_remote_config_status_response_message(remote_config_status) message = opamp_pb2.AgentToServer() message.ParseFromString(data) assert message assert message.instance_uid == client._instance_uid assert message.sequence_num == 0 assert message.capabilities == _DEFAULT_CAPABILITIES assert message.remote_config_status assert message.remote_config_status.last_remote_config_hash == b"12345678" assert message.remote_config_status.status == opamp_pb2.RemoteConfigStatuses_APPLIED assert not message.remote_config_status.error_message def test_build_remote_config_status_response_message_with_error_message( client, ): remote_config_status = messages.build_remote_config_status_message( last_remote_config_hash=b"12345678", status=opamp_pb2.RemoteConfigStatuses_FAILED, error_message="an error message", ) data = client.build_remote_config_status_response_message(remote_config_status) message = opamp_pb2.AgentToServer() message.ParseFromString(data) assert message assert message.instance_uid == client._instance_uid assert message.sequence_num == 0 assert message.capabilities == _DEFAULT_CAPABILITIES assert message.remote_config_status assert message.remote_config_status.last_remote_config_hash == b"12345678" assert message.remote_config_status.status == opamp_pb2.RemoteConfigStatuses_FAILED assert message.remote_config_status.error_message == "an error message" def test_remote_config_status_response_uses_custom_capabilities(): client = OpAMPClient( endpoint="url", agent_identifying_attributes={"foo": "bar"}, capabilities=_EFFECTIVE_CONFIG_CAPABILITIES, ) remote_config_status = messages.build_remote_config_status_message( last_remote_config_hash=b"12345678", status=opamp_pb2.RemoteConfigStatuses_APPLIED, ) data = client.build_remote_config_status_response_message(remote_config_status) message = opamp_pb2.AgentToServer() message.ParseFromString(data) assert message.capabilities == _EFFECTIVE_CONFIG_CAPABILITIES def test_update_effective_config_json_content_type(client): config = {"filename": {"a": "config"}} client.update_effective_config(config, content_type="application/json") assert isinstance(client._effective_config, opamp_pb2.EffectiveConfig) decoded_config = {} for ( file_name, config_file, ) in client._effective_config.config_map.config_map.items(): body = config_file.body.decode() decoded_config[file_name] = json.loads(body) assert config == decoded_config def test_update_effective_config_skips_non_serializable_json_content(client, caplog): caplog.set_level(logging.WARNING, logger="opentelemetry._opamp.messages") effective_config = client.update_effective_config( {"config": {"a": object()}}, content_type="application/json", ) assert effective_config.config_map.config_map == {} message = "Failed to encode effective config body as JSON" exception_records = [record for record in caplog.records if message in record.getMessage()] assert len(exception_records) == 1 assert exception_records[0].exc_info assert exception_records[0].exc_info[0] is TypeError assert "Skipping effective config entry config" in caplog.text def test_update_effective_config_text_content_type(client): config = {"config": "FEATURE_ENABLED=false\n"} effective_config = client.update_effective_config( config, content_type="text/plain", ) config_entry = effective_config.config_map.config_map["config"] assert config_entry.content_type == "text/plain" assert config_entry.body == b"FEATURE_ENABLED=false\n" def test_update_effective_config_bytes_content(client): config = {"config": b"FEATURE_ENABLED=false\n"} effective_config = client.update_effective_config( config, content_type="text/plain", ) config_entry = effective_config.config_map.config_map["config"] assert config_entry.content_type == "text/plain" assert config_entry.body == b"FEATURE_ENABLED=false\n" def test_update_effective_config_skips_unencodable_content(client, caplog): caplog.set_level(logging.WARNING, logger="opentelemetry._opamp.messages") effective_config = client.update_effective_config( {"config": {"a": "config"}}, content_type="text/plain", ) assert effective_config.config_map.config_map == {} assert "Skipping effective config entry config" in caplog.text def test_build_full_state_message_text_effective_config(client): config = {"config": "FEATURE_ENABLED=false\n"} client.update_effective_config(config, content_type="text/plain") data = client.build_full_state_message() message = opamp_pb2.AgentToServer() message.ParseFromString(data) config_entry = message.effective_config.config_map.config_map["config"] assert config_entry.content_type == "text/plain" assert config_entry.body == b"FEATURE_ENABLED=false\n" def test_build_full_state_message(client): client.update_remote_config_status( remote_config_hash=b"12345678", status=opamp_pb2.RemoteConfigStatuses_APPLIED, ) config = {"filename": {"a": "config"}} client.update_effective_config(config, content_type="application/json") data = client.build_full_state_message() message = opamp_pb2.AgentToServer() message.ParseFromString(data) assert message assert message.instance_uid == client._instance_uid assert message.sequence_num == 0 assert message.capabilities == _DEFAULT_CAPABILITIES assert message.agent_description.identifying_attributes == [ PB2KeyValue(key="foo", value=PB2AnyValue(string_value="bar")), ] assert message.remote_config_status assert message.remote_config_status.last_remote_config_hash == b"12345678" assert message.remote_config_status.status == opamp_pb2.RemoteConfigStatuses_APPLIED assert "filename" in message.effective_config.config_map.config_map config_file = message.effective_config.config_map.config_map["filename"] assert config_file.content_type == "application/json" body = config_file.body.decode() assert config["filename"] == json.loads(body) def test_build_full_state_message_no_config(client): data = client.build_full_state_message() message = opamp_pb2.AgentToServer() message.ParseFromString(data) assert message assert message.instance_uid == client._instance_uid assert message.sequence_num == 0 assert message.capabilities == _DEFAULT_CAPABILITIES assert message.agent_description.identifying_attributes == [ PB2KeyValue(key="foo", value=PB2AnyValue(string_value="bar")), ] assert message.remote_config_status assert message.remote_config_status.last_remote_config_hash == b"" assert message.remote_config_status.status == opamp_pb2.RemoteConfigStatuses_UNSET assert message.effective_config.config_map.config_map == {} def test_message_sequence_num_increases_in_send(client): client._transport = mock.Mock() for index in range(2): data = client.build_heartbeat_message() client.send(data) message = opamp_pb2.AgentToServer() message.ParseFromString(data) assert message assert message.sequence_num == index def test_send(client): client._transport = mock.Mock() client.send(b"foo") (send_call,) = client._transport.mock_calls assert send_call == mock.call.send( url="url", headers={ "Content-Type": "application/x-protobuf", "User-Agent": "OTel-OpAMP-Python/" + __version__, }, data=b"foo", timeout_millis=1000, tls_certificate=True, tls_client_certificate=None, tls_client_key=None, ) def test_decode_remote_config(client): config = opamp_pb2.AgentConfigMap() config.config_map["application/json"].body = json.dumps({"a": "config"}).encode() config.config_map["application/json"].content_type = "application/json" config.config_map["text/json"].body = json.dumps({"other": "config"}).encode() config.config_map["text/json"].content_type = "text/json" message = opamp_pb2.AgentRemoteConfig(config=config) decoded = list(client.decode_remote_config(message)) assert sorted(decoded) == sorted( [ ("application/json", {"a": "config"}), ("text/json", {"other": "config"}), ] ) def test_decode_remote_config_invalid_content_type(client): config = opamp_pb2.AgentConfigMap() config.config_map["filename"].body = b"1" config.config_map["filename"].content_type = "not/json" message = opamp_pb2.AgentRemoteConfig(config=config) with pytest.raises(OpAMPRemoteConfigParseException): list(client.decode_remote_config(message)) def test_decode_remote_config_invalid_file_body(client): config = opamp_pb2.AgentConfigMap() config.config_map["filename"].body = b"notjson" config.config_map["filename"].content_type = "application/json" message = opamp_pb2.AgentRemoteConfig(config=config) with pytest.raises(OpAMPRemoteConfigDecodeException): list(client.decode_remote_config(message)) opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/tests/opamp/test_e2e.py000066400000000000000000000060331527070072300325070ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging from time import sleep from unittest import mock import pytest from opentelemetry._opamp.agent import OpAMPAgent from opentelemetry._opamp.callbacks import OpAMPCallbacks from opentelemetry._opamp.client import OpAMPClient from opentelemetry._opamp.proto import opamp_pb2 @pytest.mark.vcr() def test_connection_remote_config_status_heartbeat_disconnection(caplog): caplog.set_level(logging.DEBUG, logger="opentelemetry._opamp.agent") class E2ECallbacks(OpAMPCallbacks): def on_message(self, agent, client, message): logger = logging.getLogger("opentelemetry._opamp.agent.opamp_handler") logger.debug("In opamp_handler") # we need to update the config only if we have a config if message.remote_config is None or not message.remote_config.config_hash: return updated_remote_config = client.update_remote_config_status( remote_config_hash=message.remote_config.config_hash, status=opamp_pb2.RemoteConfigStatuses_APPLIED, error_message="", ) if updated_remote_config is not None: logger.debug("Updated Remote Config") msg = client.build_remote_config_status_response_message(updated_remote_config) agent.send(payload=msg) opamp_client = OpAMPClient( endpoint="https://localhost:4320/v1/opamp", agent_identifying_attributes={ "service.name": "foo", "deployment.environment.name": "foo", }, tls_certificate=False, ) opamp_agent = OpAMPAgent( interval=1, callbacks=E2ECallbacks(), client=opamp_client, ) opamp_agent.start() # this should be enough for the heartbeat message to be sent sleep(1.5) opamp_agent.stop() handler_records = [ record[2] for record in caplog.record_tuples if record[0] == "opentelemetry._opamp.agent.opamp_handler" ] # connection response has ReportFullState flag, triggering a full state send. # on_message is called for: connection, full state response, config status response, heartbeat. assert handler_records == [ "In opamp_handler", "Updated Remote Config", "In opamp_handler", "In opamp_handler", "In opamp_handler", ] @pytest.mark.vcr() def test_with_server_not_responding(caplog): caplog.set_level(logging.DEBUG, logger="opentelemetry._opamp.agent") cb = mock.create_autospec(OpAMPCallbacks, instance=True) opamp_client = OpAMPClient( endpoint="https://localhost:4399/v1/opamp", agent_identifying_attributes={ "service.name": "foo", "deployment.environment.name": "foo", }, tls_certificate=False, ) opamp_agent = OpAMPAgent( interval=1, callbacks=cb, client=opamp_client, ) opamp_agent.start() opamp_agent.stop() assert cb.on_message.call_count == 0 opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/tests/opamp/transport/000077500000000000000000000000001527070072300324555ustar00rootroot00000000000000test_requests.py000066400000000000000000000121521527070072300356630ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opamp/opentelemetry-opamp-client/tests/opamp/transport# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import mock import pytest import requests from opentelemetry._opamp.proto import opamp_pb2 from opentelemetry._opamp.transport.base import base_headers from opentelemetry._opamp.transport.exceptions import OpAMPException from opentelemetry._opamp.transport.requests import RequestsTransport def test_can_instantiate_requests_transport(): transport = RequestsTransport() assert transport def test_can_instantiate_requests_transport_with_own_session(): session = requests.Session() transport = RequestsTransport(session=session) assert transport assert transport.session is session def test_can_send(): transport = RequestsTransport() serialized_message = opamp_pb2.ServerToAgent().SerializeToString() response_mock = mock.Mock(content=serialized_message) headers = {"foo": "bar"} expected_headers = {**base_headers, **headers} data = b"" with mock.patch.object(transport, "session") as session_mock: session_mock.post.return_value = response_mock response = transport.send( url="http://127.0.0.1/v1/opamp", headers=headers, data=data, timeout_millis=1_000, tls_certificate=True, ) session_mock.post.assert_called_once_with( "http://127.0.0.1/v1/opamp", headers=expected_headers, data=data, timeout=1, verify=True, cert=None, ) assert isinstance(response, opamp_pb2.ServerToAgent) def test_send_tls_certificate_mapped_to_verify(): transport = RequestsTransport() serialized_message = opamp_pb2.ServerToAgent().SerializeToString() response_mock = mock.Mock(content=serialized_message) data = b"" with mock.patch.object(transport, "session") as session_mock: session_mock.post.return_value = response_mock response = transport.send( url="https://127.0.0.1/v1/opamp", headers={}, data=data, timeout_millis=1_000, tls_certificate=False, ) session_mock.post.assert_called_once_with( "https://127.0.0.1/v1/opamp", headers=base_headers, data=data, timeout=1, verify=False, cert=None, ) assert isinstance(response, opamp_pb2.ServerToAgent) def test_send_mtls(): transport = RequestsTransport() serialized_message = opamp_pb2.ServerToAgent().SerializeToString() response_mock = mock.Mock(content=serialized_message) data = b"" with mock.patch.object(transport, "session") as session_mock: session_mock.post.return_value = response_mock response = transport.send( url="https://127.0.0.1/v1/opamp", headers={}, data=data, timeout_millis=1_000, tls_certificate="server.pem", tls_client_certificate="client.pem", tls_client_key="client.key", ) session_mock.post.assert_called_once_with( "https://127.0.0.1/v1/opamp", headers=base_headers, data=data, timeout=1, verify="server.pem", cert=("client.pem", "client.key"), ) assert isinstance(response, opamp_pb2.ServerToAgent) def test_send_mtls_no_client_key(): transport = RequestsTransport() serialized_message = opamp_pb2.ServerToAgent().SerializeToString() response_mock = mock.Mock(content=serialized_message) data = b"" with mock.patch.object(transport, "session") as session_mock: session_mock.post.return_value = response_mock response = transport.send( url="https://127.0.0.1/v1/opamp", headers={}, data=data, timeout_millis=1_000, tls_certificate="server.pem", tls_client_certificate="client.pem", ) session_mock.post.assert_called_once_with( "https://127.0.0.1/v1/opamp", headers=base_headers, data=data, timeout=1, verify="server.pem", cert="client.pem", ) assert isinstance(response, opamp_pb2.ServerToAgent) def test_send_exceptions_raises_opamp_exception(): transport = RequestsTransport() response_mock = mock.Mock() headers = {"foo": "bar"} expected_headers = {**base_headers, **headers} data = b"" with mock.patch.object(transport, "session") as session_mock: session_mock.post.return_value = response_mock response_mock.raise_for_status.side_effect = Exception with pytest.raises(OpAMPException): transport.send( url="http://127.0.0.1/v1/opamp", headers=headers, data=data, timeout_millis=1_000, tls_certificate=True, ) session_mock.post.assert_called_once_with( "http://127.0.0.1/v1/opamp", headers=expected_headers, data=data, timeout=1, verify=True, cert=None, ) opentelemetry-python-contrib-0.66b2/opentelemetry-contrib-instrumentations/000077500000000000000000000000001527070072300275235ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-contrib-instrumentations/LICENSE000066400000000000000000000261351527070072300305370ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/opentelemetry-contrib-instrumentations/README.rst000066400000000000000000000012011527070072300312040ustar00rootroot00000000000000OpenTelemetry Instrumentation ============================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-contrib-instrumentations.svg :target: https://pypi.org/project/opentelemetry-contrib-instrumentations/ Installation ------------ :: pip install opentelemetry-contrib-instrumentations This package installs all instrumentation packages hosted by the OpenTelemetry Python Contrib repository. The list of packages can be found `here `_. References ---------- * `OpenTelemetry Project `_ opentelemetry-python-contrib-0.66b2/opentelemetry-contrib-instrumentations/pyproject.toml000066400000000000000000000077611527070072300324520ustar00rootroot00000000000000[build-system] requires = [ "hatchling", ] build-backend = "hatchling.build" [project] name = "opentelemetry-contrib-instrumentations" dynamic = [ "version", ] description = "OpenTelemetry Contrib Instrumentation Packages" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-instrumentation-aio-pika==0.66b2", "opentelemetry-instrumentation-aiohttp-client==0.66b2", "opentelemetry-instrumentation-aiohttp-server==0.66b2", "opentelemetry-instrumentation-aiokafka==0.66b2", "opentelemetry-instrumentation-aiopg==0.66b2", "opentelemetry-instrumentation-asgi==0.66b2", "opentelemetry-instrumentation-asyncclick==0.66b2", "opentelemetry-instrumentation-asyncio==0.66b2", "opentelemetry-instrumentation-asyncpg==0.66b2", "opentelemetry-instrumentation-aws-lambda==0.66b2", "opentelemetry-instrumentation-boto3sqs==0.66b2", "opentelemetry-instrumentation-botocore==0.66b2", "opentelemetry-instrumentation-cassandra==0.66b2", "opentelemetry-instrumentation-celery==0.66b2", "opentelemetry-instrumentation-click==0.66b2", "opentelemetry-instrumentation-confluent-kafka==0.66b2", "opentelemetry-instrumentation-dbapi==0.66b2", "opentelemetry-instrumentation-django==0.66b2", "opentelemetry-instrumentation-exceptions==0.66b2", "opentelemetry-instrumentation-falcon==0.66b2", "opentelemetry-instrumentation-fastapi==0.66b2", "opentelemetry-instrumentation-flask==0.66b2", "opentelemetry-instrumentation-grpc==0.66b2", "opentelemetry-instrumentation-httpx==0.66b2", "opentelemetry-instrumentation-jinja2==0.66b2", "opentelemetry-instrumentation-kafka-python==0.66b2", "opentelemetry-instrumentation-logging==0.66b2", "opentelemetry-instrumentation-mysql==0.66b2", "opentelemetry-instrumentation-mysqlclient==0.66b2", "opentelemetry-instrumentation-pika==0.66b2", "opentelemetry-instrumentation-psycopg==0.66b2", "opentelemetry-instrumentation-psycopg2==0.66b2", "opentelemetry-instrumentation-pymemcache==0.66b2", "opentelemetry-instrumentation-pymongo==0.66b2", "opentelemetry-instrumentation-pymssql==0.66b2", "opentelemetry-instrumentation-pymysql==0.66b2", "opentelemetry-instrumentation-pyramid==0.66b2", "opentelemetry-instrumentation-redis==0.66b2", "opentelemetry-instrumentation-remoulade==0.66b2", "opentelemetry-instrumentation-requests==0.66b2", "opentelemetry-instrumentation-sqlalchemy==0.66b2", "opentelemetry-instrumentation-sqlite3==0.66b2", "opentelemetry-instrumentation-starlette==0.66b2", "opentelemetry-instrumentation-structlog==0.66b2", "opentelemetry-instrumentation-system-metrics==0.66b2", "opentelemetry-instrumentation-threading==0.66b2", "opentelemetry-instrumentation-tornado==0.66b2", "opentelemetry-instrumentation-tortoiseorm==0.66b2", "opentelemetry-instrumentation-urllib==0.66b2", "opentelemetry-instrumentation-urllib3==0.66b2", "opentelemetry-instrumentation-wsgi==0.66b2", ] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/opentelemetry-contrib-instrumentations" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/contrib-instrumentations/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", ] [tool.hatch.build.targets.wheel] packages = [ "src/opentelemetry", ] opentelemetry-python-contrib-0.66b2/opentelemetry-contrib-instrumentations/src/000077500000000000000000000000001527070072300303125ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-contrib-instrumentations/src/opentelemetry/000077500000000000000000000000001527070072300332065ustar00rootroot00000000000000contrib-instrumentations/000077500000000000000000000000001527070072300402135ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-contrib-instrumentations/src/opentelemetryversion.py000066400000000000000000000001441527070072300422510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-contrib-instrumentations/src/opentelemetry/contrib-instrumentations# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" opentelemetry-python-contrib-0.66b2/opentelemetry-distro/000077500000000000000000000000001527070072300237435ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-distro/README.rst000066400000000000000000000007551527070072300254410ustar00rootroot00000000000000OpenTelemetry Distro ==================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-distro.svg :target: https://pypi.org/project/opentelemetry-distro/ Installation ------------ :: pip install opentelemetry-distro This package provides entrypoints to configure OpenTelemetry. References ---------- * `OpenTelemetry Project `_ * `Example using opentelemetry-distro `_ opentelemetry-python-contrib-0.66b2/opentelemetry-distro/pyproject.toml000066400000000000000000000031711527070072300266610ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-distro" dynamic = ["version"] description = "OpenTelemetry Python Distro" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Typing :: Typed", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-instrumentation == 0.66b2", "opentelemetry-sdk ~= 1.13", ] [project.optional-dependencies] otlp = [ "opentelemetry-exporter-otlp == 1.45.2", ] [project.entry-points.opentelemetry_configurator] configurator = "opentelemetry.distro:OpenTelemetryConfigurator" [project.entry-points.opentelemetry_distro] distro = "opentelemetry.distro:OpenTelemetryDistro" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/opentelemetry-distro" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/distro/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/opentelemetry-distro/src/000077500000000000000000000000001527070072300245325ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-distro/src/opentelemetry/000077500000000000000000000000001527070072300274265ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-distro/src/opentelemetry/distro/000077500000000000000000000000001527070072300307325ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-distro/src/opentelemetry/distro/__init__.py000066400000000000000000000017031527070072300330440ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os from opentelemetry.environment_variables import ( OTEL_LOGS_EXPORTER, OTEL_METRICS_EXPORTER, OTEL_TRACES_EXPORTER, ) from opentelemetry.instrumentation.distro import BaseDistro from opentelemetry.sdk._configuration import _OTelSDKConfigurator from opentelemetry.sdk.environment_variables import OTEL_EXPORTER_OTLP_PROTOCOL class OpenTelemetryConfigurator(_OTelSDKConfigurator): pass class OpenTelemetryDistro(BaseDistro): """ The OpenTelemetry provided Distro configures a default set of configuration out of the box. """ # pylint: disable=no-self-use def _configure(self, **kwargs): os.environ.setdefault(OTEL_TRACES_EXPORTER, "otlp") os.environ.setdefault(OTEL_METRICS_EXPORTER, "otlp") os.environ.setdefault(OTEL_LOGS_EXPORTER, "otlp") os.environ.setdefault(OTEL_EXPORTER_OTLP_PROTOCOL, "grpc") opentelemetry-python-contrib-0.66b2/opentelemetry-distro/src/opentelemetry/distro/py.typed000066400000000000000000000000001527070072300324170ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-distro/src/opentelemetry/distro/version.py000066400000000000000000000001441527070072300327700ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" opentelemetry-python-contrib-0.66b2/opentelemetry-distro/test-requirements.txt000066400000000000000000000003541527070072300302060ustar00rootroot00000000000000asgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-instrumentation -e opentelemetry-distro opentelemetry-python-contrib-0.66b2/opentelemetry-distro/tests/000077500000000000000000000000001527070072300251055ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-distro/tests/__init__.py000066400000000000000000000000001527070072300272040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-distro/tests/test_distro.py000066400000000000000000000020701527070072300300210ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore import os from unittest import TestCase, mock from opentelemetry.distro import OpenTelemetryDistro from opentelemetry.environment_variables import ( OTEL_METRICS_EXPORTER, OTEL_TRACES_EXPORTER, ) from opentelemetry.sdk.environment_variables import OTEL_EXPORTER_OTLP_PROTOCOL from opentelemetry.util._importlib_metadata import ( PackageNotFoundError, version, ) class TestDistribution(TestCase): def test_package_available(self): try: version("opentelemetry-distro") except PackageNotFoundError: self.fail("opentelemetry-distro not installed") @mock.patch.dict("os.environ", {}, clear=True) def test_default_configuration(self): distro = OpenTelemetryDistro() distro.configure() self.assertEqual("otlp", os.environ.get(OTEL_TRACES_EXPORTER)) self.assertEqual("otlp", os.environ.get(OTEL_METRICS_EXPORTER)) self.assertEqual("grpc", os.environ.get(OTEL_EXPORTER_OTLP_PROTOCOL)) opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/000077500000000000000000000000001527070072300257025ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/LICENSE000066400000000000000000000261351527070072300267160ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/README.rst000066400000000000000000000135231527070072300273750ustar00rootroot00000000000000OpenTelemetry Instrumentation ============================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation.svg :target: https://pypi.org/project/opentelemetry-instrumentation/ Installation ------------ :: pip install opentelemetry-instrumentation This package provides commands that help automatically instrument a program: .. note:: You need to install a distro package to get auto instrumentation working. The ``opentelemetry-distro`` package contains the default distro and configurator and automatically configures some of the common options for users. For more info about ``opentelemetry-distro`` check `here `__ :: pip install "opentelemetry-distro[otlp]" When creating a custom distro and/or configurator, be sure to add entry points for each under ``opentelemetry_distro`` and ``opentelemetry_configurator`` respectfully. If you have entry points for multiple distros or configurators present in your environment, you should specify the entry point name of the distro and configurator you want to be used via the ``OTEL_PYTHON_DISTRO`` and ``OTEL_PYTHON_CONFIGURATOR`` environment variables. opentelemetry-bootstrap ----------------------- :: opentelemetry-bootstrap [-a |--action=][install|requirements] This command install default instrumentation packages and detects active Python site-packages to figure out which instrumentation packages the user might want to install. By default, it prints out a list of the default and detected instrumentation packages that can be added to a requirements.txt file. It also supports installing the packages when run with :code:`--action=install` or :code:`-a install` flag. All default and detectable instrumentation packages are defined `here `__. opentelemetry-instrument ------------------------ :: opentelemetry-instrument python program.py The instrument command will try to automatically detect packages used by your python program and when possible, apply automatic tracing instrumentation on them. This means your program will get automatic distributed tracing without having to make any code changes. This will also configure a global tracer and tracing exporter as well as a meter and meter exporter. By default, the instrument command will use the OTLP exporter but this can be overridden. The command supports the following configuration options as CLI arguments and environment variables: * ``--traces_exporter`` or ``OTEL_TRACES_EXPORTER`` * ``--metrics_exporter`` or ``OTEL_METRICS_EXPORTER`` * ``--distro`` or ``OTEL_PYTHON_DISTRO`` * ``--configurator`` or ``OTEL_PYTHON_CONFIGURATOR`` The exporter options define what exporter destination to use and can be set to one or more exporter names (see below). You can pass multiple values to configure multiple exporters (e.g., ``zipkin_json,otlp``). - Defaults to ``otlp``. - Can be set to ``none`` to disable automatic tracer initialization. - Can be set to ``console`` to display JSON results locally. Trace exporter names: - jaeger_proto - jaeger_thrift - opencensus - otlp - otlp_proto_grpc (``deprecated``) - otlp_proto_http (``deprecated``) - zipkin_json - zipkin_proto Metric exporter names: - otlp - otlp_proto_grpc (``deprecated``) - prometheus Note: The default transport protocol for ``otlp`` is gRPC. * ``--id-generator`` or ``OTEL_PYTHON_ID_GENERATOR`` Used to specify which IDs Generator to use for the global Tracer Provider. By default, it will use the random IDs generator. The code in ``program.py`` needs to use one of the packages for which there is an OpenTelemetry integration. For a list of the available integrations please check `here `_ * ``OTEL_PYTHON_DISABLED_INSTRUMENTATIONS`` If set by the user, opentelemetry-instrument will read this environment variable to disable specific instrumentations. e.g OTEL_PYTHON_DISABLED_INSTRUMENTATIONS="requests,django" If the variables contains ``*`` as member no instrumentation will be enabled. * ``OTEL_PYTHON_AUTO_INSTRUMENTATION_EXPERIMENTAL_GEVENT_PATCH`` If set by the user to ``patch_all`` , opentelemetry instrument will call the gevent monkeypatching method ``patch_all``. This is considered experimental but can be useful to instrument gevent applications. e.g OTEL_PYTHON_AUTO_INSTRUMENTATION_EXPERIMENTAL_GEVENT_PATCH=patch_all Examples ^^^^^^^^ :: opentelemetry-instrument --traces_exporter console flask run --port=3000 The above command will pass ``--traces_exporter console`` to the instrument command and ``--port=3000`` to ``flask run``. :: opentelemetry-instrument --traces_exporter zipkin_json,otlp celery -A tasks worker --loglevel=info The above command will configure global trace provider, attach zipkin and otlp exporters to it and then start celery with the rest of the arguments. :: opentelemetry-instrument --id_generator random flask run --port=3000 The above command will configure the global trace provider to use the Random IDs Generator, and then pass ``--port=3000`` to ``flask run``. Programmatic Auto-instrumentation --------------------------------- :: from opentelemetry.instrumentation import auto_instrumentation auto_instrumentation.initialize() If you are in an environment where you cannot use opentelemetry-instrument to inject auto-instrumentation you can do so programmatically with the code above. Please note that some instrumentations may require the ``initialize()`` method to be called before the library they instrument is imported. References ---------- * `OpenTelemetry Project `_ opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/pyproject.toml000066400000000000000000000033121527070072300306150ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-instrumentation" dynamic = ["version"] description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.4", "opentelemetry-semantic-conventions == 0.66b2", "wrapt >= 1.0.0, < 3.0.0", "packaging >= 18.0", ] [project.scripts] opentelemetry-bootstrap = "opentelemetry.instrumentation.bootstrap:run" opentelemetry-instrument = "opentelemetry.instrumentation.auto_instrumentation:run" [project.entry-points.opentelemetry_environment_variables] instrumentation = "opentelemetry.instrumentation.environment_variables" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/opentelemetry-instrumentation" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/instrumentation/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/000077500000000000000000000000001527070072300264715ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/000077500000000000000000000000001527070072300313655ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation/000077500000000000000000000000001527070072300346305ustar00rootroot00000000000000_labeler/000077500000000000000000000000001527070072300363165ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation__init__.py000066400000000000000000000046041527070072300404330ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_labeler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ OpenTelemetry Labeler ===================== The labeler utility provides a way to add custom attributes to metrics. This was inspired by OpenTelemetry Go's net/http instrumentation Labeler https://github.com/open-telemetry/opentelemetry-go-contrib/pull/306 Usage ----- The labeler is typically used within the context of an instrumented request or operation. Use ``get_labeler`` to obtain a labeler instance for the current context, then add attributes using the ``add`` or ``add_attributes`` methods. Example ------- Here's a framework-agnostic example showing manual use of the labeler: .. code-block:: python from opentelemetry.instrumentation._labeler import ( enrich_metric_attributes, get_labeler, ) from opentelemetry.metrics import get_meter meter = get_meter("example.manual") duration_histogram = meter.create_histogram( name="http.server.request.duration", unit="s", description="Duration of HTTP server requests.", ) def record_request(user_id: str, duration_s: float) -> None: labeler = get_labeler() labeler.add("user_id", user_id) labeler.add_attributes( { "has_premium": user_id in ["123", "456"], "experiment_group": "control", "feature_enabled": True, "user_segment": "active", } ) base_attributes = { "http.request.method": "GET", "http.response.status_code": 200, } duration_histogram.record( max(duration_s, 0), enrich_metric_attributes(base_attributes), ) This package introduces the shared Labeler API and helper utilities. Framework-specific integration points that call ``enrich_metric_attributes`` (for example before ``Histogram.record``) can be added by individual instrumentors. When instrumentors use ``enrich_metric_attributes``, it does not overwrite base attributes that exist at the same keys. """ from opentelemetry.instrumentation._labeler._internal import ( Labeler, clear_labeler, enrich_metric_attributes, get_labeler, get_labeler_attributes, set_labeler, ) __all__ = [ "Labeler", "clear_labeler", "enrich_metric_attributes", "get_labeler", "get_labeler_attributes", "set_labeler", ] _internal/000077500000000000000000000000001527070072300402715ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_labeler__init__.py000066400000000000000000000206331527070072300424060ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_labeler/_internal# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import threading from collections.abc import Mapping from types import MappingProxyType from typing import Any from opentelemetry.context import attach, create_key, get_value, set_value from opentelemetry.util.types import AttributeValue LABELER_CONTEXT_KEY = create_key("otel_labeler") _logger = logging.getLogger(__name__) class Labeler: """ Stores custom attributes for the current OTel context. This feature is experimental and unstable. """ def __init__(self, max_custom_attrs: int = 20, max_attr_value_length: int = 100): """ Initialize a new Labeler instance. Args: max_custom_attrs: Maximum number of custom attributes to store. When this limit is reached, new attributes will be ignored; existing attributes can still be updated. max_attr_value_length: Maximum length for string attribute values. String values exceeding this length will be truncated. """ self._lock = threading.Lock() self._attributes: dict[str, str | int | float | bool] = {} self._max_custom_attrs = max_custom_attrs self._max_attr_value_length = max_attr_value_length def add(self, key: str, value: Any) -> None: """ Add a single attribute to the labeler, subject to the labeler's limits: - If max_custom_attrs limit is reached and this is a new key, the attribute is ignored - String values exceeding max_attr_value_length are truncated Args: key: attribute key value: attribute value, must be a primitive type: str, int, float, or bool """ if not isinstance(value, (str, int, float, bool)): _logger.warning( "Skipping attribute '%s': value must be str, int, float, or bool, got %s", key, type(value).__name__, ) return with self._lock: if len(self._attributes) >= self._max_custom_attrs and key not in self._attributes: return if isinstance(value, str) and len(value) > self._max_attr_value_length: value = value[: self._max_attr_value_length] self._attributes[key] = value def add_attributes(self, attributes: dict[str, Any]) -> None: """ Add multiple attributes to the labeler, subject to the labeler's limits: - If max_custom_attrs limit is reached and this is a new key, the attribute is ignored - Existing attributes can still be updated - String values exceeding max_attr_value_length are truncated Args: attributes: Dictionary of attributes to add. Values must be primitive types (str, int, float, or bool) """ with self._lock: for key, value in attributes.items(): if not isinstance(value, (str, int, float, bool)): _logger.warning( "Skipping attribute '%s': value must be str, int, float, or bool, got %s", key, type(value).__name__, ) continue if len(self._attributes) >= self._max_custom_attrs and key not in self._attributes: continue if isinstance(value, str) and len(value) > self._max_attr_value_length: value = value[: self._max_attr_value_length] self._attributes[key] = value def get_attributes(self) -> Mapping[str, str | int | float | bool]: """ Return a read-only mapping view of attributes in this labeler. """ with self._lock: return MappingProxyType(self._attributes) def clear(self) -> None: with self._lock: self._attributes.clear() def __len__(self) -> int: with self._lock: return len(self._attributes) def _attach_context_value(value: Labeler | None) -> None: """ Attach a new OpenTelemetry context containing the given labeler value. This helper is fail-safe: context attach errors are suppressed and logged at debug level. Args: value: Labeler instance to store in context, or ``None`` to clear it. """ try: updated_context = set_value(LABELER_CONTEXT_KEY, value) attach(updated_context) except Exception: # pylint: disable=broad-exception-caught _logger.debug("Failed to attach labeler context", exc_info=True) def get_labeler() -> Labeler: """ Get the Labeler instance for the current OTel context. If no Labeler exists in the current context, a new one is created and stored in the context. Returns: Labeler instance for the current OTel context, or a new empty Labeler if no Labeler is currently stored in context. """ try: current_value = get_value(LABELER_CONTEXT_KEY) except Exception: # pylint: disable=broad-exception-caught _logger.debug("Failed to read labeler from context", exc_info=True) current_value = None if isinstance(current_value, Labeler): return current_value labeler = Labeler() _attach_context_value(labeler) return labeler def set_labeler(labeler: Any) -> None: """ Set the Labeler instance for the current OTel context. Args: labeler: The Labeler instance to set """ if not isinstance(labeler, Labeler): _logger.warning( "Skipping set_labeler: value must be Labeler, got %s", type(labeler).__name__, ) return _attach_context_value(labeler) def clear_labeler() -> None: """ Clear the Labeler instance from the current OTel context. This is primarily intended for test isolation or manual context-lifecycle management. In typical framework-instrumented request handling, applications generally should not need to call this directly. """ _attach_context_value(None) def get_labeler_attributes() -> Mapping[str, str | int | float | bool]: """ Get attributes from the current labeler, if any. Returns: Read-only mapping of custom attributes, or an empty read-only mapping if no labeler exists. """ empty_attributes: dict[str, str | int | float | bool] = {} try: current_value = get_value(LABELER_CONTEXT_KEY) except Exception: # pylint: disable=broad-exception-caught _logger.debug("Failed to read labeler attributes from context", exc_info=True) return MappingProxyType(empty_attributes) if not isinstance(current_value, Labeler): return MappingProxyType(empty_attributes) return current_value.get_attributes() def enrich_metric_attributes( base_attributes: dict[str, Any], enrich_enabled: bool = True, ) -> dict[str, AttributeValue]: """ Combines base_attributes with custom attributes from the current labeler, returning a new dictionary of attributes according to the labeler configuration: - Attributes that would override base_attributes are skipped - If max_custom_attrs limit is reached and this is a new key, the attribute is ignored - String values exceeding max_attr_value_length are truncated Args: base_attributes: The base attributes for the metric enrich_enabled: Whether to include custom labeler attributes Returns: Dictionary combining base and custom attributes. If no custom attributes, returns a copy of the original base attributes. """ if not enrich_enabled: return base_attributes.copy() labeler_attributes = get_labeler_attributes() if not labeler_attributes: return base_attributes.copy() try: labeler = get_value(LABELER_CONTEXT_KEY) except Exception: # pylint: disable=broad-exception-caught labeler = None if not isinstance(labeler, Labeler): return base_attributes.copy() enriched_attributes = base_attributes.copy() added_count = 0 for key, value in labeler_attributes.items(): if added_count >= labeler._max_custom_attrs: break if key in base_attributes: continue if isinstance(value, str) and len(value) > labeler._max_attr_value_length: value = value[: labeler._max_attr_value_length] enriched_attributes[key] = value added_count += 1 return enriched_attributes _semconv.py000066400000000000000000000543651527070072300367510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import os import threading from collections.abc import Container, Mapping, MutableMapping from enum import Enum from urllib.parse import urlparse from packaging import version as package_version from opentelemetry.instrumentation.utils import http_status_to_status_code from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_OPERATION, DB_REDIS_DATABASE_INDEX, DB_STATEMENT, DB_SYSTEM, DB_USER, ) from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_FLAVOR, HTTP_HOST, HTTP_METHOD, HTTP_SCHEME, HTTP_SERVER_NAME, HTTP_STATUS_CODE, HTTP_TARGET, HTTP_URL, HTTP_USER_AGENT, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_HOST_NAME, NET_HOST_PORT, NET_PEER_IP, NET_PEER_NAME, NET_PEER_PORT, NET_TRANSPORT, ) from opentelemetry.semconv.attributes.client_attributes import ( CLIENT_ADDRESS, CLIENT_PORT, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_NAMESPACE, DB_OPERATION_NAME, DB_QUERY_TEXT, DB_SYSTEM_NAME, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_REQUEST_METHOD_ORIGINAL, HTTP_RESPONSE_STATUS_CODE, HTTP_ROUTE, ) from opentelemetry.semconv.attributes.network_attributes import ( NETWORK_PROTOCOL_VERSION, NETWORK_TRANSPORT, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.semconv.attributes.url_attributes import ( URL_FULL, URL_PATH, URL_QUERY, URL_SCHEME, ) from opentelemetry.semconv.attributes.user_agent_attributes import ( USER_AGENT_ORIGINAL, ) from opentelemetry.semconv.schemas import Schemas from opentelemetry.trace import Span from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util.types import AttributeValue # Values defined in milliseconds HTTP_DURATION_HISTOGRAM_BUCKETS_OLD = ( 0.0, 5.0, 10.0, 25.0, 50.0, 75.0, 100.0, 250.0, 500.0, 750.0, 1000.0, 2500.0, 5000.0, 7500.0, 10000.0, ) # Values defined in seconds HTTP_DURATION_HISTOGRAM_BUCKETS_NEW = ( 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, ) # These lists represent attributes for metrics that are currently supported _client_duration_attrs_old = [ HTTP_STATUS_CODE, HTTP_HOST, HTTP_METHOD, HTTP_FLAVOR, HTTP_SCHEME, NET_PEER_PORT, NET_PEER_NAME, ] _client_duration_attrs_new = [ ERROR_TYPE, HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, NETWORK_PROTOCOL_VERSION, SERVER_ADDRESS, SERVER_PORT, # TODO: Support opt-in for scheme in new semconv # URL_SCHEME, ] _server_duration_attrs_old = [ HTTP_METHOD, HTTP_HOST, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_FLAVOR, HTTP_SERVER_NAME, NET_HOST_NAME, NET_HOST_PORT, ] _server_duration_attrs_new = [ ERROR_TYPE, HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, HTTP_ROUTE, NETWORK_PROTOCOL_VERSION, URL_SCHEME, ] _server_active_requests_count_attrs_old = [ HTTP_METHOD, HTTP_HOST, HTTP_SCHEME, HTTP_FLAVOR, HTTP_SERVER_NAME, ] _server_active_requests_count_attrs_new = [ HTTP_REQUEST_METHOD, URL_SCHEME, # TODO: Support SERVER_ADDRESS AND SERVER_PORT ] OTEL_SEMCONV_STABILITY_OPT_IN = "OTEL_SEMCONV_STABILITY_OPT_IN" # Legacy/default schema version when schema_url was first introduced _LEGACY_SCHEMA_VERSION = "1.11.0" class _OpenTelemetryStabilitySignalType(Enum): HTTP = "http" DATABASE = "database" GEN_AI = "gen_ai" class _StabilityMode(Enum): DEFAULT = "default" HTTP = "http" HTTP_DUP = "http/dup" DATABASE = "database" DATABASE_DUP = "database/dup" GEN_AI_LATEST_EXPERIMENTAL = "gen_ai_latest_experimental" def _report_new(mode: _StabilityMode): return mode != _StabilityMode.DEFAULT def _report_old(mode: _StabilityMode): return mode not in (_StabilityMode.HTTP, _StabilityMode.DATABASE) class _OpenTelemetrySemanticConventionStability: _initialized = False _lock = threading.Lock() _OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING = {} @classmethod def _initialize(cls): with cls._lock: if cls._initialized: return # Users can pass in comma delimited string for opt-in options # Only values for http, gen ai, and database stability are supported for now opt_in = os.environ.get(OTEL_SEMCONV_STABILITY_OPT_IN) if not opt_in: # early return in case of default cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING = { _OpenTelemetryStabilitySignalType.HTTP: _StabilityMode.DEFAULT, _OpenTelemetryStabilitySignalType.DATABASE: _StabilityMode.DEFAULT, _OpenTelemetryStabilitySignalType.GEN_AI: _StabilityMode.DEFAULT, } cls._initialized = True return opt_in_list = [s.strip() for s in opt_in.split(",")] cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[_OpenTelemetryStabilitySignalType.HTTP] = cls._filter_mode( opt_in_list, _StabilityMode.HTTP, _StabilityMode.HTTP_DUP ) cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[_OpenTelemetryStabilitySignalType.GEN_AI] = cls._filter_mode( opt_in_list, _StabilityMode.DEFAULT, _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL, ) cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[_OpenTelemetryStabilitySignalType.DATABASE] = cls._filter_mode( opt_in_list, _StabilityMode.DATABASE, _StabilityMode.DATABASE_DUP, ) cls._initialized = True @staticmethod def _filter_mode(opt_in_list, stable_mode, dup_mode): # Process semconv stability opt-in # http/dup,database/dup has higher precedence over http,database if dup_mode.value in opt_in_list: return dup_mode return stable_mode if stable_mode.value in opt_in_list else _StabilityMode.DEFAULT @classmethod def _get_opentelemetry_stability_opt_in_mode(cls, signal_type: _OpenTelemetryStabilitySignalType) -> _StabilityMode: # Get OpenTelemetry opt-in mode based off of signal type (http, messaging, etc.) return cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING.get(signal_type, _StabilityMode.DEFAULT) def _get_semconv_opt_in_modes( signal_types: tuple[_OpenTelemetryStabilitySignalType, ...], ) -> dict[_OpenTelemetryStabilitySignalType, _StabilityMode]: """Returns a mapping of signal type to mode for the provided signal_types (one/more of DATABASE, HTTP, GEN_AI). """ _OpenTelemetrySemanticConventionStability._initialize() return { signal_type: _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(signal_type) for signal_type in signal_types } def _filter_semconv_duration_attrs( attrs: Mapping[str, AttributeValue], old_attrs: Container[AttributeValue], new_attrs: Container[AttributeValue], sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ) -> dict[str, AttributeValue]: filtered_attrs = {} # duration is two different metrics depending on sem_conv_opt_in_mode, so no DUP attributes allowed_attributes = new_attrs if sem_conv_opt_in_mode == _StabilityMode.HTTP else old_attrs for key, val in attrs.items(): if key in allowed_attributes: filtered_attrs[key] = val return filtered_attrs def _filter_semconv_active_request_count_attr( attrs: Mapping[str, AttributeValue], old_attrs: Container[AttributeValue], new_attrs: Container[AttributeValue], sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ) -> dict[str, AttributeValue]: filtered_attrs = {} if _report_old(sem_conv_opt_in_mode): for key, val in attrs.items(): if key in old_attrs: filtered_attrs[key] = val if _report_new(sem_conv_opt_in_mode): for key, val in attrs.items(): if key in new_attrs: filtered_attrs[key] = val return filtered_attrs def set_string_attribute( result: MutableMapping[str, AttributeValue], key: str, value: AttributeValue, ) -> None: if value: result[key] = value def set_int_attribute( result: MutableMapping[str, AttributeValue], key: str, value: AttributeValue, ) -> None: if value: try: result[key] = int(value) except (ValueError, TypeError): pass def _set_http_method( result: MutableMapping[str, AttributeValue], original: str, normalized: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: original = original.strip() normalized = normalized.strip() # See https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#common-attributes # Method is case sensitive. "http.request.method_original" should not be sanitized or automatically capitalized. if original != normalized and _report_new(sem_conv_opt_in_mode): set_string_attribute(result, HTTP_REQUEST_METHOD_ORIGINAL, original) if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, HTTP_METHOD, normalized) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, HTTP_REQUEST_METHOD, normalized) def _set_http_status_code( result: MutableMapping[str, AttributeValue], code: str | int, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_int_attribute(result, HTTP_STATUS_CODE, code) if _report_new(sem_conv_opt_in_mode): set_int_attribute(result, HTTP_RESPONSE_STATUS_CODE, code) def _set_http_url( result: MutableMapping[str, AttributeValue], url: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, HTTP_URL, url) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, URL_FULL, url) def _set_http_scheme( result: MutableMapping[str, AttributeValue], scheme: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, HTTP_SCHEME, scheme) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, URL_SCHEME, scheme) def _set_http_flavor_version( result: MutableMapping[str, AttributeValue], version: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, HTTP_FLAVOR, version) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, NETWORK_PROTOCOL_VERSION, version) def _set_http_user_agent( result: MutableMapping[str, AttributeValue], user_agent: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, HTTP_USER_AGENT, user_agent) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, USER_AGENT_ORIGINAL, user_agent) # Client def _set_http_host_client( result: MutableMapping[str, AttributeValue], host: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, HTTP_HOST, host) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, SERVER_ADDRESS, host) def _set_http_net_peer_name_client( result: MutableMapping[str, AttributeValue], peer_name: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, NET_PEER_NAME, peer_name) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, SERVER_ADDRESS, peer_name) def _set_http_peer_port_client( result: MutableMapping[str, AttributeValue], port: str | int, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_int_attribute(result, NET_PEER_PORT, port) if _report_new(sem_conv_opt_in_mode): set_int_attribute(result, SERVER_PORT, port) def _set_http_network_protocol_version( result: MutableMapping[str, AttributeValue], version: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, HTTP_FLAVOR, version) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, NETWORK_PROTOCOL_VERSION, version) # Server def _set_http_net_host( result: MutableMapping[str, AttributeValue], host: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, NET_HOST_NAME, host) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, SERVER_ADDRESS, host) def _set_http_net_host_port( result: MutableMapping[str, AttributeValue], port: str | int, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_int_attribute(result, NET_HOST_PORT, port) if _report_new(sem_conv_opt_in_mode): set_int_attribute(result, SERVER_PORT, port) def _set_http_target( result: MutableMapping[str, AttributeValue], target: str, path: str | None, query: str | None, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, HTTP_TARGET, target) if _report_new(sem_conv_opt_in_mode): if path: set_string_attribute(result, URL_PATH, path) if query: set_string_attribute(result, URL_QUERY, query) def _set_http_host_server( result: MutableMapping[str, AttributeValue], host: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, HTTP_HOST, host) if _report_new(sem_conv_opt_in_mode): if not result.get(SERVER_ADDRESS): set_string_attribute(result, SERVER_ADDRESS, host) # net.peer.ip -> net.sock.peer.addr # https://github.com/open-telemetry/semantic-conventions/blob/40db676ca0e735aa84f242b5a0fb14e49438b69b/schemas/1.15.0#L18 # net.sock.peer.addr -> client.socket.address for server spans (TODO) AND client.address if missing # https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/CHANGELOG.md#v1210-2023-07-13 # https://github.com/open-telemetry/semantic-conventions/blob/main/docs/non-normative/http-migration.md#common-attributes-across-http-client-and-server-spans def _set_http_peer_ip_server( result: MutableMapping[str, AttributeValue], ip: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, NET_PEER_IP, ip) if _report_new(sem_conv_opt_in_mode): # Only populate if not already populated if not result.get(CLIENT_ADDRESS): set_string_attribute(result, CLIENT_ADDRESS, ip) def _set_http_peer_port_server( result: MutableMapping[str, AttributeValue], port: str | int, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_int_attribute(result, NET_PEER_PORT, port) if _report_new(sem_conv_opt_in_mode): set_int_attribute(result, CLIENT_PORT, port) def _set_http_net_peer_name_server( result: MutableMapping[str, AttributeValue], name: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, NET_PEER_NAME, name) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, CLIENT_ADDRESS, name) # Database def _set_db_system( result: MutableMapping[str, AttributeValue], system: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, DB_SYSTEM, system) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, DB_SYSTEM_NAME, system) def _set_db_name( result: MutableMapping[str, AttributeValue], name: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, DB_NAME, name) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, DB_NAMESPACE, name) def _set_db_statement( result: MutableMapping[str, AttributeValue], statement: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: # skip the statement if it's None but set it if it's an empty string if statement is None: return if _report_old(sem_conv_opt_in_mode): result[DB_STATEMENT] = statement if _report_new(sem_conv_opt_in_mode): result[DB_QUERY_TEXT] = statement def _set_db_user( result: MutableMapping[str, AttributeValue], user: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, DB_USER, user) # No new attribute - db.user was removed with no replacement def _set_db_operation( result: MutableMapping[str, AttributeValue], operation: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, DB_OPERATION, operation) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, DB_OPERATION_NAME, operation) def _set_db_redis_database_index( result: MutableMapping[str, AttributeValue], database_index: int, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): if database_index is not None: result[DB_REDIS_DATABASE_INDEX] = int(database_index) # No new attribute - db.redis.database_index was removed with no replacement in semconv 1.38.0 def _set_net_transport( result: MutableMapping[str, AttributeValue], old_transport: AttributeValue, new_transport: AttributeValue, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, NET_TRANSPORT, old_transport) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, NETWORK_TRANSPORT, new_transport) # General def _set_status( span: Span, metrics_attributes: MutableMapping[str, AttributeValue], status_code: int, status_code_str: str, server_span: bool = True, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ) -> None: if status_code < 0: if _report_new(sem_conv_opt_in_mode): metrics_attributes[ERROR_TYPE] = status_code_str if span.is_recording(): if _report_new(sem_conv_opt_in_mode): span.set_attribute(ERROR_TYPE, status_code_str) span.set_status( Status( StatusCode.ERROR, "Non-integer HTTP status: " + status_code_str, ) ) else: status = http_status_to_status_code(status_code, server_span=server_span) if _report_old(sem_conv_opt_in_mode): if span.is_recording(): span.set_attribute(HTTP_STATUS_CODE, status_code) metrics_attributes[HTTP_STATUS_CODE] = status_code if _report_new(sem_conv_opt_in_mode): if span.is_recording(): span.set_attribute(HTTP_RESPONSE_STATUS_CODE, status_code) metrics_attributes[HTTP_RESPONSE_STATUS_CODE] = status_code if status == StatusCode.ERROR: if span.is_recording(): span.set_attribute(ERROR_TYPE, status_code_str) metrics_attributes[ERROR_TYPE] = status_code_str if span.is_recording(): span.set_status(Status(status)) def _get_schema_url(mode: _StabilityMode) -> str: """Get schema version URL for a single signal type's opt-in mode (backwards compatible). For new instrumentations using multiple signal types, use _get_schema_url_for_signal_types() """ if mode is _StabilityMode.DEFAULT: return f"https://opentelemetry.io/schemas/{_LEGACY_SCHEMA_VERSION}" return Schemas.V1_21_0.value def _get_schema_version_for_opt_in_mode( signal_type: _OpenTelemetryStabilitySignalType, mode: _StabilityMode, ) -> str: """Get the schema version for a specific signal type and opt-in mode.""" if mode == _StabilityMode.DEFAULT: return _LEGACY_SCHEMA_VERSION signal_versions = { _OpenTelemetryStabilitySignalType.HTTP: Schemas.V1_21_0.value, _OpenTelemetryStabilitySignalType.DATABASE: Schemas.V1_25_0.value, _OpenTelemetryStabilitySignalType.GEN_AI: Schemas.V1_26_0.value, } schema_url = signal_versions.get(signal_type) if not schema_url: return _LEGACY_SCHEMA_VERSION path = urlparse(schema_url).path schema_version = path.rstrip("/").split("/")[-1] return schema_version or _LEGACY_SCHEMA_VERSION def _get_schema_url_for_signal_types( signal_types: list[_OpenTelemetryStabilitySignalType], ) -> str: """Get the highest applicable schema URL for multiple signal types. Note: Instrumentors should call _OpenTelemetrySemanticConventionStability._initialize() before using this function to ensure proper initialization of stability modes. Args: signal_types: List of signal types used by the instrumentation Returns: Schema URL string representing the highest applicable semconv version """ highest_schema_version = _LEGACY_SCHEMA_VERSION for signal_type in signal_types: mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(signal_type) schema_version = _get_schema_version_for_opt_in_mode(signal_type, mode) # Keep the highest for all signals if package_version.Version(schema_version) > package_version.Version(highest_schema_version): highest_schema_version = schema_version return f"https://opentelemetry.io/schemas/{highest_schema_version}" auto_instrumentation/000077500000000000000000000000001527070072300410445ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation__init__.py000066400000000000000000000141451527070072300431620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from argparse import REMAINDER, ArgumentParser from logging import getLogger from os import environ, execl, getcwd from os.path import abspath, dirname, pathsep from re import sub from shutil import which from opentelemetry.instrumentation.auto_instrumentation._load import ( _load_configurators, _load_distro, _load_instrumentors, ) from opentelemetry.instrumentation.environment_variables import ( OTEL_PYTHON_AUTO_INSTRUMENTATION_EXPERIMENTAL_GEVENT_PATCH, ) from opentelemetry.instrumentation.utils import _python_path_without_directory from opentelemetry.instrumentation.version import __version__ from opentelemetry.util._importlib_metadata import entry_points _logger = getLogger(__name__) def run() -> None: parser = ArgumentParser( description=""" opentelemetry-instrument automatically instruments a Python program and its dependencies and then runs the program. """, epilog=""" Optional arguments (except for --help and --version) for opentelemetry-instrument directly correspond with OpenTelemetry environment variables. The corresponding optional argument is formed by removing the OTEL_ or OTEL_PYTHON_ prefix from the environment variable and lower casing the rest. For example, the optional argument --attribute_value_length_limit corresponds with the environment variable OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT. These optional arguments will override the current value of the corresponding environment variable during the execution of the command. """, ) argument_otel_environment_variable = {} for entry_point in entry_points(group="opentelemetry_environment_variables"): environment_variable_module = entry_point.load() for attribute in dir(environment_variable_module): if attribute.startswith("OTEL_"): argument = sub(r"OTEL_(PYTHON_)?", "", attribute).lower() parser.add_argument( f"--{argument}", required=False, ) argument_otel_environment_variable[argument] = attribute parser.add_argument( "--version", help="print version information", action="version", version="%(prog)s " + __version__, ) parser.add_argument("command", help="Your Python application.") parser.add_argument( "command_args", help="Arguments for your application.", nargs=REMAINDER, ) args = parser.parse_args() for argument, otel_environment_variable in (argument_otel_environment_variable).items(): value = getattr(args, argument) if value is not None: environ[otel_environment_variable] = value python_path = environ.get("PYTHONPATH") if not python_path: python_path = [] else: python_path = python_path.split(pathsep) cwd_path = getcwd() # This is being added to support applications that are being run from their # own executable, like Django. # FIXME investigate if there is another way to achieve this if cwd_path not in python_path: python_path.insert(0, cwd_path) filedir_path = dirname(abspath(__file__)) python_path = [path for path in python_path if path != filedir_path] python_path.insert(0, filedir_path) environ["PYTHONPATH"] = pathsep.join(python_path) executable = which(args.command) execl(executable, executable, *args.command_args) def _initialize(*, swallow_exceptions: bool = True) -> None: # handle optional gevent monkey patching. This is done via environment variables so it may be used from the # opentelemetry operator gevent_patch: str | None = environ.get(OTEL_PYTHON_AUTO_INSTRUMENTATION_EXPERIMENTAL_GEVENT_PATCH) if gevent_patch is not None: if gevent_patch != "patch_all": _logger.error( "%s value must be `patch_all`", OTEL_PYTHON_AUTO_INSTRUMENTATION_EXPERIMENTAL_GEVENT_PATCH, ) else: try: # pylint: disable=import-outside-toplevel from gevent import monkey # noqa: PLC0415 getattr(monkey, gevent_patch)() except ImportError: _logger.exception("Failed to monkey patch with gevent because gevent is not available") if not swallow_exceptions: raise try: distro = _load_distro() distro.configure() _load_configurators() _load_instrumentors(distro) except Exception as exc: # pylint: disable=broad-except _logger.exception("Failed to auto initialize OpenTelemetry") if not swallow_exceptions: raise exc def initialize(*, swallow_exceptions: bool = True) -> None: """ Setup auto-instrumentation, called by the sitecustomize module :param swallow_exceptions: Whether or not to propagate instrumentation exceptions to the caller. Exceptions are logged and swallowed by default. """ filedir = dirname(abspath(__file__)) python_path = environ.get("PYTHONPATH") auto_instrumentation_path_was_present = python_path is not None and filedir in python_path.split(pathsep) # Remove the auto-instrumentation path during initialization to prevent # auto-instrumentation from executing in subprocesses spawned during this phase. # This suppression is performed to avoid creating a recursive loop scenario # where subprocesses spawned in the initialization phase execute the # initialization phase again, spawning more subprocesses. if python_path is not None: environ["PYTHONPATH"] = _python_path_without_directory(python_path, filedir, pathsep) try: _initialize(swallow_exceptions=swallow_exceptions) finally: if auto_instrumentation_path_was_present: current = environ.get("PYTHONPATH", "") if filedir not in current.split(pathsep): environ["PYTHONPATH"] = filedir + pathsep + current if current else filedir _load.py000066400000000000000000000145251527070072300425030ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from functools import cached_property from logging import getLogger from os import environ from opentelemetry.instrumentation.dependencies import ( DependencyConflictError, get_dist_dependency_conflicts, ) from opentelemetry.instrumentation.distro import BaseDistro, DefaultDistro from opentelemetry.instrumentation.environment_variables import ( OTEL_PYTHON_CONFIGURATOR, OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, OTEL_PYTHON_DISTRO, ) from opentelemetry.instrumentation.version import __version__ from opentelemetry.util._importlib_metadata import ( EntryPoint, distributions, entry_points, ) _logger = getLogger(__name__) SKIPPED_INSTRUMENTATIONS_WILDCARD = "*" class _EntryPointDistFinder: @cached_property def _mapping(self): return {self._key_for(ep): dist for dist in distributions() for ep in dist.entry_points} def dist_for(self, entry_point: EntryPoint): dist = getattr(entry_point, "dist", None) if dist: return dist return self._mapping.get(self._key_for(entry_point)) @staticmethod def _key_for(entry_point: EntryPoint): return f"{entry_point.group}:{entry_point.name}:{entry_point.value}" def _load_distro() -> BaseDistro: distro_name = environ.get(OTEL_PYTHON_DISTRO, None) for entry_point in entry_points(group="opentelemetry_distro"): try: # If no distro is specified, use first to come up. if distro_name is None or distro_name == entry_point.name: distro = entry_point.load()() if not isinstance(distro, BaseDistro): _logger.debug( "%s is not an OpenTelemetry Distro. Skipping", entry_point.name, ) continue _logger.debug("Distribution %s will be configured", entry_point.name) return distro except Exception as exc: # pylint: disable=broad-except _logger.exception("Distribution %s configuration failed", entry_point.name) raise exc return DefaultDistro() def _load_instrumentors(distro): package_to_exclude = environ.get(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, []) entry_point_finder = _EntryPointDistFinder() if isinstance(package_to_exclude, str): package_to_exclude = package_to_exclude.split(",") # to handle users entering "requests , flask" or "requests, flask" with spaces package_to_exclude = [x.strip() for x in package_to_exclude] for entry_point in entry_points(group="opentelemetry_pre_instrument"): entry_point.load()() for entry_point in entry_points(group="opentelemetry_instrumentor"): if SKIPPED_INSTRUMENTATIONS_WILDCARD in package_to_exclude: break if entry_point.name in package_to_exclude: _logger.debug("Instrumentation skipped for library %s", entry_point.name) continue try: entry_point_dist = entry_point_finder.dist_for(entry_point) conflict = get_dist_dependency_conflicts(entry_point_dist) if conflict: conflict._log(_logger, entry_point.name) continue # tell instrumentation to not run dep checks again as we already did it above distro.load_instrumentor(entry_point, skip_dep_check=True) _logger.debug("Instrumented %s", entry_point.name) except DependencyConflictError as exc: # Dependency conflicts are generally caught from get_dist_dependency_conflicts # returning a DependencyConflict. Keeping this error handling in case custom # distro and instrumentor behavior raises a DependencyConflictError later. # See https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3610 exc.conflict._log(_logger, entry_point.name) continue except ModuleNotFoundError as exc: # ModuleNotFoundError is raised when the library is not installed # and the instrumentation is not required to be loaded. # See https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3421 _logger.debug("Skipping instrumentation %s: %s", entry_point.name, exc.msg) continue except ImportError: # in scenarios using the kubernetes operator to do autoinstrumentation some # instrumentors (usually requiring binary extensions) may fail to load # because the injected autoinstrumentation code does not match the application # environment regarding python version, libc, etc... In this case it's better # to skip the single instrumentation rather than failing to load everything # so treat differently ImportError than the rest of exceptions _logger.exception("Importing of %s failed, skipping it", entry_point.name) continue except Exception as exc: # pylint: disable=broad-except _logger.exception("Instrumenting of %s failed", entry_point.name) raise exc for entry_point in entry_points(group="opentelemetry_post_instrument"): entry_point.load()() def _load_configurators(): configurator_name = environ.get(OTEL_PYTHON_CONFIGURATOR, None) configured = None for entry_point in entry_points(group="opentelemetry_configurator"): if configured is not None: _logger.warning( "Configuration of %s not loaded, %s already loaded", entry_point.name, configured, ) continue try: if configurator_name is None or configurator_name == entry_point.name: entry_point.load()().configure(auto_instrumentation_version=__version__) # type: ignore configured = entry_point.name else: _logger.warning( "Configuration of %s not loaded because %s is set by %s", entry_point.name, configurator_name, OTEL_PYTHON_CONFIGURATOR, ) except Exception as exc: # pylint: disable=broad-except _logger.exception("Configuration of %s failed", entry_point.name) raise exc sitecustomize.py000066400000000000000000000002451527070072300443260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.instrumentation.auto_instrumentation import initialize initialize() bootstrap.py000066400000000000000000000113021527070072300371350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import argparse import logging import sys from subprocess import ( PIPE, CalledProcessError, Popen, SubprocessError, check_call, ) from packaging.requirements import Requirement from opentelemetry.instrumentation.bootstrap_gen import ( default_instrumentations as gen_default_instrumentations, ) from opentelemetry.instrumentation.bootstrap_gen import ( libraries as gen_libraries, ) from opentelemetry.instrumentation.version import __version__ from opentelemetry.util._importlib_metadata import ( PackageNotFoundError, version, ) logger = logging.getLogger(__name__) def _syscall(func): def wrapper(package=None): try: if package: return func(package) return func() except SubprocessError as exp: cmd = getattr(exp, "cmd", None) if cmd: msg = f'Error calling system command "{" ".join(cmd)}"' if package: msg = f'{msg} for package "{package}"' raise RuntimeError(msg) return wrapper @_syscall def _sys_pip_install(package): # explicit upgrade strategy to override potential pip config try: check_call( [ sys.executable, "-m", "pip", "install", "-U", "--upgrade-strategy", "only-if-needed", package, ] ) except CalledProcessError as error: print(error) def _pip_check(libraries): """Ensures none of the instrumentations have dependency conflicts. Clean check reported as: 'No broken requirements found.' Dependency conflicts are reported as: 'opentelemetry-instrumentation-flask 1.0.1 has requirement opentelemetry-sdk<2.0,>=1.0, but you have opentelemetry-sdk 0.5.' To not be too restrictive, we'll only check for relevant packages. """ with Popen([sys.executable, "-m", "pip", "check"], stdout=PIPE) as check_pipe: pip_check = check_pipe.communicate()[0].decode() pip_check_lower = pip_check.lower() for package_tup in libraries: for package in package_tup: if package.lower() in pip_check_lower: raise RuntimeError(f"Dependency conflict found: {pip_check}") def _is_installed(req): req = Requirement(req) try: dist_version = version(req.name) except PackageNotFoundError: return False if not req.specifier.filter(dist_version): logger.warning( "instrumentation for package %s is available but version %s is installed. Skipping.", req, dist_version, ) return False return True def _find_installed_libraries(default_instrumentations, libraries): yield from default_instrumentations for lib in libraries: if _is_installed(lib["library"]): yield lib["instrumentation"] def _run_requirements(default_instrumentations, libraries): logger.setLevel(logging.ERROR) print("\n".join(_find_installed_libraries(default_instrumentations, libraries))) def _run_install(default_instrumentations, libraries): for lib in _find_installed_libraries(default_instrumentations, libraries): _sys_pip_install(lib) _pip_check(libraries) def run( default_instrumentations: list | None = None, libraries: list | None = None, ) -> None: action_install = "install" action_requirements = "requirements" parser = argparse.ArgumentParser( description=""" opentelemetry-bootstrap detects installed libraries and automatically installs the relevant instrumentation packages for them. """ ) parser.add_argument( "--version", help="print version information", action="version", version="%(prog)s " + __version__, ) parser.add_argument( "-a", "--action", choices=[action_install, action_requirements], default=action_requirements, help=""" install - uses pip to install the new requirements using to the currently active site-package. requirements - prints out the new requirements to stdout. Action can be piped and appended to a requirements.txt file. """, ) args = parser.parse_args() if libraries is None: libraries = gen_libraries if default_instrumentations is None: default_instrumentations = gen_default_instrumentations cmd = { action_install: _run_install, action_requirements: _run_requirements, }[args.action] cmd(default_instrumentations, libraries) bootstrap_gen.py000066400000000000000000000136471527070072300400040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. # RUN `python scripts/generate_instrumentation_bootstrap.py` TO REGENERATE. libraries = [ {"library": "openai >= 1.26.0", "instrumentation": "opentelemetry-instrumentation-openai-v2"}, {"library": "google-cloud-aiplatform >= 1.64", "instrumentation": "opentelemetry-instrumentation-vertexai>=2.0b0"}, {"library": "aio_pika >= 7.2.0, < 11.0.0", "instrumentation": "opentelemetry-instrumentation-aio-pika==0.66b2"}, {"library": "aiohttp ~= 3.0", "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.66b2"}, {"library": "aiohttp ~= 3.0", "instrumentation": "opentelemetry-instrumentation-aiohttp-server==0.66b2"}, {"library": "aiokafka >= 0.8, < 1.0", "instrumentation": "opentelemetry-instrumentation-aiokafka==0.66b2"}, {"library": "aiopg >= 0.13.0, < 2.0.0", "instrumentation": "opentelemetry-instrumentation-aiopg==0.66b2"}, {"library": "asgiref ~= 3.0", "instrumentation": "opentelemetry-instrumentation-asgi==0.66b2"}, {"library": "asyncclick ~= 8.0", "instrumentation": "opentelemetry-instrumentation-asyncclick==0.66b2"}, {"library": "asyncpg >= 0.12.0", "instrumentation": "opentelemetry-instrumentation-asyncpg==0.66b2"}, {"library": "boto3 ~= 1.0", "instrumentation": "opentelemetry-instrumentation-boto3sqs==0.66b2"}, {"library": "botocore ~= 1.0", "instrumentation": "opentelemetry-instrumentation-botocore==0.66b2"}, {"library": "aiobotocore >= 2.0, < 4.0", "instrumentation": "opentelemetry-instrumentation-botocore==0.66b2"}, {"library": "cassandra-driver ~= 3.25", "instrumentation": "opentelemetry-instrumentation-cassandra==0.66b2"}, {"library": "scylla-driver ~= 3.25", "instrumentation": "opentelemetry-instrumentation-cassandra==0.66b2"}, {"library": "celery >= 4.0, < 6.0", "instrumentation": "opentelemetry-instrumentation-celery==0.66b2"}, {"library": "click >= 8.1.3, < 9.0.0", "instrumentation": "opentelemetry-instrumentation-click==0.66b2"}, { "library": "confluent-kafka >= 1.8.2, < 3.0.0", "instrumentation": "opentelemetry-instrumentation-confluent-kafka==0.66b2", }, {"library": "django >= 2.0", "instrumentation": "opentelemetry-instrumentation-django==0.66b2"}, {"library": "falcon >= 1.4.1, < 5.0.0", "instrumentation": "opentelemetry-instrumentation-falcon==0.66b2"}, {"library": "fastapi ~= 0.92", "instrumentation": "opentelemetry-instrumentation-fastapi==0.66b2"}, {"library": "flask >= 1.0", "instrumentation": "opentelemetry-instrumentation-flask==0.66b2"}, {"library": "grpcio >= 1.42.0", "instrumentation": "opentelemetry-instrumentation-grpc==0.66b2"}, {"library": "httpx >= 0.18.0", "instrumentation": "opentelemetry-instrumentation-httpx==0.66b2"}, {"library": "httpx2 >= 2.0.0", "instrumentation": "opentelemetry-instrumentation-httpx==0.66b2"}, {"library": "jinja2 >= 2.7, < 4.0", "instrumentation": "opentelemetry-instrumentation-jinja2==0.66b2"}, {"library": "kafka-python >= 2.0, < 4.0", "instrumentation": "opentelemetry-instrumentation-kafka-python==0.66b2"}, {"library": "mysql-connector-python >= 8.0", "instrumentation": "opentelemetry-instrumentation-mysql==0.66b2"}, {"library": "mysqlclient < 3", "instrumentation": "opentelemetry-instrumentation-mysqlclient==0.66b2"}, {"library": "pika >= 0.12.0", "instrumentation": "opentelemetry-instrumentation-pika==0.66b2"}, {"library": "psycopg >= 3.1.0", "instrumentation": "opentelemetry-instrumentation-psycopg==0.66b2"}, {"library": "psycopg2 >= 2.7.3.1", "instrumentation": "opentelemetry-instrumentation-psycopg2==0.66b2"}, {"library": "psycopg2-binary >= 2.7.3.1", "instrumentation": "opentelemetry-instrumentation-psycopg2==0.66b2"}, {"library": "pymemcache >= 1.3.5, < 5", "instrumentation": "opentelemetry-instrumentation-pymemcache==0.66b2"}, {"library": "pymongo >= 3.1, < 5.0", "instrumentation": "opentelemetry-instrumentation-pymongo==0.66b2"}, {"library": "pymssql >= 2.1.5, < 3", "instrumentation": "opentelemetry-instrumentation-pymssql==0.66b2"}, {"library": "PyMySQL < 2", "instrumentation": "opentelemetry-instrumentation-pymysql==0.66b2"}, {"library": "pyramid >= 1.7", "instrumentation": "opentelemetry-instrumentation-pyramid==0.66b2"}, {"library": "redis >= 2.6", "instrumentation": "opentelemetry-instrumentation-redis==0.66b2"}, {"library": "remoulade >= 0.50", "instrumentation": "opentelemetry-instrumentation-remoulade==0.66b2"}, {"library": "requests ~= 2.0", "instrumentation": "opentelemetry-instrumentation-requests==0.66b2"}, {"library": "sqlalchemy >= 1.0.0, < 2.1.0", "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.66b2"}, {"library": "starlette >= 0.13", "instrumentation": "opentelemetry-instrumentation-starlette==0.66b2"}, {"library": "structlog >= 21.1", "instrumentation": "opentelemetry-instrumentation-structlog==0.66b2"}, {"library": "psutil >= 5", "instrumentation": "opentelemetry-instrumentation-system-metrics==0.66b2"}, {"library": "tornado >= 5.1.1", "instrumentation": "opentelemetry-instrumentation-tornado==0.66b2"}, {"library": "tortoise-orm >= 0.17.0", "instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.66b2"}, {"library": "pydantic >= 1.10.2", "instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.66b2"}, {"library": "urllib3 >= 1.0.0, < 3.0.0", "instrumentation": "opentelemetry-instrumentation-urllib3==0.66b2"}, ] default_instrumentations = [ "opentelemetry-instrumentation-asyncio==0.66b2", "opentelemetry-instrumentation-dbapi==0.66b2", "opentelemetry-instrumentation-exceptions==0.66b2", "opentelemetry-instrumentation-logging==0.66b2", "opentelemetry-instrumentation-sqlite3==0.66b2", "opentelemetry-instrumentation-threading==0.66b2", "opentelemetry-instrumentation-urllib==0.66b2", "opentelemetry-instrumentation-wsgi==0.66b2", ] cidict.py000066400000000000000000000041561527070072300363700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Iterable, Iterator, Mapping, MutableMapping from typing import ( Any, TypeVar, ) KT = TypeVar("KT") VT = TypeVar("VT") class CIDict(MutableMapping[KT, VT]): def __init__( self, data: Mapping[KT, VT] | Iterable[tuple[KT, VT]] | None = None, ) -> None: self._data: dict[KT, tuple[KT, VT]] = {} if data is None: data = {} self.update(data) @staticmethod def _normalize_key(key: KT) -> KT: if isinstance(key, str): return key.lower() # type: ignore return key def _get_entry(self, key: KT) -> tuple[KT, VT]: normalized_key = self._normalize_key(key) if normalized_key in self._data: return self._data[normalized_key] raise KeyError(repr(key)) def original_key(self, key: KT) -> KT: return self._get_entry(key)[0] def normalized_items(self) -> Iterable[tuple[KT, VT]]: return ((key, value[1]) for key, value in self._data.items()) def __setitem__(self, key: KT, value: VT, /) -> None: self._data[self._normalize_key(key)] = (key, value) def __delitem__(self, key: KT, /) -> None: try: del self._data[self._normalize_key(key)] except KeyError: raise KeyError(repr(key)) from None def __getitem__(self, key: KT, /) -> VT: return self._get_entry(key)[1] def __len__(self) -> int: return len(self._data) def __iter__(self) -> Iterator[KT]: return (key for key, _ in self._data.values()) def __repr__(self) -> str: return f"{self.__class__.__name__}({dict(self.items())!r})" def __eq__(self, other: object) -> bool: if isinstance(other, CIDict): return dict(self.normalized_items()) == dict(other.normalized_items()) if not isinstance(other, Mapping): return False ciother: CIDict[Any, Any] = CIDict(other) return dict(self.normalized_items()) == dict(ciother.normalized_items()) dependencies.py000066400000000000000000000171311527070072300375540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Collection from logging import Logger, getLogger from packaging.requirements import InvalidRequirement, Requirement from opentelemetry.util._importlib_metadata import ( Distribution, PackageNotFoundError, version, ) _logger = getLogger(__name__) def _format_deps(deps: Collection[str]) -> str: return ", ".join(str(dep) for dep in deps) class DependencyConflict: """Represents a dependency conflict in OpenTelemetry instrumentation. This class is used to track conflicts between required dependencies and the actual installed packages. It supports two scenarios: 1. Standard conflicts where all dependencies are required 2. Either/or conflicts where only one of a set of dependencies is required Attributes: required: The required dependency specification that conflicts with what's installed. found: The actual dependency that was found installed (if any). required_any: Collection of dependency specifications where any one would satisfy the requirement (for either/or scenarios). found_any: Collection of actual dependencies found for either/or scenarios. """ required: str | None = None found: str | None = None # The following fields are used when an instrumentation requires any of a set of dependencies rather than all. required_any: Collection[str] = None found_any: Collection[str] = None def __init__( self, required: str | None = None, found: str | None = None, required_any: Collection[str] = None, found_any: Collection[str] = None, ): self.required = required self.found = found # The following fields are used when an instrumentation requires any of a set of dependencies rather than all. self.required_any = required_any self.found_any = found_any @property def _is_version_conflict(self) -> bool: """False when the dependency is not installed at all, rather than installed at an incompatible version.""" return bool(self.found or self.found_any) def _log(self, logger: Logger, instrumentor_name: str) -> None: """Log as an error for a version conflict, at debug level when the dependency is not installed.""" log = logger.error if self._is_version_conflict else logger.debug log(self._format_message(instrumentor_name)) def _format_message(self, instrumentor_name: str | None = None) -> str: subject = instrumentor_name or "This instrumentation" if self.required: if self.found: return ( f'{subject} only instruments "{self.required}", ' f'but currently installed version ("{self.found}") falls outside of that range, ' f"so nothing can be instrumented." ) return ( f'{subject} only instruments "{self.required}", ' f"but no installed version was found, so nothing can be instrumented." ) if self.required_any: if self.found_any: return ( f'{subject} instruments any of "{_format_deps(self.required_any)}", ' f'but currently installed version(s) ("{_format_deps(self.found_any)}") fall outside of that range, ' f"so nothing can be instrumented." ) return ( f'{subject} requires any of "{_format_deps(self.required_any)}", ' f"but none are installed, so nothing can be instrumented." ) return f"{subject} has an unknown dependency conflict, so nothing can be instrumented." def __str__(self): return self._format_message() class DependencyConflictError(Exception): conflict: DependencyConflict def __init__(self, conflict: DependencyConflict): self.conflict = conflict def __str__(self): return str(self.conflict) def get_dist_dependency_conflicts( dist: Distribution, ) -> DependencyConflict | None: instrumentation_deps = [] instrumentation_any_deps = [] extra = "extra" instruments = "instruments" instruments_marker = {extra: instruments} instruments_any = "instruments-any" instruments_any_marker = {extra: instruments_any} if dist.requires: for dep in dist.requires: if extra not in dep: continue if instruments not in dep and instruments_any not in dep: continue req = Requirement(dep) if req.marker.evaluate(instruments_marker): # type: ignore instrumentation_deps.append(req) # type: ignore if req.marker.evaluate(instruments_any_marker): # type: ignore instrumentation_any_deps.append(req) # type: ignore return get_dependency_conflicts(instrumentation_deps, instrumentation_any_deps) # type: ignore def get_dependency_conflicts( deps: Collection[str | Requirement], # Dependencies all of which are required deps_any: Collection[str | Requirement] | None = None, # Dependencies any of which are required ) -> DependencyConflict | None: for dep in deps: if isinstance(dep, Requirement): req = dep else: try: req = Requirement(dep) except InvalidRequirement as exc: _logger.warning( 'error parsing dependency, reporting as a conflict: "%s" - %s', dep, exc, ) return DependencyConflict(dep) try: dist_version = version(req.name) except PackageNotFoundError: return DependencyConflict(dep) if not req.specifier.contains(dist_version): return DependencyConflict(dep, f"{req.name} {dist_version}") # If all the dependencies in "instruments" are present, check "instruments-any" for conflicts. if deps_any: return _get_dependency_conflicts_any(deps_any) return None # This is a helper functions designed to ease reading and meet linting requirements. def _get_dependency_conflicts_any( deps_any: Collection[str | Requirement], ) -> DependencyConflict | None: if not deps_any: return None is_dependency_conflict = True required_any: Collection[str] = [] found_any: Collection[str] = [] for dep in deps_any: if isinstance(dep, Requirement): req = dep else: try: req = Requirement(dep) except InvalidRequirement as exc: _logger.warning( 'error parsing dependency, reporting as a conflict: "%s" - %s', dep, exc, ) return DependencyConflict(dep) try: dist_version = version(req.name) except PackageNotFoundError: required_any.append(str(dep)) continue if req.specifier.contains(dist_version): # Since only one of the instrumentation_any dependencies is required, there is no dependency conflict. is_dependency_conflict = False break # If the version does not match, add it to the list of unfulfilled requirement options. required_any.append(str(dep)) found_any.append(f"{req.name} {dist_version}") if is_dependency_conflict: return DependencyConflict( required_any=required_any, found_any=found_any, ) return None distro.py000066400000000000000000000031741527070072300364340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore """ OpenTelemetry Base Distribution (Distro) """ from abc import ABC, abstractmethod from logging import getLogger from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.util._importlib_metadata import EntryPoint _LOG = getLogger(__name__) class BaseDistro(ABC): """An ABC for distro""" _instance = None def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = object.__new__(cls, *args, **kwargs) return cls._instance @abstractmethod def _configure(self, **kwargs): """Configure the distribution""" def configure(self, **kwargs): """Configure the distribution""" self._configure(**kwargs) def load_instrumentor( # pylint: disable=no-self-use self, entry_point: EntryPoint, **kwargs ): """Takes an instrumentation entry point and activates it by instantiating and calling instrument() on it. This is called for each opentelemetry_instrumentor entry point by auto instrumentation. Distros can override this method to customize the behavior by inspecting each entry point and configuring them in special ways, passing additional arguments, load a replacement/fork instead, skip loading entirely, etc. """ instrumentor: BaseInstrumentor = entry_point.load() instrumentor().instrument(**kwargs) class DefaultDistro(BaseDistro): def _configure(self, **kwargs): pass __all__ = ["BaseDistro", "DefaultDistro"] environment_variables.py000066400000000000000000000014771527070072300415300ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "OTEL_PYTHON_DISABLED_INSTRUMENTATIONS" """ .. envvar:: OTEL_PYTHON_DISABLED_INSTRUMENTATIONS """ OTEL_PYTHON_DISTRO = "OTEL_PYTHON_DISTRO" """ .. envvar:: OTEL_PYTHON_DISTRO """ OTEL_PYTHON_CONFIGURATOR = "OTEL_PYTHON_CONFIGURATOR" """ .. envvar:: OTEL_PYTHON_CONFIGURATOR """ OTEL_PYTHON_AUTO_INSTRUMENTATION_EXPERIMENTAL_GEVENT_PATCH = ( "OTEL_PYTHON_AUTO_INSTRUMENTATION_EXPERIMENTAL_GEVENT_PATCH" ) """ .. envvar:: OTEL_PYTHON_AUTO_INSTRUMENTATION_EXPERIMENTAL_GEVENT_PATCH """ OTEL_SEMCONV_STABILITY_OPT_IN = "OTEL_SEMCONV_STABILITY_OPT_IN" """ .. envvar:: OTEL_SEMCONV_STABILITY_OPT_IN Opt-in to stable semantic convention signals. Comma-separated list of signals, e.g. ``http``, ``http/dup``. """ instrumentor.py000066400000000000000000000107201527070072300376740ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore """ OpenTelemetry Base Instrumentor """ from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Collection from logging import getLogger from typing import Any from opentelemetry.instrumentation._semconv import ( _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.dependencies import ( DependencyConflict, DependencyConflictError, get_dependency_conflicts, ) _LOG = getLogger(__name__) class BaseInstrumentor(ABC): """An ABC for instrumentors. Child classes of this ABC should instrument specific third party libraries or frameworks either by using the ``opentelemetry-instrument`` command or by calling their methods directly. Since every third party library or framework is different and has different instrumentation needs, more methods can be added to the child classes as needed to provide practical instrumentation to the end user. """ _instance = None _is_instrumented_by_opentelemetry = False def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = object.__new__(cls) return cls._instance @property def is_instrumented_by_opentelemetry(self): return self._is_instrumented_by_opentelemetry @abstractmethod def instrumentation_dependencies(self) -> Collection[str]: """Return a list of python packages with versions that the will be instrumented. The format should be the same as used in requirements.txt or pyproject.toml. For example, if an instrumentation instruments requests 1.x, this method should look like: def instrumentation_dependencies(self) -> Collection[str]: return ['requests ~= 1.0'] This will ensure that the instrumentation will only be used when the specified library is present in the environment. """ def _instrument(self, **kwargs: Any): """Instrument the library""" @abstractmethod def _uninstrument(self, **kwargs: Any): """Uninstrument the library""" def _check_dependency_conflicts(self) -> DependencyConflict | None: dependencies = self.instrumentation_dependencies() return get_dependency_conflicts(dependencies) def instrument(self, **kwargs: Any): """Instrument the library This method will be called without any optional arguments by the ``opentelemetry-instrument`` command. This means that calling this method directly without passing any optional values should do the very same thing that the ``opentelemetry-instrument`` command does. """ if self._is_instrumented_by_opentelemetry: _LOG.warning("Attempting to instrument while already instrumented") return None # check if instrumentor has any missing or conflicting dependencies skip_dep_check = kwargs.pop("skip_dep_check", False) raise_exception_on_conflict = kwargs.pop("raise_exception_on_conflict", False) if not skip_dep_check: conflict = self._check_dependency_conflicts() if conflict: # auto-instrumentation path: don't log conflict as error, instead # let _load_instrumentors handle the exception if raise_exception_on_conflict: raise DependencyConflictError(conflict) # manual instrumentation path: log the conflict as error _LOG.error(conflict._format_message(self.__class__.__name__)) return None # initialize semantic conventions opt-in if needed _OpenTelemetrySemanticConventionStability._initialize() result = self._instrument( # pylint: disable=assignment-from-no-return **kwargs ) self._is_instrumented_by_opentelemetry = True return result def uninstrument(self, **kwargs: Any): """Uninstrument the library See ``BaseInstrumentor.instrument`` for more information regarding the usage of ``kwargs``. """ if self._is_instrumented_by_opentelemetry: result = self._uninstrument(**kwargs) self._is_instrumented_by_opentelemetry = False return result _LOG.warning("Attempting to uninstrument while already uninstrumented") return None __all__ = ["BaseInstrumentor"] log_utils.py000066400000000000000000000037151527070072300371320ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry._logs import SeverityNumber _STD_TO_OTEL = { 10: SeverityNumber.DEBUG, 11: SeverityNumber.DEBUG2, 12: SeverityNumber.DEBUG3, 13: SeverityNumber.DEBUG4, 14: SeverityNumber.DEBUG4, 15: SeverityNumber.DEBUG4, 16: SeverityNumber.DEBUG4, 17: SeverityNumber.DEBUG4, 18: SeverityNumber.DEBUG4, 19: SeverityNumber.DEBUG4, 20: SeverityNumber.INFO, 21: SeverityNumber.INFO2, 22: SeverityNumber.INFO3, 23: SeverityNumber.INFO4, 24: SeverityNumber.INFO4, 25: SeverityNumber.INFO4, 26: SeverityNumber.INFO4, 27: SeverityNumber.INFO4, 28: SeverityNumber.INFO4, 29: SeverityNumber.INFO4, 30: SeverityNumber.WARN, 31: SeverityNumber.WARN2, 32: SeverityNumber.WARN3, 33: SeverityNumber.WARN4, 34: SeverityNumber.WARN4, 35: SeverityNumber.WARN4, 36: SeverityNumber.WARN4, 37: SeverityNumber.WARN4, 38: SeverityNumber.WARN4, 39: SeverityNumber.WARN4, 40: SeverityNumber.ERROR, 41: SeverityNumber.ERROR2, 42: SeverityNumber.ERROR3, 43: SeverityNumber.ERROR4, 44: SeverityNumber.ERROR4, 45: SeverityNumber.ERROR4, 46: SeverityNumber.ERROR4, 47: SeverityNumber.ERROR4, 48: SeverityNumber.ERROR4, 49: SeverityNumber.ERROR4, 50: SeverityNumber.FATAL, 51: SeverityNumber.FATAL2, 52: SeverityNumber.FATAL3, 53: SeverityNumber.FATAL4, } def std_to_otel(levelno: int) -> SeverityNumber: """ Map a Python log level number as defined in https://docs.python.org/3/library/logging.html#logging-levels to an OTel log severity number as defined in https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-severitynumber """ if levelno < 10: return SeverityNumber.UNSPECIFIED if levelno > 53: return SeverityNumber.FATAL4 return _STD_TO_OTEL[levelno] propagators.py000066400000000000000000000067101527070072300374700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This module implements experimental propagators to inject trace context into response carriers. This is useful for server side frameworks that start traces when server requests and want to share the trace context with the client so the client can add its spans to the same trace. This is part of an upcoming W3C spec and will eventually make it to the Otel spec. https://w3c.github.io/trace-context/#trace-context-http-response-headers-format """ from abc import ABC, abstractmethod from opentelemetry import trace from opentelemetry.context.context import Context from opentelemetry.propagators import textmap from opentelemetry.trace import format_span_id, format_trace_id _HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers" _RESPONSE_PROPAGATOR = None def get_global_response_propagator(): return _RESPONSE_PROPAGATOR def set_global_response_propagator(propagator): global _RESPONSE_PROPAGATOR # pylint:disable=global-statement _RESPONSE_PROPAGATOR = propagator class Setter(ABC): @abstractmethod def set(self, carrier, key, value): """Inject the provided key value pair in carrier.""" class DictHeaderSetter(Setter): def set(self, carrier, key, value): # pylint: disable=no-self-use old_value = carrier.get(key, "") if old_value: value = f"{old_value}, {value}" carrier[key] = value class FuncSetter(Setter): """FuncSetter converts a function into a valid Setter. Any function that can set values in a carrier can be converted into a Setter by using FuncSetter. This is useful when injecting trace context into non-dict objects such HTTP Response objects for different framework. For example, it can be used to create a setter for Falcon response object as: setter = FuncSetter(falcon.api.Response.append_header) and then used with the propagator as: propagator.inject(falcon_response, setter=setter) This would essentially make the propagator call `falcon_response.append_header(key, value)` """ def __init__(self, func): self._func = func def set(self, carrier, key, value): self._func(carrier, key, value) default_setter = DictHeaderSetter() class ResponsePropagator(ABC): @abstractmethod def inject( self, carrier: textmap.CarrierT, context: Context | None = None, setter: textmap.Setter = default_setter, ) -> None: """Injects SpanContext into the HTTP response carrier.""" class TraceResponsePropagator(ResponsePropagator): """Experimental propagator that injects tracecontext into HTTP responses.""" def inject( self, carrier: textmap.CarrierT, context: Context | None = None, setter: textmap.Setter = default_setter, ) -> None: """Injects SpanContext into the HTTP response carrier.""" span = trace.get_current_span(context) span_context = span.get_span_context() if span_context == trace.INVALID_SPAN_CONTEXT: return header_name = "traceresponse" setter.set( carrier, header_name, f"00-{format_trace_id(span_context.trace_id)}-{format_span_id(span_context.span_id)}-{span_context.trace_flags:02x}", ) setter.set( carrier, _HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, header_name, ) py.typed000066400000000000000000000000001527070072300362360ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentationsqlcommenter_utils.py000066400000000000000000000045441527070072300410630ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import itertools import sys from typing import TYPE_CHECKING, Any from opentelemetry import context from opentelemetry.instrumentation.utils import _url_quote if sys.version_info >= (3, 14): from string.templatelib import Template as _Template else: _Template = () if TYPE_CHECKING: if sys.version_info >= (3, 14): from string.templatelib import Template else: from typing import Never Template = Never from typing import overload @overload def _add_sql_comment(sql: str, **meta: Any) -> str: ... @overload def _add_sql_comment(sql: Template, **meta: Any) -> Template: ... def _add_sql_comment(sql: str | Template, **meta: Any) -> str | Template: """ Appends comments to the sql statement and returns it """ meta.update(**_add_framework_tags()) comment = _generate_sql_comment(**meta) if isinstance(sql, _Template): last = sql.strings[-1].rstrip() if last.endswith(";"): last = last[:-1] + comment + ";" else: last += comment args = [ *itertools.chain.from_iterable(zip(sql.strings[:-1], sql.interpolations)), last, ] return _Template(*args) sql = sql.rstrip() if sql.endswith(";"): sql = sql[:-1] + comment + ";" else: sql = sql + comment return sql def _generate_sql_comment(**meta) -> str: """ Return a SQL comment with comma delimited key=value pairs created from **meta kwargs. """ key_value_delimiter = "," if not meta: # No entries added. return "" # Sort the keywords to ensure that caching works and that testing is # deterministic. It eases visual inspection as well. return ( " /*" + key_value_delimiter.join( f"{_url_quote(key)}={_url_quote(value)!r}" for key, value in sorted(meta.items()) if value is not None ) + "*/" ) def _add_framework_tags() -> dict: """ Returns orm related tags if any set by the context """ sqlcommenter_framework_values = ( context.get_value("SQLCOMMENTER_ORM_TAGS_AND_VALUES") if context.get_value("SQLCOMMENTER_ORM_TAGS_AND_VALUES") else {} ) return sqlcommenter_framework_values utils.py000066400000000000000000000152721527070072300362720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import sys import urllib.parse from collections.abc import Generator, Sequence from contextlib import contextmanager from importlib import import_module from re import escape, sub from typing import Any try: # wrapt 2.0.0+ from wrapt import BaseObjectProxy # pylint: disable=no-name-in-module except ImportError: from wrapt import ObjectProxy as BaseObjectProxy from opentelemetry import context, trace # pylint: disable=E0611 # FIXME: fix the importing of these private attributes when the location of the _SUPPRESS_HTTP_INSTRUMENTATION_KEY is defined.= from opentelemetry.context import ( _SUPPRESS_HTTP_INSTRUMENTATION_KEY, _SUPPRESS_INSTRUMENTATION_KEY, ) # pylint: disable=E0611 from opentelemetry.propagate import extract from opentelemetry.trace import StatusCode from opentelemetry.trace.propagation.tracecontext import ( TraceContextTextMapPropagator, ) propagator = TraceContextTextMapPropagator() _SUPPRESS_INSTRUMENTATION_KEY_PLAIN = "suppress_instrumentation" # Set for backward compatibility def extract_attributes_from_object( obj: Any, attributes: Sequence[str], existing: dict[str, str] | None = None ) -> dict[str, str]: extracted: dict[str, str] = {} if existing: extracted.update(existing) for attr in attributes: value = getattr(obj, attr, None) if value is not None: extracted[attr] = str(value) return extracted def http_status_to_status_code( status: int, allow_redirect: bool = True, server_span: bool = False, ) -> StatusCode: """Converts an HTTP status code to an OpenTelemetry canonical status code Args: status (int): HTTP status code """ # See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status if not isinstance(status, int): return StatusCode.UNSET if status < 100: return StatusCode.ERROR if status <= 299: return StatusCode.UNSET if status <= 399 and allow_redirect: return StatusCode.UNSET if status <= 499 and server_span: return StatusCode.UNSET return StatusCode.ERROR def unwrap(obj: object, attr: str): """Given a function that was wrapped by wrapt.wrap_function_wrapper, unwrap it The object containing the function to unwrap may be passed as dotted module path string. Args: obj: Object that holds a reference to the wrapped function or dotted import path as string attr (str): Name of the wrapped function """ if isinstance(obj, str): try: module_path, class_name = obj.rsplit(".", 1) except ValueError as exc: raise ImportError(f"Cannot parse '{obj}' as dotted import path") from exc if module_path not in sys.modules: # Was never imported, meaning it could never have been wrapped return module = import_module(module_path) try: obj = getattr(module, class_name) except AttributeError as exc: raise ImportError(f"Cannot import '{class_name}' from '{module}'") from exc func = getattr(obj, attr, None) if func and isinstance(func, BaseObjectProxy) and hasattr(func, "__wrapped__"): setattr(obj, attr, func.__wrapped__) def _start_internal_or_server_span( tracer, span_name, start_time, context_carrier, context_getter, attributes=None, ): """Returns internal or server span along with the token which can be used by caller to reset context Args: tracer : tracer in use by given instrumentation library span_name (string): name of the span start_time : start time of the span context_carrier : object which contains values that are used to construct a Context. This object must be paired with an appropriate getter which understands how to extract a value from it. context_getter : an object which contains a get function that can retrieve zero or more values from the carrier and a keys function that can get all the keys from carrier. """ token = ctx = span_kind = None if trace.get_current_span() is trace.INVALID_SPAN: ctx = extract(context_carrier, getter=context_getter) token = context.attach(ctx) span_kind = trace.SpanKind.SERVER else: ctx = context.get_current() span_kind = trace.SpanKind.INTERNAL span = tracer.start_span( name=span_name, context=ctx, kind=span_kind, start_time=start_time, attributes=attributes, ) return span, token def _url_quote(s: Any) -> str: # pylint: disable=invalid-name if not isinstance(s, (str, bytes)): return s quoted = urllib.parse.quote(s) # Since SQL uses '%' as a keyword, '%' is a by-product of url quoting # e.g. foo,bar --> foo%2Cbar # thus in our quoting, we need to escape it too to finally give # foo,bar --> foo%%2Cbar return quoted.replace("%", "%%") def _get_opentelemetry_values() -> dict[str, Any]: """ Return the OpenTelemetry Trace and Span IDs if Span ID is set in the OpenTelemetry execution context. """ # Insert the W3C TraceContext generated _headers: dict[str, Any] = {} propagator.inject(_headers) return _headers def _python_path_without_directory(python_path, directory, path_separator): return sub( rf"{escape(directory)}{path_separator}(?!$)", "", python_path, ) def is_instrumentation_enabled() -> bool: return not ( context.get_value(_SUPPRESS_INSTRUMENTATION_KEY) or context.get_value(_SUPPRESS_INSTRUMENTATION_KEY_PLAIN) ) def is_http_instrumentation_enabled() -> bool: return is_instrumentation_enabled() and not context.get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY) @contextmanager def _suppress_instrumentation(*keys: str) -> Generator[None]: """Suppress instrumentation within the context.""" ctx = context.get_current() for key in keys: ctx = context.set_value(key, True, ctx) token = context.attach(ctx) try: yield finally: if token: context.detach(token) @contextmanager def suppress_instrumentation() -> Generator[None]: """Suppress instrumentation within the context.""" with _suppress_instrumentation(_SUPPRESS_INSTRUMENTATION_KEY, _SUPPRESS_INSTRUMENTATION_KEY_PLAIN): yield @contextmanager def suppress_http_instrumentation() -> Generator[None]: """Suppress instrumentation within the context.""" with _suppress_instrumentation(_SUPPRESS_HTTP_INSTRUMENTATION_KEY): yield version.py000066400000000000000000000001441527070072300366070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/src/opentelemetry/instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/test-requirements-wrapt1.txt000066400000000000000000000000471527070072300333600ustar00rootroot00000000000000-r test-requirements.txt wrapt==1.17.3 opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/test-requirements-wrapt2.txt000066400000000000000000000000461527070072300333600ustar00rootroot00000000000000-r test-requirements.txt wrapt==2.1.2 opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/test-requirements.txt000066400000000000000000000003021527070072300321360ustar00rootroot00000000000000asgiref==3.8.1 gevent==25.5.1 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 zipp==3.19.2 -e opentelemetry-instrumentation opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/tests/000077500000000000000000000000001527070072300270445ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/tests/__init__.py000066400000000000000000000000001527070072300311430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/tests/auto_instrumentation/000077500000000000000000000000001527070072300333375ustar00rootroot00000000000000test_initialize.py000066400000000000000000000142021527070072300370310ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/tests/auto_instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore import subprocess import sys from os import environ from os.path import abspath, dirname, pathsep from unittest import TestCase from unittest.mock import MagicMock, patch from opentelemetry.instrumentation import auto_instrumentation # TODO: convert to assertNoLogs instead of mocking logger when 3.10 is baseline class TestInitialize(TestCase): auto_instrumentation_path = dirname(abspath(auto_instrumentation.__file__)) @patch.dict("os.environ", {}, clear=True) @patch("opentelemetry.instrumentation.auto_instrumentation._logger") def test_handles_pythonpath_not_set(self, logger_mock): auto_instrumentation.initialize() self.assertNotIn("PYTHONPATH", environ) logger_mock.exception.assert_not_called() @patch.dict("os.environ", {"PYTHONPATH": "."}) @patch("opentelemetry.instrumentation.auto_instrumentation._logger") def test_handles_pythonpath_set(self, logger_mock): auto_instrumentation.initialize() self.assertEqual(environ["PYTHONPATH"], ".") logger_mock.exception.assert_not_called() @patch.dict( "os.environ", {"PYTHONPATH": auto_instrumentation_path + pathsep + "foo"}, ) @patch("opentelemetry.instrumentation.auto_instrumentation._logger") def test_restores_auto_instrumentation_path_after_init(self, logger_mock): auto_instrumentation.initialize() paths = environ["PYTHONPATH"].split(pathsep) self.assertIn(self.auto_instrumentation_path, paths) self.assertIn("foo", paths) logger_mock.exception.assert_not_called() @patch.dict( "os.environ", {"PYTHONPATH": auto_instrumentation_path + pathsep + "foo"}, ) @patch("opentelemetry.instrumentation.auto_instrumentation._logger") @patch("opentelemetry.instrumentation.auto_instrumentation._load_distro") def test_preserves_pythonpath_changes_during_init(self, load_distro_mock, _logger_mock): def modify_pythonpath(*_): environ["PYTHONPATH"] = environ.get("PYTHONPATH", "") + pathsep + "added_during_init" distro = MagicMock() distro.configure.return_value = None return distro load_distro_mock.side_effect = modify_pythonpath auto_instrumentation.initialize() paths = environ["PYTHONPATH"].split(pathsep) self.assertIn(self.auto_instrumentation_path, paths) self.assertIn("added_during_init", paths) @patch.dict( "os.environ", {"PYTHONPATH": auto_instrumentation_path + pathsep + "foo"}, ) @patch("opentelemetry.instrumentation.auto_instrumentation._logger") @patch("opentelemetry.instrumentation.auto_instrumentation._load_distro") def test_subprocess_sees_pythonpath_changes(self, load_distro_mock, _logger_mock): during_init_paths: list[str] | None = None def capture_pythonpath_in_subprocess(*_): nonlocal during_init_paths result = subprocess.run( [ sys.executable, "-c", "import os; print(os.environ.get('PYTHONPATH', ''))", ], capture_output=True, text=True, check=False, ) raw = result.stdout.strip() during_init_paths = raw.split(pathsep) if raw else [] distro = MagicMock() distro.configure.return_value = None return distro load_distro_mock.side_effect = capture_pythonpath_in_subprocess auto_instrumentation.initialize() self.assertIsNotNone(during_init_paths) self.assertNotIn(self.auto_instrumentation_path, during_init_paths) result_after = subprocess.run( [ sys.executable, "-c", "import os; print(os.environ.get('PYTHONPATH', ''))", ], capture_output=True, text=True, check=False, ) raw_after = result_after.stdout.strip() after_init_paths = raw_after.split(pathsep) if raw_after else [] self.assertIn(self.auto_instrumentation_path, after_init_paths) @patch("opentelemetry.instrumentation.auto_instrumentation._logger") @patch("opentelemetry.instrumentation.auto_instrumentation._load_distro") def test_handles_exceptions(self, load_distro_mock, logger_mock): # pylint:disable=no-self-use load_distro_mock.side_effect = ValueError auto_instrumentation.initialize() logger_mock.exception.assert_called_once_with("Failed to auto initialize OpenTelemetry") @patch("opentelemetry.instrumentation.auto_instrumentation._logger") @patch("opentelemetry.instrumentation.auto_instrumentation._load_distro") def test_reraises_exceptions(self, load_distro_mock, logger_mock): # pylint:disable=no-self-use load_distro_mock.side_effect = ValueError("inner exception") with self.assertRaises(ValueError) as em: auto_instrumentation.initialize(swallow_exceptions=False) logger_mock.exception.assert_called_once_with("Failed to auto initialize OpenTelemetry") self.assertEqual("inner exception", str(em.exception)) @patch.dict( "os.environ", {"OTEL_PYTHON_AUTO_INSTRUMENTATION_EXPERIMENTAL_GEVENT_PATCH": "patch_foo"}, ) @patch("opentelemetry.instrumentation.auto_instrumentation._logger") def test_handles_invalid_gevent_monkeypatch(self, logger_mock): # pylint:disable=no-self-use auto_instrumentation.initialize() logger_mock.error.assert_called_once_with( "%s value must be `patch_all`", "OTEL_PYTHON_AUTO_INSTRUMENTATION_EXPERIMENTAL_GEVENT_PATCH", ) @patch.dict( "os.environ", {"OTEL_PYTHON_AUTO_INSTRUMENTATION_EXPERIMENTAL_GEVENT_PATCH": "patch_all"}, ) @patch("opentelemetry.instrumentation.auto_instrumentation._logger") def test_handles_patch_all_gevent_monkeypatch(self, logger_mock): # pylint:disable=no-self-use auto_instrumentation.initialize() logger_mock.error.assert_not_called() test_load.py000066400000000000000000000463321527070072300356200ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/tests/auto_instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore from unittest import TestCase from unittest.mock import Mock, call, patch from opentelemetry.instrumentation.auto_instrumentation import _load from opentelemetry.instrumentation.dependencies import ( DependencyConflict, ) from opentelemetry.instrumentation.environment_variables import ( OTEL_PYTHON_CONFIGURATOR, OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, OTEL_PYTHON_DISTRO, ) from opentelemetry.instrumentation.version import __version__ from opentelemetry.util._importlib_metadata import EntryPoint, entry_points class TestLoad(TestCase): @patch.dict("os.environ", {OTEL_PYTHON_CONFIGURATOR: "custom_configurator2"}) @patch("opentelemetry.instrumentation.auto_instrumentation._load.entry_points") def test_load_configurators(self, iter_mock): # pylint: disable=no-self-use # Add multiple entry points but only specify the 2nd in the environment variable. ep_mock1 = Mock() ep_mock1.name = "custom_configurator1" configurator_mock1 = Mock() ep_mock1.load.return_value = configurator_mock1 ep_mock2 = Mock() ep_mock2.name = "custom_configurator2" configurator_mock2 = Mock() ep_mock2.load.return_value = configurator_mock2 ep_mock3 = Mock() ep_mock3.name = "custom_configurator3" configurator_mock3 = Mock() ep_mock3.load.return_value = configurator_mock3 iter_mock.return_value = (ep_mock1, ep_mock2, ep_mock3) _load._load_configurators() configurator_mock1.assert_not_called() configurator_mock2().configure.assert_called_once_with(auto_instrumentation_version=__version__) configurator_mock3.assert_not_called() @patch.dict("os.environ", {OTEL_PYTHON_CONFIGURATOR: "custom_configurator2"}) @patch("opentelemetry.instrumentation.auto_instrumentation._load.entry_points") def test_load_configurators_no_ep(self, iter_mock): # pylint: disable=no-self-use iter_mock.return_value = () # Confirm method does not crash if not entry points exist. _load._load_configurators() @patch.dict("os.environ", {OTEL_PYTHON_CONFIGURATOR: "custom_configurator2"}) @patch("opentelemetry.instrumentation.auto_instrumentation._load.entry_points") def test_load_configurators_error(self, iter_mock): # Add multiple entry points but only specify the 2nd in the environment variable. ep_mock1 = Mock() ep_mock1.name = "custom_configurator1" configurator_mock1 = Mock() ep_mock1.load.return_value = configurator_mock1 ep_mock2 = Mock() ep_mock2.name = "custom_configurator2" configurator_mock2 = Mock() configurator_mock2().configure.side_effect = Exception() ep_mock2.load.return_value = configurator_mock2 ep_mock3 = Mock() ep_mock3.name = "custom_configurator3" configurator_mock3 = Mock() ep_mock3.load.return_value = configurator_mock3 iter_mock.return_value = (ep_mock1, ep_mock2, ep_mock3) # Confirm failed configuration raises exception. self.assertRaises(Exception, _load._load_configurators) @patch.dict("os.environ", {OTEL_PYTHON_DISTRO: "custom_distro2"}) @patch("opentelemetry.instrumentation.auto_instrumentation._load.isinstance") @patch("opentelemetry.instrumentation.auto_instrumentation._load.entry_points") def test_load_distro(self, iter_mock, isinstance_mock): # Add multiple entry points but only specify the 2nd in the environment variable. ep_mock1 = Mock() ep_mock1.name = "custom_distro1" distro_mock1 = Mock() ep_mock1.load.return_value = distro_mock1 ep_mock2 = Mock() ep_mock2.name = "custom_distro2" distro_mock2 = Mock() ep_mock2.load.return_value = distro_mock2 ep_mock3 = Mock() ep_mock3.name = "custom_distro3" distro_mock3 = Mock() ep_mock3.load.return_value = distro_mock3 iter_mock.return_value = (ep_mock1, ep_mock2, ep_mock3) # Mock entry points to be instances of BaseDistro. isinstance_mock.return_value = True self.assertEqual( _load._load_distro(), distro_mock2(), ) @patch.dict("os.environ", {OTEL_PYTHON_DISTRO: "custom_distro2"}) @patch("opentelemetry.instrumentation.auto_instrumentation._load.isinstance") @patch("opentelemetry.instrumentation.auto_instrumentation._load.DefaultDistro") @patch("opentelemetry.instrumentation.auto_instrumentation._load.entry_points") def test_load_distro_not_distro(self, iter_mock, default_distro_mock, isinstance_mock): # Add multiple entry points but only specify the 2nd in the environment variable. ep_mock1 = Mock() ep_mock1.name = "custom_distro1" distro_mock1 = Mock() ep_mock1.load.return_value = distro_mock1 ep_mock2 = Mock() ep_mock2.name = "custom_distro2" distro_mock2 = Mock() ep_mock2.load.return_value = distro_mock2 ep_mock3 = Mock() ep_mock3.name = "custom_distro3" distro_mock3 = Mock() ep_mock3.load.return_value = distro_mock3 iter_mock.return_value = (ep_mock1, ep_mock2, ep_mock3) # Confirm default distro is used if specified entry point is not a BaseDistro isinstance_mock.return_value = False self.assertEqual( _load._load_distro(), default_distro_mock(), ) @patch.dict("os.environ", {OTEL_PYTHON_DISTRO: "custom_distro2"}) @patch("opentelemetry.instrumentation.auto_instrumentation._load.DefaultDistro") @patch("opentelemetry.instrumentation.auto_instrumentation._load.entry_points") def test_load_distro_no_ep(self, iter_mock, default_distro_mock): iter_mock.return_value = () # Confirm default distro is used if there are no entry points. self.assertEqual( _load._load_distro(), default_distro_mock(), ) @patch.dict("os.environ", {OTEL_PYTHON_DISTRO: "custom_distro2"}) @patch("opentelemetry.instrumentation.auto_instrumentation._load.isinstance") @patch("opentelemetry.instrumentation.auto_instrumentation._load.entry_points") def test_load_distro_error(self, iter_mock, isinstance_mock): ep_mock1 = Mock() ep_mock1.name = "custom_distro1" distro_mock1 = Mock() ep_mock1.load.return_value = distro_mock1 ep_mock2 = Mock() ep_mock2.name = "custom_distro2" distro_mock2 = Mock() distro_mock2.side_effect = Exception() ep_mock2.load.return_value = distro_mock2 ep_mock3 = Mock() ep_mock3.name = "custom_distro3" distro_mock3 = Mock() ep_mock3.load.return_value = distro_mock3 iter_mock.return_value = (ep_mock1, ep_mock2, ep_mock3) isinstance_mock.return_value = True # Confirm method raises exception if it fails to load a distro. self.assertRaises(Exception, _load._load_distro) @staticmethod def _instrumentation_failed_to_load_call(entry_point, dependency_conflict): return call(dependency_conflict._format_message(entry_point)) @patch.dict( "os.environ", {OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: " instr1 , instr3 "}, ) @patch("opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts") @patch("opentelemetry.instrumentation.auto_instrumentation._load.entry_points") def test_load_instrumentors(self, iter_mock, mock_dep): # Mock opentelemetry_pre_instrument entry points # pylint: disable=too-many-locals pre_ep_mock1 = Mock() pre_ep_mock1.name = "pre1" pre_mock1 = Mock() pre_ep_mock1.load.return_value = pre_mock1 pre_ep_mock2 = Mock() pre_ep_mock2.name = "pre2" pre_mock2 = Mock() pre_ep_mock2.load.return_value = pre_mock2 # Mock opentelemetry_instrumentor entry points ep_mock1 = Mock() ep_mock1.name = "instr1" ep_mock2 = Mock() ep_mock2.name = "instr2" ep_mock3 = Mock() ep_mock3.name = "instr3" ep_mock4 = Mock() ep_mock4.name = "instr4" # Mock opentelemetry_instrumentor entry points post_ep_mock1 = Mock() post_ep_mock1.name = "post1" post_mock1 = Mock() post_ep_mock1.load.return_value = post_mock1 post_ep_mock2 = Mock() post_ep_mock2.name = "post2" post_mock2 = Mock() post_ep_mock2.load.return_value = post_mock2 distro_mock = Mock() # Mock entry points in order iter_mock.side_effect = [ (pre_ep_mock1, pre_ep_mock2), (ep_mock1, ep_mock2, ep_mock3, ep_mock4), (post_ep_mock1, post_ep_mock2), ] # No dependency conflict mock_dep.return_value = None _load._load_instrumentors(distro_mock) # All opentelemetry_pre_instrument entry points should be loaded pre_mock1.assert_called_once() pre_mock2.assert_called_once() self.assertEqual(iter_mock.call_count, 3) # Only non-disabled instrumentations should be loaded distro_mock.load_instrumentor.assert_has_calls( [ call(ep_mock2, skip_dep_check=True), call(ep_mock4, skip_dep_check=True), ] ) self.assertEqual(distro_mock.load_instrumentor.call_count, 2) # All opentelemetry_post_instrument entry points should be loaded post_mock1.assert_called_once() post_mock2.assert_called_once() @patch.dict( "os.environ", {OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: " instr1 , instr3 "}, ) @patch("opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts") @patch("opentelemetry.instrumentation.auto_instrumentation._load._logger") @patch("opentelemetry.instrumentation.auto_instrumentation._load.entry_points") def test_load_instrumentors_dep_conflict(self, iter_mock, mock_logger, mock_dep): # pylint: disable=no-self-use ep_mock1 = Mock() ep_mock1.name = "instr1" # disabled ep_mock2 = Mock() ep_mock2.name = "instr2" ep_mock3 = Mock() ep_mock3.name = "instr3" # disabled ep_mock4 = Mock() ep_mock4.name = "instr4" # dependency conflict dependency_conflict = DependencyConflict("1.2.3", "1.2.0") distro_mock = Mock() iter_mock.return_value = (ep_mock1, ep_mock2, ep_mock3, ep_mock4) print((ep_mock1, ep_mock2, ep_mock3, ep_mock4)) # If a dependency conflict is raised, that instrumentation should not be loaded, but others still should. # In this case, ep_mock4 will not be loaded and ep_mock2 will succeed. (ep_mock1 and ep_mock3 are disabled) mock_dep.side_effect = [None, dependency_conflict] _load._load_instrumentors(distro_mock) distro_mock.load_instrumentor.assert_has_calls( [ call(ep_mock2, skip_dep_check=True), ] ) distro_mock.load_instrumentor.assert_called_once() mock_logger.debug.assert_has_calls( [ call( "Instrumentation skipped for library %s", ep_mock1.name, ), call("Instrumented %s", ep_mock2.name), call( "Instrumentation skipped for library %s", ep_mock3.name, ), ] ) mock_logger.error.assert_has_calls( [ self._instrumentation_failed_to_load_call( ep_mock4.name, dependency_conflict, ), ] ) @patch("opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts") @patch("opentelemetry.instrumentation.auto_instrumentation._load._logger") @patch("opentelemetry.instrumentation.auto_instrumentation._load.entry_points") def test_load_instrumentors_dep_not_installed(self, iter_mock, mock_logger, mock_dep): # Mock opentelemetry_instrumentor entry points ep_mock1 = Mock() ep_mock1.name = "instr1" ep_mock2 = Mock() ep_mock2.name = "instr2" dependency_conflict = DependencyConflict("1.2.3", None) distro_mock = Mock() iter_mock.return_value = (ep_mock1, ep_mock2) mock_dep.side_effect = [None, dependency_conflict] _load._load_instrumentors(distro_mock) distro_mock.load_instrumentor.assert_called_once_with(ep_mock1, skip_dep_check=True) mock_logger.debug.assert_has_calls( [ call("Instrumented %s", ep_mock1.name), self._instrumentation_failed_to_load_call( ep_mock2.name, dependency_conflict, ), ] ) mock_logger.error.assert_not_called() @patch("opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts") @patch("opentelemetry.instrumentation.auto_instrumentation._load._logger") @patch("opentelemetry.instrumentation.auto_instrumentation._load.entry_points") def test_load_instrumentors_import_error_does_not_stop_everything(self, iter_mock, mock_logger, mock_dep): ep_mock1 = Mock(name="instr1") ep_mock2 = Mock(name="instr2") distro_mock = Mock() distro_mock.load_instrumentor.side_effect = [ImportError, None] # Mock entry points in order iter_mock.side_effect = [ (), (ep_mock1, ep_mock2), (), ] mock_dep.return_value = None _load._load_instrumentors(distro_mock) distro_mock.load_instrumentor.assert_has_calls( [ call(ep_mock1, skip_dep_check=True), call(ep_mock2, skip_dep_check=True), ] ) self.assertEqual(distro_mock.load_instrumentor.call_count, 2) mock_logger.exception.assert_any_call( "Importing of %s failed, skipping it", ep_mock1.name, ) mock_logger.debug.assert_any_call("Instrumented %s", ep_mock2.name) @patch("opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts") @patch("opentelemetry.instrumentation.auto_instrumentation._load.entry_points") def test_load_instrumentors_raises_exception(self, iter_mock, mock_dep): ep_mock1 = Mock(name="instr1") ep_mock2 = Mock(name="instr2") distro_mock = Mock() distro_mock.load_instrumentor.side_effect = [ValueError, None] # Mock entry points in order iter_mock.side_effect = [ (), (ep_mock1, ep_mock2), (), ] mock_dep.return_value = None with self.assertRaises(ValueError): _load._load_instrumentors(distro_mock) distro_mock.load_instrumentor.assert_has_calls( [ call(ep_mock1, skip_dep_check=True), ] ) self.assertEqual(distro_mock.load_instrumentor.call_count, 1) @patch("opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts") @patch("opentelemetry.instrumentation.auto_instrumentation._load._logger") @patch("opentelemetry.instrumentation.auto_instrumentation._load.entry_points") def test_load_instrumentors_module_not_found_error(self, iter_mock, mock_logger, mock_dep): ep_mock1 = Mock() ep_mock1.name = "instr1" ep_mock2 = Mock() ep_mock2.name = "instr2" distro_mock = Mock() mock_dep.return_value = None distro_mock.load_instrumentor.side_effect = [ ModuleNotFoundError("No module named 'fake_module'"), None, ] iter_mock.side_effect = [(), (ep_mock1, ep_mock2), ()] _load._load_instrumentors(distro_mock) distro_mock.load_instrumentor.assert_has_calls( [ call(ep_mock1, skip_dep_check=True), call(ep_mock2, skip_dep_check=True), ] ) self.assertEqual(distro_mock.load_instrumentor.call_count, 2) mock_logger.debug.assert_any_call( "Skipping instrumentation %s: %s", "instr1", "No module named 'fake_module'", ) mock_logger.debug.assert_any_call("Instrumented %s", ep_mock2.name) def test_load_instrumentors_no_entry_point_mocks(self): distro_mock = Mock() _load._load_instrumentors(distro_mock) # this has no specific assert because it is run for every instrumentation self.assertTrue(distro_mock) def test_entry_point_dist_finder(self): entry_point_finder = _load._EntryPointDistFinder() self.assertTrue(entry_point_finder._mapping) entry_point = list(entry_points(group="opentelemetry_environment_variables"))[0] self.assertTrue(entry_point) self.assertTrue(entry_point.dist) # this will not hit cache entry_point_dist = entry_point_finder.dist_for(entry_point) self.assertTrue(entry_point_dist) # dist are not comparable so we are sure we are not hitting the cache self.assertEqual(entry_point.dist, entry_point_dist) # this will hit cache entry_point_without_dist = EntryPoint( name=entry_point.name, group=entry_point.group, value=entry_point.value, ) self.assertIsNone(entry_point_without_dist.dist) new_entry_point_dist = entry_point_finder.dist_for(entry_point_without_dist) # dist are not comparable, being truthy is enough self.assertTrue(new_entry_point_dist) @patch.dict( "os.environ", {OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "*"}, ) @patch("opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts") @patch("opentelemetry.instrumentation.auto_instrumentation._load.entry_points") def test_no_instrumentor_called_with_wildcard(self, iter_mock, mock_dep): # Mock opentelemetry_pre_instrument entry points # pylint: disable=too-many-locals pre_ep_mock1 = Mock() pre_ep_mock1.name = "pre1" pre_mock1 = Mock() pre_ep_mock1.load.return_value = pre_mock1 # Mock opentelemetry_instrumentor entry points ep_mock1 = Mock() ep_mock1.name = "instr1" # Mock opentelemetry_instrumentor entry points post_ep_mock1 = Mock() post_ep_mock1.name = "post1" post_mock1 = Mock() post_ep_mock1.load.return_value = post_mock1 distro_mock = Mock() # Mock entry points in order iter_mock.side_effect = [ (pre_ep_mock1,), (ep_mock1,), (post_ep_mock1,), ] _load._load_instrumentors(distro_mock) self.assertEqual(iter_mock.call_count, 3) # All opentelemetry_pre_instrument entry points should be loaded pre_mock1.assert_called_once() # No instrumentations should be loaded mock_dep.assert_not_called() distro_mock.load_instrumentor.assert_not_called() self.assertEqual(distro_mock.load_instrumentor.call_count, 0) # All opentelemetry_post_instrument entry points should be loaded post_mock1.assert_called_once() test_run.py000066400000000000000000000063151527070072300355020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/tests/auto_instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore from os import environ, getcwd from os.path import abspath, dirname, pathsep from unittest import TestCase from unittest.mock import patch from opentelemetry.environment_variables import OTEL_TRACES_EXPORTER from opentelemetry.instrumentation import auto_instrumentation class TestRun(TestCase): auto_instrumentation_path = dirname(abspath(auto_instrumentation.__file__)) @classmethod def setUpClass(cls): cls.execl_patcher = patch("opentelemetry.instrumentation.auto_instrumentation.execl") cls.which_patcher = patch("opentelemetry.instrumentation.auto_instrumentation.which") cls.execl_patcher.start() cls.which_patcher.start() @classmethod def tearDownClass(cls): cls.execl_patcher.stop() cls.which_patcher.stop() @patch("sys.argv", ["instrument", ""]) @patch.dict("os.environ", {"PYTHONPATH": ""}) def test_empty(self): auto_instrumentation.run() self.assertEqual( environ["PYTHONPATH"], pathsep.join([self.auto_instrumentation_path, getcwd()]), ) @patch("sys.argv", ["instrument", ""]) @patch.dict("os.environ", {"PYTHONPATH": "abc"}) def test_non_empty(self): auto_instrumentation.run() self.assertEqual( environ["PYTHONPATH"], pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), ) @patch("sys.argv", ["instrument", ""]) @patch.dict( "os.environ", {"PYTHONPATH": pathsep.join(["abc", auto_instrumentation_path])}, ) def test_after_path(self): auto_instrumentation.run() self.assertEqual( environ["PYTHONPATH"], pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), ) @patch("sys.argv", ["instrument", ""]) @patch.dict( "os.environ", {"PYTHONPATH": pathsep.join([auto_instrumentation_path, "abc", auto_instrumentation_path])}, ) def test_single_path(self): auto_instrumentation.run() self.assertEqual( environ["PYTHONPATH"], pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), ) class TestExecl(TestCase): @patch("sys.argv", ["1", "2", "3"]) @patch("opentelemetry.instrumentation.auto_instrumentation.which") @patch("opentelemetry.instrumentation.auto_instrumentation.execl") def test_execl(self, mock_execl, mock_which): # pylint: disable=no-self-use mock_which.configure_mock(return_value="python") auto_instrumentation.run() mock_execl.assert_called_with("python", "python", "3") class TestArgs(TestCase): @patch("opentelemetry.instrumentation.auto_instrumentation.execl") def test_exporter(self, _): # pylint: disable=no-self-use with patch("sys.argv", ["instrument", "2"]): auto_instrumentation.run() self.assertIsNone(environ.get(OTEL_TRACES_EXPORTER)) with patch( "sys.argv", ["instrument", "--traces_exporter", "jaeger", "1", "2"], ): auto_instrumentation.run() self.assertEqual(environ.get(OTEL_TRACES_EXPORTER), "jaeger") test_sitecustomize.py000066400000000000000000000011051527070072300375750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/tests/auto_instrumentation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore from unittest import TestCase from unittest.mock import patch class TestSiteCustomize(TestCase): # pylint:disable=import-outside-toplevel,unused-import,no-self-use @patch("opentelemetry.instrumentation.auto_instrumentation.initialize") def test_sitecustomize_side_effects(self, initialize_mock): initialize_mock.assert_not_called() import opentelemetry.instrumentation.auto_instrumentation.sitecustomize # NOQA initialize_mock.assert_called_once() opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/tests/test_bootstrap.py000066400000000000000000000064411527070072300324770ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore from io import StringIO from random import sample from unittest import TestCase from unittest.mock import call, patch from opentelemetry.instrumentation import bootstrap from opentelemetry.instrumentation.bootstrap_gen import ( default_instrumentations, libraries, ) def sample_packages(packages, rate): return sample( list(packages), int(len(packages) * rate), ) class TestBootstrap(TestCase): installed_libraries = {} installed_instrumentations = {} @classmethod def setUpClass(cls): cls.installed_libraries = sample_packages([lib["instrumentation"] for lib in libraries], 0.6) # treat 50% of sampled packages as pre-installed cls.installed_instrumentations = sample_packages(cls.installed_libraries, 0.5) cls.pkg_patcher = patch( "opentelemetry.instrumentation.bootstrap._find_installed_libraries", return_value=cls.installed_libraries, ) cls.pip_install_patcher = patch( "opentelemetry.instrumentation.bootstrap._sys_pip_install", ) cls.pip_check_patcher = patch( "opentelemetry.instrumentation.bootstrap._pip_check", ) def setUp(self): super().setUp() self.mock_pip_check = self.pip_check_patcher.start() self.mock_pip_install = self.pip_install_patcher.start() def tearDown(self): super().tearDown() self.pip_check_patcher.stop() self.pip_install_patcher.stop() @patch("sys.argv", ["bootstrap", "-a", "pipenv"]) def test_run_unknown_cmd(self): with self.assertRaises(SystemExit): bootstrap.run() @patch("sys.argv", ["bootstrap", "-a", "requirements"]) def test_run_cmd_print(self): self.pkg_patcher.start() with patch("sys.stdout", new=StringIO()) as fake_out: bootstrap.run() self.assertEqual( fake_out.getvalue(), "\n".join(self.installed_libraries) + "\n", ) self.pkg_patcher.stop() @patch("sys.argv", ["bootstrap", "-a", "install"]) def test_run_cmd_install(self): self.pkg_patcher.start() bootstrap.run() self.mock_pip_install.assert_has_calls( [call(i) for i in self.installed_libraries], any_order=True, ) self.mock_pip_check.assert_called_once() self.pkg_patcher.stop() @patch("sys.argv", ["bootstrap", "-a", "install"]) def test_can_override_available_libraries(self): bootstrap.run(libraries=[]) self.mock_pip_install.assert_has_calls( [call(i) for i in default_instrumentations], any_order=True, ) self.mock_pip_check.assert_called_once() @patch("sys.argv", ["bootstrap", "-a", "install"]) def test_can_override_available_default_instrumentations(self): with patch( "opentelemetry.instrumentation.bootstrap._is_installed", return_value=True, ): bootstrap.run(default_instrumentations=[]) self.mock_pip_install.assert_has_calls( [call(i) for i in self.installed_libraries], any_order=True, ) self.mock_pip_check.assert_called_once() opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/tests/test_cidict.py000066400000000000000000000121411527070072300317130ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=redefined-outer-name from typing import Any import pytest from opentelemetry.instrumentation.cidict import CIDict @pytest.fixture def simple_cidict() -> CIDict[str, int]: return CIDict({"Alpha": 1, "Beta": 2, "Gamma": 3}) @pytest.mark.parametrize( "data, expected_len", [ (None, 0), ({}, 0), ({"a": 1, "b": 2}, 2), ({"A": 1, "a": 2}, 1), ], ) def test_init_from_mapping(data: dict[str, int] | None, expected_len: int) -> None: assert len(CIDict(data)) == expected_len @pytest.mark.parametrize( "pairs, expected_len", [ ([], 0), ([("x", 10), ("y", 20)], 2), ([("X", 1), ("x", 2)], 1), ], ) def test_init_from_iterable_of_pairs(pairs: list[tuple[str, int]], expected_len: int) -> None: assert len(CIDict(pairs)) == expected_len def test_init_non_string_keys() -> None: cid: CIDict[int, str] = CIDict({1: "one", 2: "two"}) assert cid[1] == "one" assert cid[2] == "two" @pytest.mark.parametrize("key", ["Alpha", "alpha", "ALPHA", "aLpHa"]) def test_getitem_case_insensitive(simple_cidict: CIDict[str, int], key: str) -> None: assert simple_cidict[key] == 1 def test_getitem_missing_raises(simple_cidict: CIDict[str, int]) -> None: with pytest.raises(KeyError): _ = simple_cidict["missing"] @pytest.mark.parametrize( "set_key, get_key, value", [ ("Hello", "hello", 99), ("WORLD", "world", 42), ("MiXeD", "mixed", 7), ], ) def test_setitem_case_insensitive(set_key: str, get_key: str, value: int) -> None: cid: CIDict[str, int] = CIDict() cid[set_key] = value assert cid[get_key] == value def test_setitem_overwrite_updates_original_key() -> None: cid: CIDict[str, int] = CIDict() cid["Hello"] = 1 cid["HELLO"] = 2 assert cid["hello"] == 2 assert cid.original_key("hello") == "HELLO" @pytest.mark.parametrize("del_key", ["Alpha", "alpha", "ALPHA", "aLpHa"]) def test_delitem_case_insensitive(simple_cidict: CIDict[str, int], del_key: str) -> None: del simple_cidict[del_key] assert "alpha" not in simple_cidict assert len(simple_cidict) == 2 def test_delitem_missing_raises(simple_cidict: CIDict[str, int]) -> None: with pytest.raises(KeyError): del simple_cidict["nonexistent"] def test_iter_yields_original_keys(simple_cidict: CIDict[str, int]) -> None: assert set(simple_cidict) == {"Alpha", "Beta", "Gamma"} @pytest.mark.parametrize( "lookup_key, expected_original", [ ("Alpha", "Alpha"), ("alpha", "Alpha"), ("ALPHA", "Alpha"), ], ) def test_original_key(simple_cidict, lookup_key: str, expected_original: str) -> None: assert simple_cidict.original_key(lookup_key) == expected_original def test_normalized_items(simple_cidict: CIDict[str, int]) -> None: items = list(simple_cidict.normalized_items()) assert set(items) == {("alpha", 1), ("beta", 2), ("gamma", 3)} @pytest.mark.parametrize( "key, expected", [ ("Alpha", True), ("alpha", True), ("ALPHA", True), ("missing", False), ], ) def test_contains(simple_cidict: CIDict[str, int], key: str, expected: bool) -> None: assert (key in simple_cidict) is expected @pytest.mark.parametrize( "other, expected", [ (CIDict({"alpha": 1, "beta": 2, "gamma": 3}), True), (CIDict({"ALPHA": 1, "BETA": 2, "GAMMA": 3}), True), ({"alpha": 1, "beta": 2, "gamma": 3}, True), ({"ALPHA": 1, "BETA": 2, "GAMMA": 3}, True), (CIDict({"alpha": 99, "beta": 2, "gamma": 3}), False), (CIDict({"alpha": 1, "beta": 2}), False), ("not a mapping", False), (42, False), ], ) def test_eq(simple_cidict: CIDict[str, int], other: Any, expected: bool) -> None: assert (simple_cidict == other) is expected def test_repr(simple_cidict: CIDict[str, int]) -> None: assert repr(simple_cidict) == "CIDict({'Alpha': 1, 'Beta': 2, 'Gamma': 3})" @pytest.mark.parametrize( "pop_key, expected_value, remaining_len", [ ("Alpha", 1, 2), ("BETA", 2, 2), ("gamma", 3, 2), ], ) def test_pop( simple_cidict: CIDict[str, int], pop_key: str, expected_value: int, remaining_len: int, ) -> None: assert simple_cidict.pop(pop_key) == expected_value assert len(simple_cidict) == remaining_len def test_setdefault_existing(simple_cidict: CIDict[str, int]) -> None: assert simple_cidict.setdefault("ALPHA", 999) == 1 assert simple_cidict["alpha"] == 1 def test_setdefault_missing(simple_cidict: CIDict[str, int]) -> None: assert simple_cidict.setdefault("Delta", 4) == 4 assert simple_cidict["delta"] == 4 def test_keys(simple_cidict: CIDict[str, int]) -> None: assert set(simple_cidict.keys()) == {"Alpha", "Beta", "Gamma"} def test_values(simple_cidict: CIDict[str, int]) -> None: assert set(simple_cidict.values()) == {1, 2, 3} def test_items(simple_cidict: CIDict[str, int]) -> None: assert set(simple_cidict.items()) == { ("Alpha", 1), ("Beta", 2), ("Gamma", 3), } opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/tests/test_dependencies.py000066400000000000000000000343441527070072300331130ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access from unittest.mock import Mock, patch import pytest from packaging.requirements import Requirement from opentelemetry.instrumentation.dependencies import ( DependencyConflict, get_dependency_conflicts, get_dist_dependency_conflicts, ) from opentelemetry.test.test_base import TestBase from opentelemetry.util._importlib_metadata import ( Distribution, PackageNotFoundError, ) class TestDependencyConflicts(TestBase): def test_get_dependency_conflicts_empty(self): self.assertIsNone(get_dependency_conflicts([])) def test_get_dependency_conflicts_no_conflict_requirement(self): req = Requirement("pytest") self.assertIsNone(get_dependency_conflicts([req])) def test_get_dependency_conflicts_no_conflict(self): self.assertIsNone(get_dependency_conflicts(["pytest"])) def test_get_dependency_conflicts_not_installed_requirement(self): req = Requirement("this-package-does-not-exist") conflict = get_dependency_conflicts([req]) self.assertTrue(conflict is not None) self.assertTrue(isinstance(conflict, DependencyConflict)) self.assertEqual( str(conflict), 'This instrumentation only instruments "this-package-does-not-exist", but no installed version was found, so nothing can be instrumented.', ) def test_get_dependency_conflicts_not_installed(self): conflict = get_dependency_conflicts(["this-package-does-not-exist"]) self.assertTrue(conflict is not None) self.assertTrue(isinstance(conflict, DependencyConflict)) self.assertEqual( str(conflict), 'This instrumentation only instruments "this-package-does-not-exist", but no installed version was found, so nothing can be instrumented.', ) def test_get_dependency_conflicts_mismatched_version(self): conflict = get_dependency_conflicts(["pytest == 5000"]) self.assertTrue(conflict is not None) self.assertTrue(isinstance(conflict, DependencyConflict)) self.assertEqual( str(conflict), f'This instrumentation only instruments "pytest == 5000", but currently installed version ("pytest {pytest.__version__}") falls outside of that range, so nothing can be instrumented.', ) def test_get_dist_dependency_conflicts(self): class MockDistribution(Distribution): def locate_file(self, path): pass def read_text(self, filename): pass @property def requires(self): return ['test-pkg ~= 1.0; extra == "instruments"'] dist = MockDistribution() conflict = get_dist_dependency_conflicts(dist) self.assertTrue(conflict is not None) self.assertTrue(isinstance(conflict, DependencyConflict)) self.assertEqual( str(conflict), 'This instrumentation only instruments "test-pkg~=1.0; extra == "instruments"", but no installed version was found, so nothing can be instrumented.', ) def test_get_dist_dependency_conflicts_requires_none(self): class MockDistribution(Distribution): def locate_file(self, path): pass def read_text(self, filename): pass @property def requires(self): # TODO: make another test for returning something with a blank list for both and and or return None dist = MockDistribution() conflict = get_dist_dependency_conflicts(dist) self.assertTrue(conflict is None) @patch("opentelemetry.instrumentation.dependencies.version") def test_get_dist_dependency_conflicts_any(self, version_mock): class MockDistribution(Distribution): def locate_file(self, path): pass def read_text(self, filename): pass @property def requires(self): return [ 'foo ~= 1.0; extra == "instruments-any"', 'bar ~= 1.0; extra == "instruments-any"', ] dist = MockDistribution() def version_side_effect(package_name): if package_name == "foo": raise PackageNotFoundError("foo not found") if package_name == "bar": return "1.0.0" raise PackageNotFoundError(f"{package_name} not found") version_mock.side_effect = version_side_effect conflict = get_dist_dependency_conflicts(dist) self.assertIsNone(conflict) @patch("opentelemetry.instrumentation.dependencies.version") def test_get_dist_dependency_conflicts_neither(self, version_mock): class MockDistribution(Distribution): def locate_file(self, path): pass def read_text(self, filename): pass @property def requires(self): return [ 'foo ~= 1.0; extra == "instruments-any"', 'bar ~= 1.0; extra == "instruments-any"', ] dist = MockDistribution() # version_mock.side_effect = lambda x: "1.0.0" if x == "foo" else "2.0.0" # version_mock("foo").return_value = "2.0.0" version_mock.side_effect = PackageNotFoundError("not found") conflict = get_dist_dependency_conflicts(dist) self.assertTrue(conflict is not None) self.assertTrue(isinstance(conflict, DependencyConflict)) self.assertEqual( str(conflict), """This instrumentation requires any of "foo~=1.0; extra == "instruments-any", bar~=1.0; extra == "instruments-any"", but none are installed, so nothing can be instrumented.""", ) # Tests when both "and" and "either" dependencies are specified and both pass. @patch("opentelemetry.instrumentation.dependencies.version") def test_get_dist_dependency_conflicts_any_and(self, version_mock): class MockDistribution(Distribution): def locate_file(self, path): pass def read_text(self, filename): pass @property def requires(self): # This indicates the instrumentation requires (foo and (bar or baz))) return [ 'foo ~= 1.0; extra == "instruments"', 'bar ~= 2.0; extra == "instruments-any"', 'baz ~= 3.0; extra == "instruments-any"', ] dist = MockDistribution() def version_side_effect(package_name): if package_name == "foo": return "1.2.0" if package_name == "bar": raise PackageNotFoundError("bar not found") if package_name == "baz": return "3.7.0" raise PackageNotFoundError(f"{package_name} not found") version_mock.side_effect = version_side_effect conflict = get_dist_dependency_conflicts(dist) self.assertIsNone(conflict) # Tests when both "and" and "either" dependencies are specified but the "and" dependencies fail to resolve. @patch("opentelemetry.instrumentation.dependencies.version") def test_get_dist_dependency_conflicts_any_and_failed(self, version_mock): class MockDistribution(Distribution): def locate_file(self, path): pass def read_text(self, filename): pass @property def requires(self): # This indicates the instrumentation requires (foo and (bar or baz))) return [ 'foo ~= 1.0; extra == "instruments"', 'bar ~= 2.0; extra == "instruments-any"', 'baz ~= 3.0; extra == "instruments-any"', ] dist = MockDistribution() def version_side_effect(package_name): if package_name == "foo": raise PackageNotFoundError("foo not found") if package_name == "bar": raise PackageNotFoundError("bar not found") if package_name == "baz": return "3.7.0" raise PackageNotFoundError(f"{package_name} not found") version_mock.side_effect = version_side_effect conflict = get_dist_dependency_conflicts(dist) self.assertTrue(conflict is not None) self.assertTrue(isinstance(conflict, DependencyConflict)) self.assertEqual( str(conflict), 'This instrumentation only instruments "foo~=1.0; extra == "instruments"", but no installed version was found, so nothing can be instrumented.', ) # Tests when both "and" and "either" dependencies are specified but the "either" dependencies fail to resolve. @patch("opentelemetry.instrumentation.dependencies.version") def test_get_dist_dependency_conflicts_and_any_failed(self, version_mock): class MockDistribution(Distribution): def locate_file(self, path): pass def read_text(self, filename): pass @property def requires(self): # This indicates the instrumentation requires (foo and (bar or baz))) return [ 'foo ~= 1.0; extra == "instruments"', 'bar ~= 2.0; extra == "instruments-any"', 'baz ~= 3.0; extra == "instruments-any"', ] dist = MockDistribution() def version_side_effect(package_name): if package_name == "foo": return "1.7.0" if package_name == "bar": raise PackageNotFoundError("bar not found") if package_name == "baz": raise PackageNotFoundError("baz not found") raise PackageNotFoundError(f"{package_name} not found") version_mock.side_effect = version_side_effect conflict = get_dist_dependency_conflicts(dist) self.assertTrue(conflict is not None) self.assertTrue(isinstance(conflict, DependencyConflict)) self.assertEqual( str(conflict), """This instrumentation requires any of "bar~=2.0; extra == "instruments-any", baz~=3.0; extra == "instruments-any"", but none are installed, so nothing can be instrumented.""", ) def test_dependency_conflict_format_message(self): cases = [ ( "required_version_mismatch", DependencyConflict("google-genai>=1.32.0,<3", "google-genai 1.31.0"), "GoogleGenAIInstrumentor", True, 'GoogleGenAIInstrumentor only instruments "google-genai>=1.32.0,<3", but currently installed version ("google-genai 1.31.0") falls outside of that range, so nothing can be instrumented.', ), ( "required_not_installed", DependencyConflict("google-genai>=1.32.0,<3", None), "GoogleGenAIInstrumentor", False, 'GoogleGenAIInstrumentor only instruments "google-genai>=1.32.0,<3", but no installed version was found, so nothing can be instrumented.', ), ( "required_any_version_mismatch", DependencyConflict( required_any=["psycopg2>=2.7.3.1", "psycopg2-binary>=2.7.3.1"], found_any=["psycopg2 2.6.0"], ), "Psycopg2Instrumentor", True, 'Psycopg2Instrumentor instruments any of "psycopg2>=2.7.3.1, psycopg2-binary>=2.7.3.1", but currently installed version(s) ("psycopg2 2.6.0") fall outside of that range, so nothing can be instrumented.', ), ( "required_any_none_installed", DependencyConflict( required_any=["kafka-python>=2.0,<3.0", "kafka-python-ng>=2.0,<3.0"], found_any=[], ), "KafkaInstrumentor", False, 'KafkaInstrumentor requires any of "kafka-python>=2.0,<3.0, kafka-python-ng>=2.0,<3.0", but none are installed, so nothing can be instrumented.', ), ( "without_instrumentor_name", DependencyConflict("google-genai>=1.32.0,<3", "google-genai 1.31.0"), None, True, 'This instrumentation only instruments "google-genai>=1.32.0,<3", but currently installed version ("google-genai 1.31.0") falls outside of that range, so nothing can be instrumented.', ), ] for name, conflict, instrumentor_name, is_version_conflict, expected_message in cases: with self.subTest(case=name): self.assertEqual(conflict._is_version_conflict, is_version_conflict) self.assertEqual( conflict._format_message(instrumentor_name), expected_message, ) if instrumentor_name is None: self.assertEqual(str(conflict), expected_message) def test_dependency_conflict_log(self): cases = [ ( "version_conflict", DependencyConflict("google-genai>=1.32.0,<3", "google-genai 1.31.0"), True, "error", ), ( "not_installed", DependencyConflict("google-genai>=1.32.0,<3", None), False, "debug", ), ] for name, conflict, is_version_conflict, expected_level in cases: with self.subTest(case=name): self.assertEqual(conflict._is_version_conflict, is_version_conflict) mock_logger = Mock() conflict._log(mock_logger, "GoogleGenAIInstrumentor") expected_message = conflict._format_message("GoogleGenAIInstrumentor") if expected_level == "error": mock_logger.error.assert_called_once_with(expected_message) mock_logger.debug.assert_not_called() else: mock_logger.debug.assert_called_once_with(expected_message) mock_logger.error.assert_not_called() opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/tests/test_distro.py000066400000000000000000000025421527070072300317640ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore from unittest import TestCase from opentelemetry.instrumentation.distro import BaseDistro from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.util._importlib_metadata import EntryPoint class MockInstrumetor(BaseInstrumentor): def instrumentation_dependencies(self): return [] def _instrument(self, **kwargs): pass def _uninstrument(self, **kwargs): pass class MockEntryPoint(EntryPoint): def __init__(self, name, value, group): # pylint: disable=super-init-not-called pass def load(self, *args, **kwargs): # pylint: disable=signature-differs return MockInstrumetor class MockDistro(BaseDistro): def _configure(self, **kwargs): pass class TestDistro(TestCase): def test_load_instrumentor(self): # pylint: disable=protected-access distro = MockDistro() instrumentor = MockInstrumetor() entry_point = MockEntryPoint( "MockInstrumetor", value="opentelemetry", group="opentelemetry_distro", ) self.assertFalse(instrumentor._is_instrumented_by_opentelemetry) distro.load_instrumentor(entry_point) self.assertTrue(instrumentor._is_instrumented_by_opentelemetry) opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/tests/test_instrumentor.py000066400000000000000000000061051527070072300332300ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore from logging import WARNING from unittest import TestCase from unittest.mock import patch from opentelemetry.instrumentation.dependencies import ( DependencyConflict, DependencyConflictError, ) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor class TestInstrumentor(TestCase): class Instrumentor(BaseInstrumentor): def _instrument(self, **kwargs): return "instrumented" def _uninstrument(self, **kwargs): return "uninstrumented" def instrumentation_dependencies(self): return [] def test_protect(self): instrumentor = self.Instrumentor() with self.assertLogs(level=WARNING): self.assertIs(instrumentor.uninstrument(), None) self.assertEqual(instrumentor.instrument(), "instrumented") with self.assertLogs(level=WARNING): self.assertIs(instrumentor.instrument(), None) self.assertEqual(instrumentor.uninstrument(), "uninstrumented") with self.assertLogs(level=WARNING): self.assertIs(instrumentor.uninstrument(), None) def test_singleton(self): self.assertIs(self.Instrumentor(), self.Instrumentor()) @patch("opentelemetry.instrumentation.instrumentor._LOG") @patch("opentelemetry.instrumentation.instrumentor.BaseInstrumentor._check_dependency_conflicts") def test_instrument_missing_dependency_raise(self, mock__check_dependency_conflicts, mock_logger): instrumentor = self.Instrumentor() mock__check_dependency_conflicts.return_value = DependencyConflict("missing", "missing") self.assertRaises( DependencyConflictError, instrumentor.instrument, raise_exception_on_conflict=True, ) mock_logger.error.assert_not_called() @patch("opentelemetry.instrumentation.instrumentor._LOG") @patch("opentelemetry.instrumentation.instrumentor.BaseInstrumentor._check_dependency_conflicts") def test_instrument_missing_dependency_log_error(self, mock__check_dependency_conflicts, mock_logger): instrumentor = self.Instrumentor() conflict = DependencyConflict("missing", "missing") mock__check_dependency_conflicts.return_value = conflict self.assertIsNone(instrumentor.instrument(raise_exception_on_conflict=False)) mock_logger.error.assert_any_call(conflict._format_message("Instrumentor")) @patch("opentelemetry.instrumentation.instrumentor._LOG") @patch("opentelemetry.instrumentation.instrumentor.BaseInstrumentor._check_dependency_conflicts") def test_instrument_missing_dependency_not_installed_log_error(self, mock__check_dependency_conflicts, mock_logger): instrumentor = self.Instrumentor() conflict = DependencyConflict("missing", None) mock__check_dependency_conflicts.return_value = conflict self.assertIsNone(instrumentor.instrument(raise_exception_on_conflict=False)) mock_logger.error.assert_any_call(conflict._format_message("Instrumentor")) opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/tests/test_labeler.py000066400000000000000000000146201527070072300320660ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import contextvars import threading import unittest from unittest.mock import patch from opentelemetry.instrumentation._labeler import ( Labeler, clear_labeler, enrich_metric_attributes, get_labeler, get_labeler_attributes, set_labeler, ) from opentelemetry.instrumentation._labeler._internal import ( LABELER_CONTEXT_KEY, ) class TestLabeler(unittest.TestCase): def setUp(self): clear_labeler() def test_labeler_init(self): labeler = Labeler() self.assertEqual(labeler.get_attributes(), {}) self.assertEqual(len(labeler), 0) def test_add_single_attribute(self): labeler = Labeler() labeler.add("test_key", "test_value") self.assertEqual(labeler.get_attributes(), {"test_key": "test_value"}) def test_add_attributes_dict(self): labeler = Labeler() attrs = {"key1": "value1", "key2": 42, "key3": False} labeler.add_attributes(attrs) self.assertEqual(labeler.get_attributes(), attrs) def test_overwrite_attribute(self): labeler = Labeler() labeler.add("key1", "original") labeler.add("key1", "updated") self.assertEqual(labeler.get_attributes(), {"key1": "updated"}) def test_clear_attributes(self): labeler = Labeler() labeler.add("key1", "value1") labeler.add("key2", "value2") labeler.clear() self.assertEqual(labeler.get_attributes(), {}) def test_add_invalid_types_logs_warning_and_skips(self): labeler = Labeler() with patch("opentelemetry.instrumentation._labeler._internal._logger.warning") as mock_warning: labeler.add("valid", "value") labeler.add("dict_key", {"nested": "dict"}) labeler.add("list_key", [1, 2, 3]) labeler.add("none_key", None) labeler.add("another_valid", 123) self.assertEqual(mock_warning.call_count, 3) self.assertEqual(labeler.get_attributes(), {"valid": "value", "another_valid": 123}) def test_limit_and_truncation(self): labeler = Labeler(max_custom_attrs=2, max_attr_value_length=5) labeler.add("a", "1234567") labeler.add("b", "ok") labeler.add("c", "ignored") self.assertEqual(labeler.get_attributes(), {"a": "12345", "b": "ok"}) def test_enrich_metric_attributes_skips_base_overrides(self): base_attributes = {"http.method": "GET", "http.status_code": 200} labeler = get_labeler() labeler.add("http.method", "POST") labeler.add("custom_attr", "test-value") enriched = enrich_metric_attributes(base_attributes) self.assertEqual(enriched["http.method"], "GET") self.assertEqual(enriched["custom_attr"], "test-value") self.assertEqual(base_attributes["http.method"], "GET") def test_thread_safety(self): labeler = Labeler(max_custom_attrs=1100) num_threads = 5 num_ops = 100 def worker(thread_id): for index in range(num_ops): labeler.add(f"thread_{thread_id}_{index}", f"v_{index}") threads = [threading.Thread(target=worker, args=(i,)) for i in range(num_threads)] for thread in threads: thread.start() for thread in threads: thread.join() self.assertEqual(len(labeler.get_attributes()), num_threads * num_ops) class TestLabelerContext(unittest.TestCase): def setUp(self): clear_labeler() def test_get_labeler_creates_new(self): labeler = get_labeler() self.assertIsInstance(labeler, Labeler) def test_get_labeler_returns_same_instance(self): labeler1 = get_labeler() labeler1.add("test", "value") labeler2 = get_labeler() self.assertIs(labeler1, labeler2) def test_set_labeler(self): custom_labeler = Labeler() custom_labeler.add("custom", "value") set_labeler(custom_labeler) self.assertIs(get_labeler(), custom_labeler) def test_set_labeler_invalid_type_is_ignored(self): with patch("opentelemetry.instrumentation._labeler._internal._logger.warning") as mock_warning: set_labeler("bad") # type: ignore[arg-type] self.assertEqual(mock_warning.call_count, 1) def test_clear_labeler(self): labeler = get_labeler() labeler.add("test", "value") clear_labeler() new_labeler = get_labeler() self.assertIsNot(new_labeler, labeler) self.assertEqual(new_labeler.get_attributes(), {}) def test_get_labeler_attributes(self): clear_labeler() self.assertEqual(get_labeler_attributes(), {}) labeler = get_labeler() labeler.add("test", "value") self.assertEqual(get_labeler_attributes(), {"test": "value"}) def test_context_isolation(self): def context_worker(context_id, results): labeler = get_labeler() labeler.add("context_id", context_id) results[context_id] = dict(labeler.get_attributes()) results = {} for operation in range(3): ctx = contextvars.copy_context() ctx.run(context_worker, operation, results) for operation in range(3): self.assertEqual(results[operation], {"context_id": operation}) class TestLabelerFailSafe(unittest.TestCase): def setUp(self): clear_labeler() def test_get_labeler_failsafe_on_get_value_error(self): with patch( "opentelemetry.instrumentation._labeler._internal.get_value", side_effect=RuntimeError("boom"), ): labeler = get_labeler() self.assertIsInstance(labeler, Labeler) def test_set_and_clear_failsafe_on_attach_error(self): labeler = Labeler() with patch( "opentelemetry.instrumentation._labeler._internal.attach", side_effect=RuntimeError("boom"), ): set_labeler(labeler) clear_labeler() self.assertIsInstance(get_labeler(), Labeler) def test_get_labeler_attributes_failsafe(self): with patch( "opentelemetry.instrumentation._labeler._internal.get_value", side_effect=RuntimeError("boom"), ): attrs = get_labeler_attributes() self.assertEqual(attrs, {}) def test_context_key_constant(self): self.assertTrue(isinstance(LABELER_CONTEXT_KEY, str)) opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/tests/test_log_utils.py000066400000000000000000000015251527070072300324610ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from opentelemetry._logs import SeverityNumber from opentelemetry.instrumentation.log_utils import std_to_otel class TestLogUtils(TestCase): def test_std_to_otel(self): for levelno, expected in ( (0, SeverityNumber.UNSPECIFIED), (9, SeverityNumber.UNSPECIFIED), (10, SeverityNumber.DEBUG), (14, SeverityNumber.DEBUG4), (20, SeverityNumber.INFO), (30, SeverityNumber.WARN), (40, SeverityNumber.ERROR), (50, SeverityNumber.FATAL), (53, SeverityNumber.FATAL4), (54, SeverityNumber.FATAL4), ): with self.subTest(levelno=levelno): self.assertEqual(std_to_otel(levelno), expected) opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/tests/test_propagators.py000066400000000000000000000040521527070072300330170ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access import unittest from opentelemetry import trace from opentelemetry.instrumentation import propagators from opentelemetry.instrumentation.propagators import ( DictHeaderSetter, TraceResponsePropagator, get_global_response_propagator, set_global_response_propagator, ) from opentelemetry.test.test_base import TestBase class TestGlobals(TestBase): def test_get_set(self): original = propagators._RESPONSE_PROPAGATOR propagators._RESPONSE_PROPAGATOR = None self.assertIsNone(get_global_response_propagator()) prop = TraceResponsePropagator() set_global_response_propagator(prop) self.assertIs(prop, get_global_response_propagator()) propagators._RESPONSE_PROPAGATOR = original class TestDictHeaderSetter(unittest.TestCase): def test_simple(self): setter = DictHeaderSetter() carrier = {} setter.set(carrier, "kk", "vv") self.assertIn("kk", carrier) self.assertEqual(carrier["kk"], "vv") def test_append(self): setter = DictHeaderSetter() carrier = {"kk": "old"} setter.set(carrier, "kk", "vv") self.assertIn("kk", carrier) self.assertEqual(carrier["kk"], "old, vv") class TestTraceResponsePropagator(TestBase): def test_inject(self): span = trace.NonRecordingSpan( trace.SpanContext( trace_id=1, span_id=2, is_remote=False, trace_flags=trace.DEFAULT_TRACE_OPTIONS, trace_state=trace.DEFAULT_TRACE_STATE, ), ) ctx = trace.set_span_in_context(span) prop = TraceResponsePropagator() carrier = {} prop.inject(carrier, ctx) self.assertEqual(carrier["Access-Control-Expose-Headers"], "traceresponse") self.assertEqual( carrier["traceresponse"], "00-00000000000000000000000000000001-0000000000000002-00", ) opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/tests/test_semconv.py000066400000000000000000000642071527070072300321400ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os from unittest import TestCase from unittest.mock import Mock, patch from opentelemetry.instrumentation._semconv import ( _LEGACY_SCHEMA_VERSION, OTEL_SEMCONV_STABILITY_OPT_IN, _get_schema_url_for_signal_types, _get_schema_version_for_opt_in_mode, _get_semconv_opt_in_modes, _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _set_db_name, _set_db_operation, _set_db_redis_database_index, _set_db_statement, _set_db_system, _set_db_user, _set_net_transport, _set_status, _StabilityMode, ) from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_OPERATION, DB_REDIS_DATABASE_INDEX, DB_STATEMENT, DB_SYSTEM, DB_USER, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_TRANSPORT, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_NAMESPACE, DB_OPERATION_NAME, DB_QUERY_TEXT, DB_SYSTEM_NAME, ) from opentelemetry.semconv.attributes.network_attributes import ( NETWORK_TRANSPORT, ) from opentelemetry.trace.status import StatusCode def stability_mode(mode): def decorator(test_case): @patch.dict(os.environ, {OTEL_SEMCONV_STABILITY_OPT_IN: mode}) def wrapper(*args, **kwargs): _OpenTelemetrySemanticConventionStability._initialized = False _OpenTelemetrySemanticConventionStability._initialize() return test_case(*args, **kwargs) return wrapper return decorator class TestOpenTelemetrySemConvStability(TestCase): @stability_mode("") def test_default_mode(self): self.assertEqual( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP ), _StabilityMode.DEFAULT, ) self.assertEqual( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.DATABASE ), _StabilityMode.DEFAULT, ) self.assertEqual( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.GEN_AI ), _StabilityMode.DEFAULT, ) @stability_mode("http") def test_http_stable_mode(self): self.assertEqual( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP ), _StabilityMode.HTTP, ) @stability_mode("http/dup") def test_http_dup_mode(self): self.assertEqual( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP ), _StabilityMode.HTTP_DUP, ) @stability_mode("database") def test_database_stable_mode(self): self.assertEqual( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.DATABASE ), _StabilityMode.DATABASE, ) @stability_mode("database/dup") def test_database_dup_mode(self): self.assertEqual( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.DATABASE ), _StabilityMode.DATABASE_DUP, ) @stability_mode("gen_ai_latest_experimental") def test_genai_latest_experimental(self): self.assertEqual( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.GEN_AI ), _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL, ) @stability_mode("database,http,gen_ai_latest_experimental") def test_multiple_stability_database_http_modes(self): self.assertEqual( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.DATABASE ), _StabilityMode.DATABASE, ) self.assertEqual( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP ), _StabilityMode.HTTP, ) self.assertEqual( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.GEN_AI ), _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL, ) @stability_mode("") def test_get_semconv_opt_in_modes_default(self): self.assertEqual( _get_semconv_opt_in_modes( ( _OpenTelemetryStabilitySignalType.DATABASE, _OpenTelemetryStabilitySignalType.HTTP, ) ), { _OpenTelemetryStabilitySignalType.DATABASE: _StabilityMode.DEFAULT, _OpenTelemetryStabilitySignalType.HTTP: _StabilityMode.DEFAULT, }, ) @stability_mode("database,http/dup") def test_get_semconv_opt_in_modes_database_and_http_dup(self): self.assertEqual( _get_semconv_opt_in_modes( ( _OpenTelemetryStabilitySignalType.DATABASE, _OpenTelemetryStabilitySignalType.HTTP, ) ), { _OpenTelemetryStabilitySignalType.DATABASE: _StabilityMode.DATABASE, _OpenTelemetryStabilitySignalType.HTTP: _StabilityMode.HTTP_DUP, }, ) @stability_mode("database,http,gen_ai_latest_experimental") def test_get_semconv_opt_in_modes_database_http_and_gen_ai(self): self.assertEqual( _get_semconv_opt_in_modes( ( _OpenTelemetryStabilitySignalType.DATABASE, _OpenTelemetryStabilitySignalType.HTTP, _OpenTelemetryStabilitySignalType.GEN_AI, ) ), { _OpenTelemetryStabilitySignalType.DATABASE: _StabilityMode.DATABASE, _OpenTelemetryStabilitySignalType.HTTP: _StabilityMode.HTTP, _OpenTelemetryStabilitySignalType.GEN_AI: _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL, }, ) @stability_mode("database,http/dup") def test_multiple_stability_database_http_dup_modes(self): self.assertEqual( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.DATABASE ), _StabilityMode.DATABASE, ) self.assertEqual( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP ), _StabilityMode.HTTP_DUP, ) @stability_mode("database/dup,http") def test_multiple_stability_database_dup_http_stable_modes(self): self.assertEqual( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.DATABASE ), _StabilityMode.DATABASE_DUP, ) self.assertEqual( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP ), _StabilityMode.HTTP, ) @stability_mode("database,database/dup,http,http/dup") def test_stability_mode_dup_precedence(self): self.assertEqual( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.DATABASE ), _StabilityMode.DATABASE_DUP, ) self.assertEqual( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.HTTP ), _StabilityMode.HTTP_DUP, ) class TestOpenTelemetrySemConvSchemaUrl(TestCase): @stability_mode("") def test_get_schema_version_for_opt_in_mode_default(self): version = _get_schema_version_for_opt_in_mode(_OpenTelemetryStabilitySignalType.HTTP, _StabilityMode.DEFAULT) self.assertEqual(version, _LEGACY_SCHEMA_VERSION) version = _get_schema_version_for_opt_in_mode( _OpenTelemetryStabilitySignalType.DATABASE, _StabilityMode.DEFAULT ) self.assertEqual(version, _LEGACY_SCHEMA_VERSION) version = _get_schema_version_for_opt_in_mode(_OpenTelemetryStabilitySignalType.GEN_AI, _StabilityMode.DEFAULT) self.assertEqual(version, _LEGACY_SCHEMA_VERSION) @stability_mode("") def test_get_schema_version_for_opt_in_mode_http_stable(self): version = _get_schema_version_for_opt_in_mode(_OpenTelemetryStabilitySignalType.HTTP, _StabilityMode.HTTP) self.assertEqual(version, "1.21.0") @stability_mode("") def test_get_schema_version_for_opt_in_mode_database_stable(self): version = _get_schema_version_for_opt_in_mode( _OpenTelemetryStabilitySignalType.DATABASE, _StabilityMode.DATABASE ) self.assertEqual(version, "1.25.0") @stability_mode("") def test_get_schema_version_for_opt_in_mode_gen_ai_stable(self): version = _get_schema_version_for_opt_in_mode( _OpenTelemetryStabilitySignalType.GEN_AI, _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL, ) self.assertEqual(version, "1.26.0") @stability_mode("") def test_get_schema_url_for_signal_types_single_http_default(self): url = _get_schema_url_for_signal_types([_OpenTelemetryStabilitySignalType.HTTP]) self.assertEqual(url, f"https://opentelemetry.io/schemas/{_LEGACY_SCHEMA_VERSION}") @stability_mode("http") def test_get_schema_url_for_signal_types_single_http_stable(self): url = _get_schema_url_for_signal_types([_OpenTelemetryStabilitySignalType.HTTP]) self.assertEqual(url, "https://opentelemetry.io/schemas/1.21.0") @stability_mode("database") def test_get_schema_url_for_signal_types_single_database_stable(self): url = _get_schema_url_for_signal_types([_OpenTelemetryStabilitySignalType.DATABASE]) self.assertEqual(url, "https://opentelemetry.io/schemas/1.25.0") @stability_mode("http,database") def test_get_schema_url_for_signal_types_multiple_both_stable(self): # DATABASE has higher version (1.25.0) than HTTP (1.21.0) url = _get_schema_url_for_signal_types( [ _OpenTelemetryStabilitySignalType.HTTP, _OpenTelemetryStabilitySignalType.DATABASE, ] ) self.assertEqual(url, "https://opentelemetry.io/schemas/1.25.0") @stability_mode("http") def test_get_schema_url_for_signal_types_mixed_modes(self): # HTTP is stable (1.21.0), DATABASE is default (1.11.0) # Should return HTTP version as it's higher url = _get_schema_url_for_signal_types( [ _OpenTelemetryStabilitySignalType.HTTP, _OpenTelemetryStabilitySignalType.DATABASE, ] ) self.assertEqual(url, "https://opentelemetry.io/schemas/1.21.0") @stability_mode("database") def test_get_schema_url_for_signal_types_database_only_stable(self): # DATABASE is stable (1.25.0), HTTP is default (1.11.0) # Should return DATABASE version as it's highest url = _get_schema_url_for_signal_types( [ _OpenTelemetryStabilitySignalType.HTTP, _OpenTelemetryStabilitySignalType.DATABASE, ] ) self.assertEqual(url, "https://opentelemetry.io/schemas/1.25.0") @stability_mode("") def test_get_schema_url_for_signal_types_empty_list(self): url = _get_schema_url_for_signal_types([]) self.assertEqual(url, f"https://opentelemetry.io/schemas/{_LEGACY_SCHEMA_VERSION}") @stability_mode("http/dup,database/dup") def test_get_schema_url_for_signal_types_dup_modes(self): url = _get_schema_url_for_signal_types( [ _OpenTelemetryStabilitySignalType.HTTP, _OpenTelemetryStabilitySignalType.DATABASE, ] ) self.assertEqual(url, "https://opentelemetry.io/schemas/1.25.0") @stability_mode("http,database,gen_ai_latest_experimental") def test_get_schema_url_for_signal_types_with_gen_ai(self): # GEN_AI should be highest at 1.26.0 url = _get_schema_url_for_signal_types( [ _OpenTelemetryStabilitySignalType.HTTP, _OpenTelemetryStabilitySignalType.DATABASE, _OpenTelemetryStabilitySignalType.GEN_AI, ] ) self.assertEqual(url, "https://opentelemetry.io/schemas/1.26.0") class TestOpenTelemetrySemConvStabilityHTTP(TestCase): def test_net_transport_http_default(self): result = {} _set_net_transport( result, "ip_tcp", "tcp", sem_conv_opt_in_mode=_StabilityMode.DEFAULT, ) self.assertIn(NET_TRANSPORT, result) self.assertEqual(result[NET_TRANSPORT], "ip_tcp") self.assertNotIn(NETWORK_TRANSPORT, result) def test_net_transport_http_stable(self): result = {} _set_net_transport( result, "ip_tcp", "tcp", sem_conv_opt_in_mode=_StabilityMode.HTTP, ) self.assertNotIn(NET_TRANSPORT, result) self.assertIn(NETWORK_TRANSPORT, result) self.assertEqual(result[NETWORK_TRANSPORT], "tcp") def test_net_transport_http_dup(self): result = {} _set_net_transport( result, "ip_tcp", "tcp", sem_conv_opt_in_mode=_StabilityMode.HTTP_DUP, ) self.assertIn(NET_TRANSPORT, result) self.assertEqual(result[NET_TRANSPORT], "ip_tcp") self.assertIn(NETWORK_TRANSPORT, result) self.assertEqual(result[NETWORK_TRANSPORT], "tcp") def test_net_transport_none_value(self): result = {} _set_net_transport( result, None, None, sem_conv_opt_in_mode=_StabilityMode.HTTP_DUP, ) self.assertNotIn(NET_TRANSPORT, result) self.assertNotIn(NETWORK_TRANSPORT, result) def test_set_status_for_non_http_code_with_recording_span(self): span = Mock() span.is_recording.return_value = True metric_attributes = {} _set_status( span, metric_attributes, -1, "Exception", sem_conv_opt_in_mode=_StabilityMode.DEFAULT, ) self.assertIsNone(metric_attributes.get("error.type")) span.set_attribute.assert_not_called() status_call = span.set_status.call_args[0][0] self.assertEqual(status_call.status_code, StatusCode.ERROR) self.assertEqual(status_call.description, "Non-integer HTTP status: " + "Exception") def test_status_code_http_default(self): span = Mock() metrics_attributes = {} _set_status( span=span, metrics_attributes=metrics_attributes, status_code=404, status_code_str="404", server_span=True, sem_conv_opt_in_mode=_StabilityMode.DEFAULT, ) # Verify only old conventions are emitted span.set_attribute.assert_called_with("http.status_code", 404) self.assertIn("http.status_code", metrics_attributes) self.assertNotIn("http.response_status_code", metrics_attributes) def test_status_code_http_stable(self): span = Mock() metrics_attributes = {} _set_status( span=span, metrics_attributes=metrics_attributes, status_code=200, status_code_str="200", server_span=True, sem_conv_opt_in_mode=_StabilityMode.HTTP, ) # Verify only new conventions are emitted span.set_attribute.assert_called_with("http.response.status_code", 200) self.assertIn("http.response.status_code", metrics_attributes) self.assertNotIn("http.status_code", metrics_attributes) def test_status_code_http_dup(self): span = Mock() metrics_attributes = {} _set_status( span=span, metrics_attributes=metrics_attributes, status_code=500, status_code_str="500", server_span=True, sem_conv_opt_in_mode=_StabilityMode.HTTP_DUP, ) # Verify both old and new conventions are emitted span.set_attribute.assert_any_call("http.status_code", 500) span.set_attribute.assert_any_call("http.response.status_code", 500) self.assertIn("http.status_code", metrics_attributes) self.assertIn("http.response.status_code", metrics_attributes) def test_error_status_code_new_mode(self): span = Mock() metrics_attributes = {} _set_status( span=span, metrics_attributes=metrics_attributes, status_code=500, status_code_str="500", server_span=True, sem_conv_opt_in_mode=_StabilityMode.HTTP, ) # Verify error type is set for new conventions span.set_attribute.assert_any_call("error.type", "500") self.assertIn("error.type", metrics_attributes) self.assertEqual(metrics_attributes["error.type"], "500") def test_non_recording_span(self): span = Mock() span.is_recording.return_value = False metrics_attributes = {} _set_status( span=span, metrics_attributes=metrics_attributes, status_code=200, status_code_str="200", server_span=True, sem_conv_opt_in_mode=_StabilityMode.HTTP_DUP, ) # Verify no span attributes are set if not recording span.set_attribute.assert_not_called() span.set_status.assert_not_called() # Verify status code set for metrics independent of tracing decision self.assertIn("http.status_code", metrics_attributes) self.assertIn("http.response.status_code", metrics_attributes) # pylint: disable=too-many-public-methods class TestOpenTelemetrySemConvStabilityDatabase(TestCase): def test_db_system_default(self): result = {} _set_db_system(result, "postgresql", sem_conv_opt_in_mode=_StabilityMode.DEFAULT) self.assertIn(DB_SYSTEM, result) self.assertEqual(result[DB_SYSTEM], "postgresql") self.assertNotIn(DB_SYSTEM_NAME, result) def test_db_system_database_stable(self): result = {} _set_db_system(result, "postgresql", sem_conv_opt_in_mode=_StabilityMode.DATABASE) self.assertNotIn(DB_SYSTEM, result) self.assertIn(DB_SYSTEM_NAME, result) self.assertEqual(result[DB_SYSTEM_NAME], "postgresql") def test_db_system_database_dup(self): result = {} _set_db_system( result, "postgresql", sem_conv_opt_in_mode=_StabilityMode.DATABASE_DUP, ) self.assertIn(DB_SYSTEM, result) self.assertEqual(result[DB_SYSTEM], "postgresql") self.assertIn(DB_SYSTEM_NAME, result) self.assertEqual(result[DB_SYSTEM_NAME], "postgresql") def test_db_system_none_value(self): result = {} _set_db_system(result, None, sem_conv_opt_in_mode=_StabilityMode.DATABASE_DUP) self.assertNotIn(DB_SYSTEM, result) self.assertNotIn(DB_SYSTEM_NAME, result) def test_db_name_default(self): result = {} _set_db_name(result, "my_database", sem_conv_opt_in_mode=_StabilityMode.DEFAULT) self.assertIn(DB_NAME, result) self.assertEqual(result[DB_NAME], "my_database") self.assertNotIn(DB_NAMESPACE, result) def test_db_name_database_stable(self): result = {} _set_db_name(result, "my_database", sem_conv_opt_in_mode=_StabilityMode.DATABASE) self.assertNotIn(DB_NAME, result) self.assertIn(DB_NAMESPACE, result) self.assertEqual(result[DB_NAMESPACE], "my_database") def test_db_name_database_dup(self): result = {} _set_db_name( result, "my_database", sem_conv_opt_in_mode=_StabilityMode.DATABASE_DUP, ) self.assertIn(DB_NAME, result) self.assertEqual(result[DB_NAME], "my_database") self.assertIn(DB_NAMESPACE, result) self.assertEqual(result[DB_NAMESPACE], "my_database") def test_db_name_none_value(self): result = {} _set_db_name(result, None, sem_conv_opt_in_mode=_StabilityMode.DATABASE_DUP) self.assertNotIn(DB_NAME, result) self.assertNotIn(DB_NAMESPACE, result) def test_db_redis_database_index_default(self): result = {} _set_db_redis_database_index(result, 0, sem_conv_opt_in_mode=_StabilityMode.DEFAULT) self.assertIn(DB_REDIS_DATABASE_INDEX, result) self.assertEqual(result[DB_REDIS_DATABASE_INDEX], 0) self.assertNotIn(DB_NAMESPACE, result) def test_db_redis_database_index_database_stable(self): result = {} _set_db_redis_database_index(result, 0, sem_conv_opt_in_mode=_StabilityMode.DATABASE) self.assertNotIn(DB_REDIS_DATABASE_INDEX, result) self.assertNotIn(DB_NAMESPACE, result) def test_db_redis_database_index_database_dup(self): result = {} _set_db_redis_database_index(result, 0, sem_conv_opt_in_mode=_StabilityMode.DATABASE_DUP) self.assertIn(DB_REDIS_DATABASE_INDEX, result) self.assertEqual(result[DB_REDIS_DATABASE_INDEX], 0) self.assertNotIn(DB_NAMESPACE, result) def test_db_redis_database_index_none_value(self): result = {} _set_db_redis_database_index(result, None, sem_conv_opt_in_mode=_StabilityMode.DATABASE_DUP) self.assertNotIn(DB_REDIS_DATABASE_INDEX, result) self.assertNotIn(DB_NAMESPACE, result) def test_db_statement_default(self): result = {} _set_db_statement( result, "SELECT * FROM users", sem_conv_opt_in_mode=_StabilityMode.DEFAULT, ) self.assertIn(DB_STATEMENT, result) self.assertEqual(result[DB_STATEMENT], "SELECT * FROM users") self.assertNotIn(DB_QUERY_TEXT, result) def test_db_statement_database_stable(self): result = {} _set_db_statement( result, "SELECT * FROM users", sem_conv_opt_in_mode=_StabilityMode.DATABASE, ) self.assertNotIn(DB_STATEMENT, result) self.assertIn(DB_QUERY_TEXT, result) self.assertEqual(result[DB_QUERY_TEXT], "SELECT * FROM users") def test_db_statement_database_dup(self): result = {} _set_db_statement( result, "SELECT * FROM users", sem_conv_opt_in_mode=_StabilityMode.DATABASE_DUP, ) self.assertIn(DB_STATEMENT, result) self.assertEqual(result[DB_STATEMENT], "SELECT * FROM users") self.assertIn(DB_QUERY_TEXT, result) self.assertEqual(result[DB_QUERY_TEXT], "SELECT * FROM users") def test_db_statement_empty(self): result = {} _set_db_statement(result, "", sem_conv_opt_in_mode=_StabilityMode.DEFAULT) self.assertEqual(result[DB_STATEMENT], "") def test_db_statement_none_value(self): result = {} _set_db_statement(result, None, sem_conv_opt_in_mode=_StabilityMode.DEFAULT) self.assertNotIn(DB_STATEMENT, result) self.assertNotIn(DB_QUERY_TEXT, result) def test_db_user_default(self): result = {} _set_db_user(result, "admin", sem_conv_opt_in_mode=_StabilityMode.DEFAULT) self.assertIn(DB_USER, result) self.assertEqual(result[DB_USER], "admin") def test_db_user_database_stable(self): result = {} _set_db_user(result, "admin", sem_conv_opt_in_mode=_StabilityMode.DATABASE) # No new attribute - db.user was removed with no replacement self.assertNotIn(DB_USER, result) def test_db_user_database_dup(self): result = {} _set_db_user( result, "admin", sem_conv_opt_in_mode=_StabilityMode.DATABASE_DUP, ) self.assertIn(DB_USER, result) self.assertEqual(result[DB_USER], "admin") def test_db_user_none_value(self): result = {} _set_db_user(result, None, sem_conv_opt_in_mode=_StabilityMode.DEFAULT) self.assertNotIn(DB_USER, result) def test_db_operation_default(self): result = {} _set_db_operation( result, "SELECT", sem_conv_opt_in_mode=_StabilityMode.DEFAULT, ) self.assertIn(DB_OPERATION, result) self.assertEqual(result[DB_OPERATION], "SELECT") self.assertNotIn(DB_OPERATION_NAME, result) def test_db_operation_database_stable(self): result = {} _set_db_operation( result, "SELECT", sem_conv_opt_in_mode=_StabilityMode.DATABASE, ) self.assertNotIn(DB_OPERATION, result) self.assertIn(DB_OPERATION_NAME, result) self.assertEqual(result[DB_OPERATION_NAME], "SELECT") def test_db_operation_database_dup(self): result = {} _set_db_operation( result, "SELECT", sem_conv_opt_in_mode=_StabilityMode.DATABASE_DUP, ) self.assertIn(DB_OPERATION, result) self.assertEqual(result[DB_OPERATION], "SELECT") self.assertIn(DB_OPERATION_NAME, result) self.assertEqual(result[DB_OPERATION_NAME], "SELECT") def test_db_operation_none_value(self): result = {} _set_db_operation(result, None, sem_conv_opt_in_mode=_StabilityMode.DEFAULT) self.assertNotIn(DB_OPERATION, result) self.assertNotIn(DB_OPERATION_NAME, result) opentelemetry-python-contrib-0.66b2/opentelemetry-instrumentation/tests/test_utils.py000066400000000000000000000422511527070072300316210ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import sys import unittest from http import HTTPStatus from unittest import mock import pytest from wrapt import wrap_function_wrapper try: # wrapt 2.0.0+ from wrapt import BaseObjectProxy # pylint: disable=no-name-in-module except ImportError: from wrapt import ObjectProxy as BaseObjectProxy from opentelemetry.context import ( _SUPPRESS_HTTP_INSTRUMENTATION_KEY, _SUPPRESS_INSTRUMENTATION_KEY, get_current, get_value, ) from opentelemetry.instrumentation.sqlcommenter_utils import _add_sql_comment from opentelemetry.instrumentation.utils import ( _python_path_without_directory, http_status_to_status_code, is_http_instrumentation_enabled, is_instrumentation_enabled, suppress_http_instrumentation, suppress_instrumentation, unwrap, ) from opentelemetry.trace import StatusCode class _MockInterpolation: def __init__(self, expression: str) -> None: self.expression = expression class _MockTemplate: """Mock for string.templatelib.Template""" def __init__(self, *args) -> None: strings: list[str] = [] interpolations: list[_MockInterpolation] = [] for arg in args: if isinstance(arg, str): strings.append(arg) else: interpolations.append(arg) self.strings = tuple(strings) self.interpolations = interpolations class WrappedClass: def method(self): pass def wrapper_method(self): pass # pylint: disable=too-many-public-methods class TestUtils(unittest.TestCase): # See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status def test_http_status_to_status_code(self): for status_code, expected in ( (HTTPStatus.OK, StatusCode.UNSET), (HTTPStatus.ACCEPTED, StatusCode.UNSET), (HTTPStatus.IM_USED, StatusCode.UNSET), (HTTPStatus.MULTIPLE_CHOICES, StatusCode.UNSET), (HTTPStatus.BAD_REQUEST, StatusCode.ERROR), (HTTPStatus.UNAUTHORIZED, StatusCode.ERROR), (HTTPStatus.FORBIDDEN, StatusCode.ERROR), (HTTPStatus.NOT_FOUND, StatusCode.ERROR), ( HTTPStatus.UNPROCESSABLE_ENTITY, StatusCode.ERROR, ), ( HTTPStatus.TOO_MANY_REQUESTS, StatusCode.ERROR, ), (HTTPStatus.NOT_IMPLEMENTED, StatusCode.ERROR), (HTTPStatus.SERVICE_UNAVAILABLE, StatusCode.ERROR), ( HTTPStatus.GATEWAY_TIMEOUT, StatusCode.ERROR, ), ( HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, StatusCode.ERROR, ), (600, StatusCode.ERROR), (99, StatusCode.ERROR), ): with self.subTest(status_code=status_code): actual = http_status_to_status_code(int(status_code)) self.assertEqual(actual, expected, status_code) def test_http_status_to_status_code_none(self): for status_code, expected in ((None, StatusCode.UNSET),): with self.subTest(status_code=status_code): actual = http_status_to_status_code(status_code) self.assertEqual(actual, expected, status_code) def test_http_status_to_status_code_redirect(self): for status_code, expected in ( (HTTPStatus.MULTIPLE_CHOICES, StatusCode.ERROR), (HTTPStatus.MOVED_PERMANENTLY, StatusCode.ERROR), (HTTPStatus.TEMPORARY_REDIRECT, StatusCode.ERROR), (HTTPStatus.PERMANENT_REDIRECT, StatusCode.ERROR), ): with self.subTest(status_code=status_code): actual = http_status_to_status_code(int(status_code), allow_redirect=False) self.assertEqual(actual, expected, status_code) def test_http_status_to_status_code_server(self): for status_code, expected in ( (HTTPStatus.OK, StatusCode.UNSET), (HTTPStatus.ACCEPTED, StatusCode.UNSET), (HTTPStatus.IM_USED, StatusCode.UNSET), (HTTPStatus.MULTIPLE_CHOICES, StatusCode.UNSET), (HTTPStatus.BAD_REQUEST, StatusCode.UNSET), (HTTPStatus.UNAUTHORIZED, StatusCode.UNSET), (HTTPStatus.FORBIDDEN, StatusCode.UNSET), (HTTPStatus.NOT_FOUND, StatusCode.UNSET), ( HTTPStatus.UNPROCESSABLE_ENTITY, StatusCode.UNSET, ), ( HTTPStatus.TOO_MANY_REQUESTS, StatusCode.UNSET, ), (HTTPStatus.NOT_IMPLEMENTED, StatusCode.ERROR), (HTTPStatus.SERVICE_UNAVAILABLE, StatusCode.ERROR), ( HTTPStatus.GATEWAY_TIMEOUT, StatusCode.ERROR, ), ( HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, StatusCode.ERROR, ), (600, StatusCode.ERROR), (99, StatusCode.ERROR), ): with self.subTest(status_code=status_code): actual = http_status_to_status_code(int(status_code), server_span=True) self.assertEqual(actual, expected, status_code) def test_remove_current_directory_from_python_path_windows(self): directory = r"c:\users\Trayvon Martin\workplace\opentelemetry-python-contrib\opentelemetry-instrumentation\src\opentelemetry\instrumentation\auto_instrumentation" path_separator = r";" python_path = r"c:\users\Trayvon Martin\workplace\opentelemetry-python-contrib\opentelemetry-instrumentation\src\opentelemetry\instrumentation\auto_instrumentation;C:\Users\trayvonmartin\workplace" actual_python_path = _python_path_without_directory(python_path, directory, path_separator) expected_python_path = r"C:\Users\trayvonmartin\workplace" self.assertEqual(actual_python_path, expected_python_path) def test_remove_current_directory_from_python_path_linux(self): directory = r"/home/georgefloyd/workplace/opentelemetry-python-contrib/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation" path_separator = r":" python_path = r"/home/georgefloyd/workplace/opentelemetry-python-contrib/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation:/home/georgefloyd/workplace" actual_python_path = _python_path_without_directory(python_path, directory, path_separator) expected_python_path = r"/home/georgefloyd/workplace" self.assertEqual(actual_python_path, expected_python_path) def test_remove_current_directory_from_python_path_windows_only_path(self): directory = r"c:\users\Charleena Lyles\workplace\opentelemetry-python-contrib\opentelemetry-instrumentation\src\opentelemetry\instrumentation\auto_instrumentation" path_separator = r";" python_path = r"c:\users\Charleena Lyles\workplace\opentelemetry-python-contrib\opentelemetry-instrumentation\src\opentelemetry\instrumentation\auto_instrumentation" actual_python_path = _python_path_without_directory(python_path, directory, path_separator) self.assertEqual(actual_python_path, python_path) def test_remove_current_directory_from_python_path_linux_only_path(self): directory = r"/home/SandraBland/workplace/opentelemetry-python-contrib/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation" path_separator = r":" python_path = r"/home/SandraBland/workplace/opentelemetry-python-contrib/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation" actual_python_path = _python_path_without_directory(python_path, directory, path_separator) self.assertEqual(actual_python_path, python_path) def test_add_sql_comments_with_semicolon(self): sql_query_without_semicolon = "Select 1;" comments = {"comment_1": "value 1", "comment 2": "value 3"} commented_sql_without_semicolon = _add_sql_comment(sql_query_without_semicolon, **comments) self.assertEqual( commented_sql_without_semicolon, "Select 1 /*comment%%202='value%%203',comment_1='value%%201'*/;", ) def test_add_sql_comments_without_semicolon(self): sql_query_without_semicolon = "Select 1" comments = {"comment_1": "value 1", "comment 2": "value 3"} commented_sql_without_semicolon = _add_sql_comment(sql_query_without_semicolon, **comments) self.assertEqual( commented_sql_without_semicolon, "Select 1 /*comment%%202='value%%203',comment_1='value%%201'*/", ) def test_add_sql_comments_without_comments(self): sql_query_without_semicolon = "Select 1" comments = {} commented_sql_without_semicolon = _add_sql_comment(sql_query_without_semicolon, **comments) self.assertEqual(commented_sql_without_semicolon, "Select 1") @mock.patch( "opentelemetry.instrumentation.sqlcommenter_utils._Template", _MockTemplate, ) def test_add_sql_comment_t_string_without_semicolon(self): template = _MockTemplate("SELECT ", _MockInterpolation("val"), " FROM foo") result = _add_sql_comment(template, k="v") self.assertIsInstance(result, _MockTemplate) self.assertEqual(result.strings, ("SELECT ", " FROM foo /*k='v'*/")) self.assertEqual(result.interpolations[0].expression, "val") @mock.patch( "opentelemetry.instrumentation.sqlcommenter_utils._Template", _MockTemplate, ) def test_add_sql_comment_t_string_with_semicolon(self): template = _MockTemplate("SELECT ", _MockInterpolation("val"), " FROM foo;") result = _add_sql_comment(template, k="v") self.assertIsInstance(result, _MockTemplate) self.assertEqual(result.strings, ("SELECT ", " FROM foo /*k='v'*/;")) self.assertEqual(result.interpolations[0].expression, "val") @mock.patch( "opentelemetry.instrumentation.sqlcommenter_utils._Template", _MockTemplate, ) def test_add_sql_comment_t_string_no_meta(self): template = _MockTemplate("SELECT ", _MockInterpolation("val"), " FROM foo") result = _add_sql_comment(template) self.assertIsInstance(result, _MockTemplate) self.assertEqual(result.strings, ("SELECT ", " FROM foo")) self.assertEqual(result.interpolations[0].expression, "val") @mock.patch( "opentelemetry.instrumentation.sqlcommenter_utils._Template", _MockTemplate, ) def test_add_sql_comment_t_string_multiple_interpolations(self): template = _MockTemplate( "INSERT INTO t (a, b) VALUES (", _MockInterpolation("a"), ", ", _MockInterpolation("b"), ")", ) result = _add_sql_comment(template, k="v") self.assertIsInstance(result, _MockTemplate) self.assertEqual( result.strings, ("INSERT INTO t (a, b) VALUES (", ", ", ") /*k='v'*/"), ) self.assertEqual(result.interpolations[0].expression, "a") self.assertEqual(result.interpolations[1].expression, "b") @pytest.mark.skipif( sys.version_info < (3, 14), reason="requires Python 3.14+ for t-strings", ) def test_add_sql_comment_real_t_string_without_semicolon(self): # pylint: disable-next=import-outside-toplevel from string.templatelib import Interpolation, Template # noqa: PLC0415 val = 42 template = Template("SELECT ", Interpolation(val, "val"), " FROM foo") result = _add_sql_comment(template, k="v") self.assertIsInstance(result, Template) self.assertEqual(result.strings, ("SELECT ", " FROM foo /*k='v'*/")) self.assertEqual(result.interpolations[0].expression, "val") @pytest.mark.skipif( sys.version_info < (3, 14), reason="requires Python 3.14+ for t-strings", ) def test_add_sql_comment_real_t_string_with_semicolon(self): # pylint: disable-next=import-outside-toplevel from string.templatelib import Interpolation, Template # noqa: PLC0415 val = 42 template = Template("SELECT ", Interpolation(val, "val"), " FROM foo;") result = _add_sql_comment(template, k="v") self.assertIsInstance(result, Template) self.assertEqual(result.strings, ("SELECT ", " FROM foo /*k='v'*/;")) self.assertEqual(result.interpolations[0].expression, "val") def test_is_instrumentation_enabled_by_default(self): self.assertTrue(is_instrumentation_enabled()) self.assertTrue(is_http_instrumentation_enabled()) def test_suppress_instrumentation(self): with suppress_instrumentation(): self.assertFalse(is_instrumentation_enabled()) self.assertFalse(is_http_instrumentation_enabled()) self.assertTrue(is_instrumentation_enabled()) self.assertTrue(is_http_instrumentation_enabled()) def test_suppress_http_instrumentation(self): with suppress_http_instrumentation(): self.assertFalse(is_http_instrumentation_enabled()) self.assertTrue(is_instrumentation_enabled()) self.assertTrue(is_instrumentation_enabled()) self.assertTrue(is_http_instrumentation_enabled()) def test_suppress_instrumentation_key(self): self.assertIsNone(get_value(_SUPPRESS_INSTRUMENTATION_KEY)) self.assertIsNone(get_value("suppress_instrumentation")) with suppress_instrumentation(): ctx = get_current() self.assertIn(_SUPPRESS_INSTRUMENTATION_KEY, ctx) self.assertIn("suppress_instrumentation", ctx) self.assertTrue(get_value(_SUPPRESS_INSTRUMENTATION_KEY)) self.assertTrue(get_value("suppress_instrumentation")) self.assertIsNone(get_value(_SUPPRESS_INSTRUMENTATION_KEY)) self.assertIsNone(get_value("suppress_instrumentation")) def test_suppress_http_instrumentation_key(self): self.assertIsNone(get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY)) with suppress_http_instrumentation(): ctx = get_current() self.assertIn(_SUPPRESS_HTTP_INSTRUMENTATION_KEY, ctx) self.assertTrue(get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY)) self.assertIsNone(get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY)) class UnwrapTestCase(unittest.TestCase): @staticmethod def _wrap_method(): return wrap_function_wrapper(WrappedClass, "method", WrappedClass.wrapper_method) def test_can_unwrap_object_attribute(self): self._wrap_method() instance = WrappedClass() self.assertTrue(isinstance(instance.method, BaseObjectProxy)) unwrap(WrappedClass, "method") self.assertFalse(isinstance(instance.method, BaseObjectProxy)) def test_can_unwrap_object_attribute_as_string(self): self._wrap_method() instance = WrappedClass() self.assertTrue(isinstance(instance.method, BaseObjectProxy)) unwrap("tests.test_utils.WrappedClass", "method") self.assertFalse(isinstance(instance.method, BaseObjectProxy)) def test_raises_import_error_if_path_not_well_formed(self): self._wrap_method() instance = WrappedClass() self.assertTrue(isinstance(instance.method, BaseObjectProxy)) with self.assertRaisesRegex(ImportError, "Cannot parse '' as dotted import path"): unwrap("", "method") unwrap(WrappedClass, "method") self.assertFalse(isinstance(instance.method, BaseObjectProxy)) def test_noop_if_module_not_imported(self): # A module that exists but hasn't been imported, treated # as no-op. self.assertNotIn("email.generator", sys.modules) unwrap("email.generator.BytesGenerator", "flatten") self.assertNotIn("email.generator", sys.modules) def test_noop_if_cannot_find_module(self): self._wrap_method() instance = WrappedClass() self.assertTrue(isinstance(instance.method, BaseObjectProxy)) # Treated same as an existing module that hasn't been imported, # as a no-op. unwrap("does.not.exist.WrappedClass", "method") unwrap(WrappedClass, "method") self.assertFalse(isinstance(instance.method, BaseObjectProxy)) def test_raises_import_error_if_cannot_find_object(self): self._wrap_method() instance = WrappedClass() self.assertTrue(isinstance(instance.method, BaseObjectProxy)) with self.assertRaisesRegex(ImportError, "Cannot import 'NotWrappedClass' from"): unwrap("tests.test_utils.NotWrappedClass", "method") unwrap(WrappedClass, "method") self.assertFalse(isinstance(instance.method, BaseObjectProxy)) # pylint: disable=no-self-use def test_does_nothing_if_cannot_find_attribute(self): instance = WrappedClass() unwrap(instance, "method_not_found") def test_does_nothing_if_attribute_is_not_from_wrapt(self): instance = WrappedClass() self.assertFalse(isinstance(instance.method, BaseObjectProxy)) unwrap(WrappedClass, "method") self.assertFalse(isinstance(instance.method, BaseObjectProxy)) opentelemetry-python-contrib-0.66b2/processor/000077500000000000000000000000001527070072300215645ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/processor/opentelemetry-processor-baggage/000077500000000000000000000000001527070072300300505ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/processor/opentelemetry-processor-baggage/LICENSE000066400000000000000000000261351527070072300310640ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/processor/opentelemetry-processor-baggage/README.rst000066400000000000000000000067671527070072300315570ustar00rootroot00000000000000OpenTelemetry Baggage Span Processor ==================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-processor-baggage.svg :target: https://pypi.org/project/opentelemetry-processor-baggage/ The BaggageSpanProcessor reads entries stored in Baggage from the parent context and adds the baggage entries' keys and values to the span as attributes on span start. Installation ------------ :: pip install opentelemetry-processor-baggage Add this span processor to a tracer provider. Keys and values added to Baggage will appear on subsequent child spans for a trace within this service *and* be propagated to external services in accordance with any configured propagation formats configured. If the external services also have a Baggage span processor, the keys and values will appear in those child spans as well. [!WARNING] Do not put sensitive information in Baggage. To repeat: a consequence of adding data to Baggage is that the keys and values will appear in all outgoing HTTP headers from the application. Usage ----- Add the span processor when configuring the tracer provider. To configure the span processor to copy all baggage entries during configuration: :: from opentelemetry.processor.baggage import BaggageSpanProcessor, ALLOW_ALL_BAGGAGE_KEYS tracer_provider = TracerProvider() tracer_provider.add_span_processor(BaggageSpanProcessor(ALLOW_ALL_BAGGAGE_KEYS)) Alternatively, you can provide a custom baggage key predicate to select which baggage keys you want to copy. For example, to only copy baggage entries that start with `my-key`: :: starts_with_predicate = lambda baggage_key: baggage_key.startswith("my-key") tracer_provider.add_span_processor(BaggageSpanProcessor(starts_with_predicate)) For example, to only copy baggage entries that match the regex `^key.+`: :: regex_predicate = lambda baggage_key: baggage_key.startswith("^key.+") tracer_provider.add_span_processor(BaggageSpanProcessor(regex_predicate)) BaggageLogProcessor ------------------- The BaggageLogProcessor reads entries stored in Baggage from the current context and adds the baggage entries' keys and values to the log record as attributes on emit. Add this log processor to a logger provider. To configure the log processor to copy all baggage entries: :: from opentelemetry.processor.baggage import BaggageLogProcessor, ALLOW_ALL_BAGGAGE_KEYS logger_provider = LoggerProvider() logger_provider.add_log_record_processor(BaggageLogProcessor(ALLOW_ALL_BAGGAGE_KEYS)) Alternatively, you can provide a custom baggage key predicate to select which baggage keys you want to copy. For example, to only copy baggage entries that start with `my-key`: :: starts_with_predicate = lambda baggage_key: baggage_key.startswith("my-key") logger_provider.add_log_record_processor(BaggageLogProcessor(starts_with_predicate)) For example, to only copy baggage entries that match the regex `^key.+`: :: regex_predicate = lambda baggage_key: re.match(r"^key.+", baggage_key) is not None logger_provider.add_log_record_processor(BaggageLogProcessor(regex_predicate)) For example, to copy baggage entries matching multiple predicates: :: multiple_predicates = [ lambda baggage_key: baggage_key.startswith("my-key"), lambda baggage_key: baggage_key.startswith("other-key"), ] logger_provider.add_log_record_processor(BaggageLogProcessor(multiple_predicates)) References ---------- * `OpenTelemetry Project `_ opentelemetry-python-contrib-0.66b2/processor/opentelemetry-processor-baggage/pyproject.toml000066400000000000000000000025331527070072300327670ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-processor-baggage" dynamic = ["version"] description = "OpenTelemetry Baggage Span Processor" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.5", "opentelemetry-sdk ~= 1.5", "wrapt >= 1.0.0, < 3.0.0", ] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/processor/opentelemetry-processor-baggage" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/processor/baggage/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/processor/opentelemetry-processor-baggage/src/000077500000000000000000000000001527070072300306375ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/processor/opentelemetry-processor-baggage/src/opentelemetry/000077500000000000000000000000001527070072300335335ustar00rootroot00000000000000processor/000077500000000000000000000000001527070072300354735ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/processor/opentelemetry-processor-baggage/src/opentelemetrybaggage/000077500000000000000000000000001527070072300370505ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/processor/opentelemetry-processor-baggage/src/opentelemetry/processor__init__.py000066400000000000000000000005671527070072300411710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/processor/opentelemetry-processor-baggage/src/opentelemetry/processor/baggage# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=import-error from .log_processor import BaggageLogProcessor from .processor import ALLOW_ALL_BAGGAGE_KEYS, BaggageSpanProcessor from .version import __version__ __all__ = [ "ALLOW_ALL_BAGGAGE_KEYS", "BaggageLogProcessor", "BaggageSpanProcessor", "__version__", ] log_processor.py000066400000000000000000000045731527070072300423130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/processor/opentelemetry-processor-baggage/src/opentelemetry/processor/baggage# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Sequence from opentelemetry.baggage import get_all as get_all_baggage from opentelemetry.processor.baggage.processor import BaggageKeyPredicateT from opentelemetry.sdk._logs import LogRecordProcessor, ReadWriteLogRecord BaggageKeyPredicate = BaggageKeyPredicateT | Sequence[BaggageKeyPredicateT] class BaggageLogProcessor(LogRecordProcessor): """ The BaggageLogProcessor reads entries stored in Baggage from the current context and adds the baggage entries' keys and values to the log record as attributes on emit. Add this log processor to a logger provider. ⚠ Warning ⚠️ Do not put sensitive information in Baggage. To repeat: a consequence of adding data to Baggage is that the keys and values will appear in all outgoing HTTP headers from the application. """ def __init__( self, baggage_key_predicate: BaggageKeyPredicate, max_baggage_attributes: int = 128, ) -> None: if callable(baggage_key_predicate): self._predicates = [baggage_key_predicate] else: self._predicates = list(baggage_key_predicate) self._max_baggage_attributes = max_baggage_attributes def _matches(self, key: str) -> bool: return any(predicate(key) for predicate in self._predicates) def on_emit(self, log_record: ReadWriteLogRecord) -> None: """Add baggage entries as log record attributes on emit. Baggage keys are filtered using the provided predicate(s). If a baggage key already exists in the log record attributes, it will not be overwritten to avoid collisions with attributes added by stdlib logging, calls to logging.emit, or custom LogRecordProcessors. At most max_baggage_attributes baggage entries will be added. """ baggage = get_all_baggage() count = 0 for key, value in baggage.items(): if count >= self._max_baggage_attributes: break if self._matches(key): if key not in log_record.log_record.attributes: log_record.log_record.attributes[key] = value count += 1 def shutdown(self) -> None: pass def force_flush(self, timeout_millis: int = 30000) -> bool: return True processor.py000066400000000000000000000041551527070072300414460ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/processor/opentelemetry-processor-baggage/src/opentelemetry/processor/baggage# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable, Sequence from opentelemetry.baggage import get_all as get_all_baggage from opentelemetry.context import Context from opentelemetry.sdk.trace.export import SpanProcessor from opentelemetry.trace import Span # A BaggageKeyPredicate is a function that takes a baggage key and returns a boolean BaggageKeyPredicateT = Callable[[str], bool] BaggageKeyPredicates = BaggageKeyPredicateT | Sequence[BaggageKeyPredicateT] # A BaggageKeyPredicate that always returns True, allowing all baggage keys to be added to spans ALLOW_ALL_BAGGAGE_KEYS: BaggageKeyPredicateT = lambda _: True # pylint:disable=invalid-name # noqa: E731 class BaggageSpanProcessor(SpanProcessor): """ The BaggageSpanProcessor reads entries stored in Baggage from the parent context and adds the baggage entries' keys and values to the span as attributes on span start. Add this span processor to a tracer provider. Keys and values added to Baggage will appear on subsequent child spans for a trace within this service *and* be propagated to external services in accordance with any configured propagation formats configured. If the external services also have a Baggage span processor, the keys and values will appear in those child spans as well. ⚠ Warning ⚠️ Do not put sensitive information in Baggage. To repeat: a consequence of adding data to Baggage is that the keys and values will appear in all outgoing HTTP headers from the application. """ def __init__(self, baggage_key_predicate: BaggageKeyPredicates) -> None: if callable(baggage_key_predicate): self._predicates = [baggage_key_predicate] else: self._predicates = list(baggage_key_predicate) def on_start(self, span: "Span", parent_context: Context | None = None) -> None: baggage = get_all_baggage(parent_context) for key, value in baggage.items(): if any(predicate(key) for predicate in self._predicates): span.set_attribute(key, value) version.py000066400000000000000000000001441527070072300411060ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/processor/opentelemetry-processor-baggage/src/opentelemetry/processor/baggage# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" opentelemetry-python-contrib-0.66b2/processor/opentelemetry-processor-baggage/test-requirements.txt000066400000000000000000000002351527070072300343110ustar00rootroot00000000000000importlib_metadata==8.0.0 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 pytest==7.4.4 Deprecated==1.2.14 -e processor/opentelemetry-processor-baggage opentelemetry-python-contrib-0.66b2/processor/opentelemetry-processor-baggage/tests/000077500000000000000000000000001527070072300312125ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/processor/opentelemetry-processor-baggage/tests/__init__.py000066400000000000000000000001141527070072300333170ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 test_baggage_log_processor.py000066400000000000000000000121731527070072300370650ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/processor/opentelemetry-processor-baggage/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import re import unittest from opentelemetry.baggage import set_baggage from opentelemetry.context import attach, detach from opentelemetry.processor.baggage import ( ALLOW_ALL_BAGGAGE_KEYS, BaggageLogProcessor, ) from opentelemetry.sdk._logs import LoggerProvider, LogRecordProcessor from opentelemetry.sdk._logs.export import ( BatchLogRecordProcessor, InMemoryLogRecordExporter, ) class BaggageLogProcessorTest(unittest.TestCase): def setUp(self): self.exporter = InMemoryLogRecordExporter() self.logger_provider = LoggerProvider() self.logger_provider.add_log_record_processor(BaggageLogProcessor(ALLOW_ALL_BAGGAGE_KEYS)) self.logger_provider.add_log_record_processor(BatchLogRecordProcessor(self.exporter)) self.logger = self.logger_provider.get_logger("test-logger") def _get_attributes(self): self.logger_provider.force_flush() logs = self.exporter.get_finished_logs() self.assertTrue(len(logs) > 0) return logs[-1].log_record.attributes def test_check_the_baggage(self): self.assertIsInstance(BaggageLogProcessor(ALLOW_ALL_BAGGAGE_KEYS), LogRecordProcessor) def test_baggage_added_to_log_record(self): token = attach(set_baggage("queen", "bee")) self.logger.emit(None) attributes = self._get_attributes() self.assertEqual(attributes.get("queen"), "bee") detach(token) def test_baggage_with_prefix(self): token = attach(set_baggage("queen", "bee")) logger_provider = LoggerProvider() logger_provider.add_log_record_processor(BaggageLogProcessor(lambda key: key.startswith("que"))) exporter = InMemoryLogRecordExporter() logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter)) logger = logger_provider.get_logger("test-logger") logger.emit(None) logger_provider.force_flush() logs = exporter.get_finished_logs() attributes = logs[-1].log_record.attributes self.assertEqual(attributes.get("queen"), "bee") detach(token) def test_baggage_with_regex(self): token = attach(set_baggage("queen", "bee")) logger_provider = LoggerProvider() logger_provider.add_log_record_processor(BaggageLogProcessor(lambda key: re.match(r"que.*", key) is not None)) exporter = InMemoryLogRecordExporter() logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter)) logger = logger_provider.get_logger("test-logger") logger.emit(None) logger_provider.force_flush() logs = exporter.get_finished_logs() attributes = logs[-1].log_record.attributes self.assertEqual(attributes.get("queen"), "bee") detach(token) def test_no_baggage_not_added(self): self.logger.emit(None) self.logger_provider.force_flush() logs = self.exporter.get_finished_logs() self.assertTrue(len(logs) > 0) attributes = logs[-1].log_record.attributes self.assertNotIn("queen", attributes) def test_multiple_predicates(self): token1 = attach(set_baggage("queen", "bee")) token2 = attach(set_baggage("king", "cobra")) logger_provider = LoggerProvider() logger_provider.add_log_record_processor( BaggageLogProcessor( [ lambda key: key.startswith("que"), lambda key: key.startswith("kin"), ] ) ) exporter = InMemoryLogRecordExporter() logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter)) logger = logger_provider.get_logger("test-logger") logger.emit(None) logger_provider.force_flush() logs = exporter.get_finished_logs() attributes = logs[-1].log_record.attributes self.assertEqual(attributes.get("queen"), "bee") self.assertEqual(attributes.get("king"), "cobra") detach(token2) detach(token1) def test_max_baggage_attributes_limit(self): token1 = attach(set_baggage("key1", "val1")) token2 = attach(set_baggage("key2", "val2")) token3 = attach(set_baggage("key3", "val3")) logger_provider = LoggerProvider() logger_provider.add_log_record_processor(BaggageLogProcessor(ALLOW_ALL_BAGGAGE_KEYS, max_baggage_attributes=2)) exporter = InMemoryLogRecordExporter() logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter)) logger = logger_provider.get_logger("test-logger") logger.emit(None) logger_provider.force_flush() logs = exporter.get_finished_logs() attributes = logs[-1].log_record.attributes self.assertEqual(len(attributes), 2) detach(token3) detach(token2) detach(token1) @staticmethod def has_prefix(baggage_key: str) -> bool: return baggage_key.startswith("que") @staticmethod def matches_regex(baggage_key: str) -> bool: return re.match(r"que.*", baggage_key) is not None if __name__ == "__main__": unittest.main() test_baggage_processor.py000066400000000000000000000142651527070072300362300ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/processor/opentelemetry-processor-baggage/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import re import unittest from opentelemetry.baggage import get_all as get_all_baggage from opentelemetry.baggage import set_baggage from opentelemetry.context import attach, detach from opentelemetry.processor.baggage import ( ALLOW_ALL_BAGGAGE_KEYS, BaggageSpanProcessor, ) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SpanProcessor from opentelemetry.trace import Span, Tracer class BaggageSpanProcessorTest(unittest.TestCase): def test_check_the_baggage(self): self.assertIsInstance(BaggageSpanProcessor(ALLOW_ALL_BAGGAGE_KEYS), SpanProcessor) def test_set_baggage_attaches_to_child_spans_and_detaches_properly_with_context( self, ): tracer_provider = TracerProvider() tracer_provider.add_span_processor(BaggageSpanProcessor(ALLOW_ALL_BAGGAGE_KEYS)) # tracer has no baggage to start tracer = tracer_provider.get_tracer("my-tracer") self.assertIsInstance(tracer, Tracer) self.assertEqual(get_all_baggage(), {}) # set baggage in context ctx = set_baggage("queen", "bee") with tracer.start_as_current_span(name="bumble", context=ctx) as bumble_span: # span should have baggage key-value pair in context self.assertEqual(get_all_baggage(ctx), {"queen": "bee"}) # span should have baggage key-value pair in attribute self.assertEqual(bumble_span._attributes["queen"], "bee") with tracer.start_as_current_span(name="child_span", context=ctx) as child_span: self.assertIsInstance(child_span, Span) # child span should have baggage key-value pair in context self.assertEqual(get_all_baggage(ctx), {"queen": "bee"}) # child span should have baggage key-value pair in attribute self.assertEqual(child_span._attributes["queen"], "bee") def test_baggage_span_processor_with_string_prefix( self, ): tracer_provider = TracerProvider() tracer_provider.add_span_processor(BaggageSpanProcessor(self.has_prefix)) # tracer has no baggage to start tracer = tracer_provider.get_tracer("my-tracer") self.assertIsInstance(tracer, Tracer) self.assertEqual(get_all_baggage(), {}) # set baggage in context ctx = set_baggage("queen", "bee") with tracer.start_as_current_span(name="bumble", context=ctx) as bumble_span: # span should have baggage key-value pair in context self.assertEqual(get_all_baggage(ctx), {"queen": "bee"}) # span should have baggage key-value pair in attribute self.assertEqual(bumble_span._attributes["queen"], "bee") with tracer.start_as_current_span(name="child_span", context=ctx) as child_span: self.assertIsInstance(child_span, Span) # child span should have baggage key-value pair in context self.assertEqual(get_all_baggage(ctx), {"queen": "bee"}) # child span should have baggage key-value pair in attribute self.assertEqual(child_span._attributes["queen"], "bee") def test_baggage_span_processor_with_regex( self, ): tracer_provider = TracerProvider() tracer_provider.add_span_processor(BaggageSpanProcessor(self.matches_regex)) # tracer has no baggage to start tracer = tracer_provider.get_tracer("my-tracer") self.assertIsInstance(tracer, Tracer) self.assertEqual(get_all_baggage(), {}) # set baggage in context ctx = set_baggage("queen", "bee") with tracer.start_as_current_span(name="bumble", context=ctx) as bumble_span: # span should have baggage key-value pair in context self.assertEqual(get_all_baggage(ctx), {"queen": "bee"}) # span should have baggage key-value pair in attribute self.assertEqual(bumble_span._attributes["queen"], "bee") with tracer.start_as_current_span(name="child_span", context=ctx) as child_span: self.assertIsInstance(child_span, Span) # child span should have baggage key-value pair in context self.assertEqual(get_all_baggage(ctx), {"queen": "bee"}) # child span should have baggage key-value pair in attribute self.assertEqual(child_span._attributes["queen"], "bee") def test_set_baggage_attaches_to_child_spans_and_detaches_properly_with_token( self, ): tracer_provider = TracerProvider() tracer_provider.add_span_processor(BaggageSpanProcessor(ALLOW_ALL_BAGGAGE_KEYS)) # tracer has no baggage to start tracer = tracer_provider.get_tracer("my-tracer") self.assertIsInstance(tracer, Tracer) self.assertEqual(get_all_baggage(), {}) # create a context token and set baggage honey_token = attach(set_baggage("bumble", "bee")) self.assertEqual(get_all_baggage(), {"bumble": "bee"}) # in a new span, ensure the baggage is there with tracer.start_as_current_span("parent") as span: self.assertEqual(get_all_baggage(), {"bumble": "bee"}) self.assertEqual(span._attributes["bumble"], "bee") # create a second context token and set more baggage moar_token = attach(set_baggage("moar", "bee")) self.assertEqual(get_all_baggage(), {"bumble": "bee", "moar": "bee"}) # in a child span, ensure all baggage is there as attributes with tracer.start_as_current_span("child") as child_span: self.assertEqual(get_all_baggage(), {"bumble": "bee", "moar": "bee"}) self.assertEqual(child_span._attributes["bumble"], "bee") self.assertEqual(child_span._attributes["moar"], "bee") detach(moar_token) detach(honey_token) self.assertEqual(get_all_baggage(), {}) @staticmethod def has_prefix(baggage_key: str) -> bool: return baggage_key.startswith("que") @staticmethod def matches_regex(baggage_key: str) -> bool: return re.match(r"que.*", baggage_key) is not None opentelemetry-python-contrib-0.66b2/propagator/000077500000000000000000000000001527070072300217235ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/000077500000000000000000000000001527070072300305445ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/.changelog/000077500000000000000000000000001527070072300325515ustar00rootroot00000000000000.gitignore000066400000000000000000000000141527070072300344550ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/.changelog!.gitignore 2961.changed000066400000000000000000000000531527070072300344040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/.changelogUpdate `opentelemetry-api` version to 1.16 3774.changed000066400000000000000000000000731527070072300344110ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/.changelogaws-xray-propagator: ensure trace state is not overwritten opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/CHANGELOG.md000066400000000000000000000011731527070072300323570ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Version 1.0.2 (2024-08-05) See [common CHANGELOG](../../CHANGELOG.md) for the changes in this and prior versions. opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/LICENSE000066400000000000000000000261351527070072300315600ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/MANIFEST.rst000066400000000000000000000002511527070072300325020ustar00rootroot00000000000000graft src graft tests global-exclude *.pyc global-exclude *.pyo global-exclude __pycache__/* include CHANGELOG.md include MANIFEST.in include README.rst include LICENSE opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/README.rst000066400000000000000000000026411527070072300322360ustar00rootroot00000000000000OpenTelemetry Propagator for AWS X-Ray Service ============================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-propagator-aws-xray.svg :target: https://pypi.org/project/opentelemetry-propagator-aws-xray/ This library provides the propagator necessary to inject or extract a tracing context across AWS services. Installation ------------ :: pip install opentelemetry-propagator-aws-xray Usage (AWS X-Ray Propagator) ---------------------------- **NOTE**: Because the parent context extracted from the `X-Amzn-Trace-Id` header assumes the context is _not_ sampled by default, users should make sure to add `Sampled=1` to their `X-Amzn-Trace-Id` headers so that the child spans are sampled. Use the provided AWS X-Ray Propagator to inject the necessary context into traces sent to external systems. This can be done by either setting this environment variable: :: export OTEL_PROPAGATORS = xray Or by setting this propagator in your instrumented application: .. code-block:: python from opentelemetry.propagate import set_global_textmap from opentelemetry.propagators.aws import AwsXRayPropagator set_global_textmap(AwsXRayPropagator()) References ---------- * `OpenTelemetry Project `_ * `AWS X-Ray Propagation Trace Header `_ benchmark-requirements.txt000066400000000000000000000000301527070072300356720ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xraypytest-benchmark==4.0.0 opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/benchmarks/000077500000000000000000000000001527070072300326615ustar00rootroot00000000000000test_benchmark_aws_xray_propagator.py000066400000000000000000000011321527070072300423150ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/benchmarks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from requests.structures import CaseInsensitiveDict from opentelemetry.propagators.aws.aws_xray_propagator import ( TRACE_HEADER_KEY, AwsXRayPropagator, ) XRAY_PROPAGATOR = AwsXRayPropagator() def test_extract_single_header(benchmark): benchmark( XRAY_PROPAGATOR.extract, {TRACE_HEADER_KEY: "bdb5b63237ed38aea578af665aa5aa60-00000000000000000c32d953d73ad225"}, ) def test_inject_empty_context(benchmark): benchmark(XRAY_PROPAGATOR.inject, {}, setter=CaseInsensitiveDict.__setitem__) opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/pyproject.toml000066400000000000000000000042611527070072300334630ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-propagator-aws-xray" dynamic = ["version"] description = "AWS X-Ray Propagator for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.16", ] [project.entry-points.opentelemetry_propagator] xray = "opentelemetry.propagators.aws:AwsXRayPropagator" xray-lambda = "opentelemetry.propagators.aws:AwsXRayLambdaPropagator" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/propagator/opentelemetry-propagator-aws-xray" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/propagators/aws/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] [tool.towncrier] directory = ".changelog" filename = "CHANGELOG.md" start_string = "\n" template = "../../scripts/changelog_template.j2" issue_format = "[#{issue}](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/{issue})" wrap = true issue_pattern = "^(\\d+)" [[tool.towncrier.type]] directory = "added" name = "Added" showcontent = true [[tool.towncrier.type]] directory = "changed" name = "Changed" showcontent = true [[tool.towncrier.type]] directory = "deprecated" name = "Deprecated" showcontent = true [[tool.towncrier.type]] directory = "removed" name = "Removed" showcontent = true [[tool.towncrier.type]] directory = "fixed" name = "Fixed" showcontent = true opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/src/000077500000000000000000000000001527070072300313335ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/src/opentelemetry/000077500000000000000000000000001527070072300342275ustar00rootroot00000000000000propagators/000077500000000000000000000000001527070072300365115ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/src/opentelemetryaws/000077500000000000000000000000001527070072300373035ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/src/opentelemetry/propagators__init__.py000066400000000000000000000003771527070072300414230ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/src/opentelemetry/propagators/aws# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.propagators.aws.aws_xray_propagator import ( AwsXRayLambdaPropagator, AwsXRayPropagator, ) __all__ = ["AwsXRayLambdaPropagator", "AwsXRayPropagator"] aws_xray_propagator.py000066400000000000000000000250051527070072300437520ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/src/opentelemetry/propagators/aws# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ AWS X-Ray Propagator -------------------- The **AWS X-Ray Propagator** provides a propagator that when used, adds a `trace header`_ to outgoing traces that is compatible with the AWS X-Ray backend service. This allows the trace context to be propagated when a trace spans multiple AWS services. The same propagator setup is used to extract a context sent by external systems so that child span have the correct parent context. **NOTE**: Because the parent context parsed from the ``X-Amzn-Trace-Id`` header assumes the context is _not_ sampled by default, users should make sure to add ``Sampled=1`` to their ``X-Amzn-Trace-Id`` headers so that the child spans are sampled. Usage ----- Use the provided AWS X-Ray Propagator to inject the necessary context into traces sent to external systems. This can be done by either setting this environment variable: :: export OTEL_PROPAGATORS = xray Or by setting this propagator in your instrumented application: .. code-block:: python from opentelemetry.propagate import set_global_textmap from opentelemetry.propagators.aws import AwsXRayPropagator set_global_textmap(AwsXRayPropagator()) API --- .. _trace header: https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader """ import logging from os import environ from opentelemetry import trace from opentelemetry.context import Context from opentelemetry.propagators.textmap import ( CarrierT, Getter, Setter, TextMapPropagator, default_getter, default_setter, ) TRACE_HEADER_KEY = "X-Amzn-Trace-Id" AWS_TRACE_HEADER_ENV_KEY = "_X_AMZN_TRACE_ID" KV_PAIR_DELIMITER = ";" KEY_AND_VALUE_DELIMITER = "=" TRACE_ID_KEY = "Root" TRACE_ID_LENGTH = 35 TRACE_ID_VERSION = "1" TRACE_ID_DELIMITER = "-" TRACE_ID_DELIMITER_INDEX_1 = 1 TRACE_ID_DELIMITER_INDEX_2 = 10 TRACE_ID_FIRST_PART_LENGTH = 8 PARENT_ID_KEY = "Parent" PARENT_ID_LENGTH = 16 SAMPLED_FLAG_KEY = "Sampled" SAMPLED_FLAG_LENGTH = 1 IS_SAMPLED = "1" NOT_SAMPLED = "0" _logger = logging.getLogger(__name__) class AwsParseTraceHeaderError(Exception): def __init__(self, message): super().__init__() self.message = message class AwsXRayPropagator(TextMapPropagator): """Propagator for the AWS X-Ray Trace Header propagation protocol. See: https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader """ # AWS def extract( self, carrier: CarrierT, context: Context | None = None, getter: Getter[CarrierT] = default_getter, ) -> Context: if context is None: context = Context() trace_header_list = getter.get(carrier, TRACE_HEADER_KEY) if not trace_header_list or len(trace_header_list) != 1: return context trace_header = trace_header_list[0] if not trace_header: return context try: ( trace_id, span_id, sampled, ) = AwsXRayPropagator._extract_span_properties(trace_header) except AwsParseTraceHeaderError as err: _logger.debug(err.message) return context options = 0 if sampled: options |= trace.TraceFlags.SAMPLED current_span = trace.get_current_span(context=context) if current_span and current_span.get_span_context(): tracestate = current_span.get_span_context().trace_state else: tracestate = trace.TraceState() span_context = trace.SpanContext( trace_id=trace_id, span_id=span_id, is_remote=True, trace_flags=trace.TraceFlags(options), trace_state=tracestate, ) if not span_context.is_valid: _logger.debug("Invalid Span Extracted. Inserting INVALID span into provided context.") return context return trace.set_span_in_context(trace.NonRecordingSpan(span_context), context=context) @staticmethod def _extract_span_properties(trace_header): trace_id = trace.INVALID_TRACE_ID span_id = trace.INVALID_SPAN_ID sampled = False for kv_pair_str in trace_header.split(KV_PAIR_DELIMITER): try: key_str, value_str = kv_pair_str.split(KEY_AND_VALUE_DELIMITER) key, value = key_str.strip(), value_str.strip() except ValueError as ex: raise AwsParseTraceHeaderError( ( "Error parsing X-Ray trace header. Invalid key value pair: %s. Returning INVALID span context.", kv_pair_str, ) ) from ex if key == TRACE_ID_KEY: if not AwsXRayPropagator._validate_trace_id(value): raise AwsParseTraceHeaderError( ( "Invalid TraceId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", TRACE_HEADER_KEY, trace_header, ) ) try: trace_id = AwsXRayPropagator._parse_trace_id(value) except ValueError as ex: raise AwsParseTraceHeaderError( ( "Invalid TraceId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", TRACE_HEADER_KEY, trace_header, ) ) from ex elif key == PARENT_ID_KEY: if not AwsXRayPropagator._validate_span_id(value): raise AwsParseTraceHeaderError( ( "Invalid ParentId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", TRACE_HEADER_KEY, trace_header, ) ) try: span_id = AwsXRayPropagator._parse_span_id(value) except ValueError as ex: raise AwsParseTraceHeaderError( ( "Invalid TraceId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", TRACE_HEADER_KEY, trace_header, ) ) from ex elif key == SAMPLED_FLAG_KEY: if not AwsXRayPropagator._validate_sampled_flag(value): raise AwsParseTraceHeaderError( ( "Invalid Sampling flag in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", TRACE_HEADER_KEY, trace_header, ) ) sampled = AwsXRayPropagator._parse_sampled_flag(value) return trace_id, span_id, sampled @staticmethod def _validate_trace_id(trace_id_str): return ( len(trace_id_str) == TRACE_ID_LENGTH and trace_id_str.startswith(TRACE_ID_VERSION) and trace_id_str[TRACE_ID_DELIMITER_INDEX_1] == TRACE_ID_DELIMITER and trace_id_str[TRACE_ID_DELIMITER_INDEX_2] == TRACE_ID_DELIMITER ) @staticmethod def _parse_trace_id(trace_id_str): timestamp_subset = trace_id_str[TRACE_ID_DELIMITER_INDEX_1 + 1 : TRACE_ID_DELIMITER_INDEX_2] unique_id_subset = trace_id_str[TRACE_ID_DELIMITER_INDEX_2 + 1 : TRACE_ID_LENGTH] return int(timestamp_subset + unique_id_subset, 16) @staticmethod def _validate_span_id(span_id_str): return len(span_id_str) == PARENT_ID_LENGTH @staticmethod def _parse_span_id(span_id_str): return int(span_id_str, 16) @staticmethod def _validate_sampled_flag(sampled_flag_str): return len(sampled_flag_str) == SAMPLED_FLAG_LENGTH and sampled_flag_str in ( IS_SAMPLED, NOT_SAMPLED, ) @staticmethod def _parse_sampled_flag(sampled_flag_str): return sampled_flag_str[0] == IS_SAMPLED def inject( self, carrier: CarrierT, context: Context | None = None, setter: Setter[CarrierT] = default_setter, ) -> None: span = trace.get_current_span(context=context) span_context = span.get_span_context() if not span_context.is_valid: return otel_trace_id = f"{span_context.trace_id:032x}" xray_trace_id = TRACE_ID_DELIMITER.join( [ TRACE_ID_VERSION, otel_trace_id[:TRACE_ID_FIRST_PART_LENGTH], otel_trace_id[TRACE_ID_FIRST_PART_LENGTH:], ] ) parent_id = f"{span_context.span_id:016x}" sampling_flag = IS_SAMPLED if span_context.trace_flags & trace.TraceFlags.SAMPLED else NOT_SAMPLED # TODO: Add OT trace state to the X-Ray trace header trace_header = KV_PAIR_DELIMITER.join( [ KEY_AND_VALUE_DELIMITER.join([key, value]) for key, value in [ (TRACE_ID_KEY, xray_trace_id), (PARENT_ID_KEY, parent_id), (SAMPLED_FLAG_KEY, sampling_flag), ] ] ) setter.set( carrier, TRACE_HEADER_KEY, trace_header, ) @property def fields(self): """Returns a set with the fields set in `inject`.""" return {TRACE_HEADER_KEY} class AwsXRayLambdaPropagator(AwsXRayPropagator): """Implementation of the AWS X-Ray Trace Header propagation protocol but with special handling for Lambda's ``_X_AMZN_TRACE_ID`` environment variable. """ def extract( self, carrier: CarrierT, context: Context | None = None, getter: Getter[CarrierT] = default_getter, ) -> Context: xray_context = super().extract(carrier, context=context, getter=getter) if trace.get_current_span(context=context).get_span_context().is_valid: return xray_context trace_header = environ.get(AWS_TRACE_HEADER_ENV_KEY) if trace_header is None: return xray_context return super().extract( {TRACE_HEADER_KEY: trace_header}, context=xray_context, getter=getter, ) version.py000066400000000000000000000001471527070072300413440ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/src/opentelemetry/propagators/aws# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "1.1.0.dev" test-requirements-0.txt000066400000000000000000000007421527070072300350660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xrayasgiref==3.8.1 certifi==2024.7.4 charset-normalizer==3.3.2 Deprecated==1.2.14 idna==3.7 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 pytest-benchmark==4.0.0 requests==2.33.1 tomli==2.0.1 typing_extensions==4.12.2 urllib3==2.7.0 wrapt==1.16.0 zipp==3.19.2 opentelemetry-api==1.16 # when updating, also update in pyproject.toml opentelemetry-sdk==1.16 # when updating, also update in pyproject.toml -e propagator/opentelemetry-propagator-aws-xray test-requirements-1.txt000066400000000000000000000006451527070072300350710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xrayasgiref==3.8.1 certifi==2024.7.4 charset-normalizer==3.3.2 Deprecated==1.2.14 idna==3.7 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 pytest-benchmark==4.0.0 requests==2.33.1 tomli==2.0.1 typing_extensions==4.12.2 urllib3==2.7.0 wrapt==1.16.0 zipp==3.19.2 # test with the latest version of opentelemetry-api, sdk, and semantic conventions -e propagator/opentelemetry-propagator-aws-xray opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/tests/000077500000000000000000000000001527070072300317065ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/tests/__init__.py000066400000000000000000000000001527070072300340050ustar00rootroot00000000000000test_aws_xray_lambda_propagator.py000066400000000000000000000104521527070072300406350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from os import environ from unittest import TestCase from unittest.mock import patch from requests.structures import CaseInsensitiveDict from opentelemetry.context import get_current from opentelemetry.propagators.aws.aws_xray_propagator import ( TRACE_HEADER_KEY, AwsXRayLambdaPropagator, ) from opentelemetry.propagators.textmap import DefaultGetter from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.trace import ( Link, NonRecordingSpan, SpanContext, TraceState, get_current_span, use_span, ) from opentelemetry.util._importlib_metadata import entry_points class AwsXRayLambdaPropagatorTest(TestCase): def test_extract_no_environment_variable(self): actual_context = get_current_span( AwsXRayLambdaPropagator().extract({}, context=get_current(), getter=DefaultGetter()) ).get_span_context() self.assertEqual(hex(actual_context.trace_id), "0x0") self.assertEqual(hex(actual_context.span_id), "0x0") self.assertFalse( actual_context.trace_flags.sampled, ) self.assertEqual(actual_context.trace_state, TraceState.get_default()) def test_extract_no_environment_variable_valid_context(self): with use_span(NonRecordingSpan(SpanContext(1, 2, False))): actual_context = get_current_span( AwsXRayLambdaPropagator().extract({}, context=get_current(), getter=DefaultGetter()) ).get_span_context() self.assertEqual(hex(actual_context.trace_id), "0x1") self.assertEqual(hex(actual_context.span_id), "0x2") self.assertFalse( actual_context.trace_flags.sampled, ) self.assertEqual(actual_context.trace_state, TraceState.get_default()) @patch.dict( environ, {"_X_AMZN_TRACE_ID": ("Root=1-00000001-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1;Foo=Bar")}, ) def test_extract_from_environment_variable(self): actual_context = get_current_span( AwsXRayLambdaPropagator().extract({}, context=get_current(), getter=DefaultGetter()) ).get_span_context() self.assertEqual(hex(actual_context.trace_id), "0x1d188f8fa79d48a391a778fa6") self.assertEqual(hex(actual_context.span_id), "0x53995c3f42cd8ad8") self.assertTrue( actual_context.trace_flags.sampled, ) self.assertEqual(actual_context.trace_state, TraceState.get_default()) @patch.dict( environ, {"_X_AMZN_TRACE_ID": ("Root=1-00000002-240000000000000000000002;Parent=1600000000000002;Sampled=1;Foo=Bar")}, ) def test_add_link_from_environment_variable(self): propagator = AwsXRayLambdaPropagator() default_getter = DefaultGetter() carrier = CaseInsensitiveDict( {TRACE_HEADER_KEY: ("Root=1-00000001-240000000000000000000001;Parent=1600000000000001;Sampled=1")} ) extracted_context = propagator.extract(carrier, context=get_current(), getter=default_getter) link_context = propagator.extract(carrier, context=extracted_context, getter=default_getter) span = ReadableSpan("test", parent=extracted_context, links=[Link(link_context)]) span_parent_context = get_current_span(span.parent).get_span_context() self.assertEqual(hex(span_parent_context.trace_id), "0x2240000000000000000000002") self.assertEqual(hex(span_parent_context.span_id), "0x1600000000000002") self.assertTrue( span_parent_context.trace_flags.sampled, ) self.assertEqual(span_parent_context.trace_state, TraceState.get_default()) span_link_context = get_current_span(span.links[0].context).get_span_context() self.assertEqual(hex(span_link_context.trace_id), "0x1240000000000000000000001") self.assertEqual(hex(span_link_context.span_id), "0x1600000000000001") self.assertTrue( span_link_context.trace_flags.sampled, ) self.assertEqual(span_link_context.trace_state, TraceState.get_default()) def test_load_entry_point(self): self.assertIs( next(iter(entry_points(group="opentelemetry_propagator", name="xray-lambda"))).load(), AwsXRayLambdaPropagator, ) test_aws_xray_propagator.py000066400000000000000000000273301527070072300373400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-aws-xray/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from unittest.mock import Mock, patch from requests.structures import CaseInsensitiveDict import opentelemetry.trace as trace_api from opentelemetry.context import Context from opentelemetry.propagators.aws.aws_xray_propagator import ( TRACE_HEADER_KEY, AwsXRayPropagator, ) from opentelemetry.trace import ( DEFAULT_TRACE_OPTIONS, DEFAULT_TRACE_STATE, INVALID_SPAN_CONTEXT, SpanContext, TraceFlags, TraceState, set_span_in_context, ) TRACE_ID_BASE16 = "8a3c60f7d188f8fa79d48a391a778fa6" SPAN_ID_BASE16 = "53995c3f42cd8ad8" # Propagators Usage Methods def get_as_list(dict_object, key): value = dict_object.get(key) return [value] if value is not None else [] # Inject Methods def build_test_current_context( trace_id=int(TRACE_ID_BASE16, 16), span_id=int(SPAN_ID_BASE16, 16), is_remote=True, trace_flags=DEFAULT_TRACE_OPTIONS, trace_state=DEFAULT_TRACE_STATE, ): return set_span_in_context( trace_api.NonRecordingSpan(build_test_span_context(trace_id, span_id, is_remote, trace_flags, trace_state)) ) # Extract Methods def get_nested_span_context(parent_context): return trace_api.get_current_span(parent_context).get_span_context() # Helper Methods def build_test_span_context( trace_id=int(TRACE_ID_BASE16, 16), span_id=int(SPAN_ID_BASE16, 16), is_remote=True, trace_flags=DEFAULT_TRACE_OPTIONS, trace_state=DEFAULT_TRACE_STATE, ): return SpanContext( trace_id, span_id, is_remote, trace_flags, trace_state, ) class AwsXRayPropagatorTest(unittest.TestCase): XRAY_PROPAGATOR = AwsXRayPropagator() # Inject Tests def test_inject_into_non_sampled_context(self): carrier = CaseInsensitiveDict() AwsXRayPropagatorTest.XRAY_PROPAGATOR.inject( carrier, build_test_current_context(), ) injected_items = set(carrier.items()) expected_items = set( CaseInsensitiveDict( {TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0"} ).items() ) self.assertEqual(injected_items, expected_items) def test_inject_into_sampled_context(self): carrier = CaseInsensitiveDict() AwsXRayPropagatorTest.XRAY_PROPAGATOR.inject( carrier, build_test_current_context(trace_flags=TraceFlags(TraceFlags.SAMPLED)), ) injected_items = set(carrier.items()) expected_items = set( CaseInsensitiveDict( {TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1"} ).items() ) self.assertEqual(injected_items, expected_items) def test_inject_into_context_with_non_default_state(self): carrier = CaseInsensitiveDict() AwsXRayPropagatorTest.XRAY_PROPAGATOR.inject( carrier, build_test_current_context(trace_state=TraceState([("foo", "bar")])), ) # TODO: (NathanielRN) Assert trace state when the propagator supports it injected_items = set(carrier.items()) expected_items = set( CaseInsensitiveDict( {TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0"} ).items() ) self.assertEqual(injected_items, expected_items) def test_inject_reported_fields_matches_carrier_fields(self): carrier = CaseInsensitiveDict() AwsXRayPropagatorTest.XRAY_PROPAGATOR.inject( carrier, build_test_current_context(), ) injected_keys = set(carrier.keys()) self.assertEqual(injected_keys, AwsXRayPropagatorTest.XRAY_PROPAGATOR.fields) # Extract Tests def test_extract_empty_carrier_to_explicit_ctx(self): orig_ctx = Context({"k1": "v1"}) context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract(CaseInsensitiveDict(), orig_ctx) self.assertDictEqual(orig_ctx, context_with_extracted) def test_extract_empty_carrier_to_implicit_ctx(self): context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract(CaseInsensitiveDict()) self.assertDictEqual(Context(), context_with_extracted) def test_extract_not_sampled_context(self): context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( CaseInsensitiveDict( {TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0"} ), ) self.assertEqual( get_nested_span_context(context_with_extracted), build_test_span_context(), ) def test_extract_sampled_context(self): context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( CaseInsensitiveDict( {TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1"} ), ) self.assertEqual( get_nested_span_context(context_with_extracted), build_test_span_context(trace_flags=TraceFlags(TraceFlags.SAMPLED)), ) def test_extract_different_order(self): context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( CaseInsensitiveDict( {TRACE_HEADER_KEY: "Sampled=0;Parent=53995c3f42cd8ad8;Root=1-8a3c60f7-d188f8fa79d48a391a778fa6"} ), ) self.assertEqual( get_nested_span_context(context_with_extracted), build_test_span_context(), ) def test_extract_with_additional_fields(self): context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( CaseInsensitiveDict( {TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0;Foo=Bar"} ), ) self.assertEqual( get_nested_span_context(context_with_extracted), build_test_span_context(), ) def test_extract_with_extra_whitespace(self): context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( CaseInsensitiveDict( { TRACE_HEADER_KEY: " Root = 1-8a3c60f7-d188f8fa79d48a391a778fa6 ; Parent = 53995c3f42cd8ad8 ; Sampled = 0 " } ), ) self.assertEqual( get_nested_span_context(context_with_extracted), build_test_span_context(), ) def test_extract_invalid_xray_trace_header(self): context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( CaseInsensitiveDict({TRACE_HEADER_KEY: ""}), ) self.assertEqual( get_nested_span_context(context_with_extracted), INVALID_SPAN_CONTEXT, ) def test_extract_invalid_to_explicit_ctx(self): trace_headers = [ "Root=1-12345678-abcdefghijklmnopqrstuvwx;Parent=53995c3f42cd8ad8;Sampled=0", # invalid trace id "Root=1-8a3c60f7-d188f8fa79d48a391a778fa600;Parent=53995c3f42cd8ad8;Sampled=0", # invalid size trace id "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=abcdefghijklmnop;Sampled=0", # invalid span id "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad800;Sampled=0" # invalid size span id "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=", # no sampled flag "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=011", # invalid size sampled "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=a", # non numeric sampled flag ] for trace_header in trace_headers: with self.subTest(trace_header=trace_header): orig_ctx = Context({"k1": "v1"}) ctx = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( CaseInsensitiveDict({TRACE_HEADER_KEY: trace_header}), orig_ctx, ) self.assertDictEqual(orig_ctx, ctx) def test_extract_invalid_to_implicit_ctx(self): trace_headers = [ "Root=1-12345678-abcdefghijklmnopqrstuvwx;Parent=53995c3f42cd8ad8;Sampled=0", # invalid trace id "Root=1-8a3c60f7-d188f8fa79d48a391a778fa600;Parent=53995c3f42cd8ad8;Sampled=0", # invalid size trace id "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=abcdefghijklmnop;Sampled=0", # invalid span id "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad800;Sampled=0" # invalid size span id "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=", # no sampled flag "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=011", # invalid size sampled "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=a", # non numeric sampled flag ] for trace_header in trace_headers: with self.subTest(trace_header=trace_header): ctx = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( CaseInsensitiveDict({TRACE_HEADER_KEY: trace_header}), ) self.assertDictEqual(Context(), ctx) @patch("opentelemetry.propagators.aws.aws_xray_propagator.trace") def test_fields(self, mock_trace): """Make sure the fields attribute returns the fields used in inject""" mock_trace.configure_mock( **{ "get_current_span.return_value": Mock( **{"get_span_context.return_value": Mock(is_valid=True, trace_id=1, span_id=1)} ) } ) mock_setter = Mock() AwsXRayPropagatorTest.XRAY_PROPAGATOR.inject({}, setter=mock_setter) inject_fields = set() for call in mock_setter.mock_calls: inject_fields.add(call[1][1]) self.assertEqual(AwsXRayPropagatorTest.XRAY_PROPAGATOR.fields, inject_fields) def test_extract_trace_state_from_context(self): """Test that extract properly propagates the trace state extracted by other propagators.""" context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( CaseInsensitiveDict( { TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0", } ), context=set_span_in_context( trace_api.NonRecordingSpan( SpanContext( int(TRACE_ID_BASE16, 16), int(SPAN_ID_BASE16, 16), True, DEFAULT_TRACE_OPTIONS, TraceState([("foo", "bar"), ("baz", "qux")]), ) ) ), ) extracted_span_context = get_nested_span_context(context_with_extracted) expected_trace_state = TraceState([("foo", "bar"), ("baz", "qux")]) self.assertEqual(extracted_span_context.trace_state, expected_trace_state) def test_extract_no_trace_state_from_context(self): """Test that extract defaults to an empty trace state correctly.""" context_with_extracted = AwsXRayPropagatorTest.XRAY_PROPAGATOR.extract( CaseInsensitiveDict( { TRACE_HEADER_KEY: "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=0", } ) ) extracted_span_context = get_nested_span_context(context_with_extracted) self.assertEqual(extracted_span_context.trace_state, TraceState([])) opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-ot-trace/000077500000000000000000000000001527070072300305075ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-ot-trace/README.rst000066400000000000000000000047471527070072300322120ustar00rootroot00000000000000OpenTelemetry OT Trace Propagator ================================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-propagator-ot-trace.svg :target: https://pypi.org/project/opentelemetry-propagator-ot-trace/ Installation ------------ :: pip install opentelemetry-propagator-ot-trace .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ OTTrace Format -------------- So far there is no "formal" specification of the OTTrace format. The best document that servers this purpose that exists now is this_ implementation. .. _this: https://github.com/opentracing/basictracer-python/blob/master/basictracer/text_propagator.py ===================== ======================================================================================================================================= ===================== Header Name Description Required ===================== ======================================================================================================================================= ===================== ``ot-tracer-traceid`` uint64 encoded as a string of 16 hex characters yes ``ot-tracer-spanid`` uint64 encoded as a string of 16 hex characters yes ``ot-tracer-sampled`` boolean encoded as a string with the values ``true`` or ``false`` no ``ot-baggage-*`` repeated string to string key-value baggage items; keys are prefixed with ``ot-baggage-`` and the corresponding value is the raw string if baggage is present ===================== ======================================================================================================================================= ===================== Interop and trace ids --------------------- The OT Trace propagation format expects trace ids to be 64-bits. In order to interop with OpenTelemetry, trace ids need to be truncated to 64-bits before sending them on the wire. When truncating, the least significant (right-most) bits MUST be retained. For example, a trace id of ``3c3039f4d78d5c02ee8e3e41b17ce105`` would be truncated to ``ee8e3e41b17ce105``. References ---------- * `OpenTelemetry Project `_ opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-ot-trace/pyproject.toml000066400000000000000000000026731527070072300334330ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-propagator-ot-trace" dynamic = ["version"] description = "OT Trace Propagator for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-sdk ~= 1.12", ] [project.entry-points.opentelemetry_propagator] ottrace = "opentelemetry.propagators.ot_trace:OTTracePropagator" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/propagator/opentelemetry-propagator-ot-trace" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/propagators/ot_trace/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-ot-trace/src/000077500000000000000000000000001527070072300312765ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-ot-trace/src/opentelemetry/000077500000000000000000000000001527070072300341725ustar00rootroot00000000000000propagators/000077500000000000000000000000001527070072300364545ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-ot-trace/src/opentelemetryot_trace/000077500000000000000000000000001527070072300402545ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-ot-trace/src/opentelemetry/propagators__init__.py000066400000000000000000000161041527070072300423670ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-ot-trace/src/opentelemetry/propagators/ot_trace# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Iterable from logging import getLogger from re import compile as re_compile from typing import Any from opentelemetry.baggage import get_all, set_baggage from opentelemetry.context import Context from opentelemetry.propagators.textmap import ( CarrierT, Getter, Setter, TextMapPropagator, default_getter, default_setter, ) from opentelemetry.trace import ( INVALID_SPAN_ID, INVALID_TRACE_ID, NonRecordingSpan, SpanContext, TraceFlags, get_current_span, set_span_in_context, ) _logger = getLogger(__name__) OT_TRACE_ID_HEADER = "ot-tracer-traceid" OT_SPAN_ID_HEADER = "ot-tracer-spanid" OT_SAMPLED_HEADER = "ot-tracer-sampled" OT_BAGGAGE_PREFIX = "ot-baggage-" # https://www.w3.org/TR/baggage/#limits _MAX_BAGGAGE_ENTRIES = 180 _MAX_BAGGAGE_BYTES_PER_ENTRY = 4096 _MAX_BAGGAGE_TOTAL_BYTES = 8192 _valid_header_name = re_compile(r"[\w_^`!#$%&'*+.|~]+") _valid_header_value = re_compile(r"[\t\x20-\x7e\x80-\xff]+") _valid_extract_traceid = re_compile(r"[0-9a-f]{1,32}") _valid_extract_spanid = re_compile(r"[0-9a-f]{1,16}") class OTTracePropagator(TextMapPropagator): """Propagator for the OTTrace HTTP header format""" def extract( self, carrier: CarrierT, context: Context | None = None, getter: Getter[CarrierT] = default_getter, ) -> Context: if context is None: context = Context() traceid = _extract_identifier( getter.get(carrier, OT_TRACE_ID_HEADER), _valid_extract_traceid, INVALID_TRACE_ID, ) spanid = _extract_identifier( getter.get(carrier, OT_SPAN_ID_HEADER), _valid_extract_spanid, INVALID_SPAN_ID, ) if _extract_first_element(getter.get(carrier, OT_SAMPLED_HEADER)) == "true": traceflags = TraceFlags.SAMPLED else: traceflags = TraceFlags.DEFAULT if traceid != INVALID_TRACE_ID and spanid != INVALID_SPAN_ID: context = set_span_in_context( NonRecordingSpan( SpanContext( trace_id=traceid, span_id=spanid, is_remote=True, trace_flags=TraceFlags(traceflags), ) ), context, ) baggage = dict(get_all(context)) or {} # Use len() as a fast byte approximation to avoid encoding overhead. total_bytes = sum(len(k) + len(v) for k, v in baggage.items()) num_baggage_entries = len(baggage) for key in getter.keys(carrier): if not key.startswith(OT_BAGGAGE_PREFIX): continue value = _extract_first_element(getter.get(carrier, key)) if value is None: continue # Count every ot-baggage-* entry towards the cap *before* # the per-entry byte check, so a flood of oversized entries # cannot keep the loop running past the cap. if num_baggage_entries >= _MAX_BAGGAGE_ENTRIES: _logger.warning("ot-baggage exceeded the maximum number of list-members") break num_baggage_entries += 1 baggage_key = key[len(OT_BAGGAGE_PREFIX) :] entry_bytes = len(baggage_key) + len(value) if entry_bytes > _MAX_BAGGAGE_BYTES_PER_ENTRY: _logger.warning( "ot-baggage entry with key `%s` exceeded the maximum number of bytes per list-member", baggage_key, ) continue delta_bytes = entry_bytes - ( len(baggage_key) + len(baggage[baggage_key]) if baggage_key in baggage else 0 ) if total_bytes + delta_bytes > _MAX_BAGGAGE_TOTAL_BYTES: _logger.warning("ot-baggage exceeded the maximum number of total bytes") break total_bytes += delta_bytes baggage[baggage_key] = value for key, value in baggage.items(): context = set_baggage(key, value, context) return context def inject( self, carrier: CarrierT, context: Context | None = None, setter: Setter[CarrierT] = default_setter, ) -> None: span_context = get_current_span(context).get_span_context() if span_context.trace_id == INVALID_TRACE_ID: return setter.set(carrier, OT_TRACE_ID_HEADER, hex(span_context.trace_id)[2:][-16:]) setter.set( carrier, OT_SPAN_ID_HEADER, hex(span_context.span_id)[2:][-16:], ) if span_context.trace_flags == TraceFlags.SAMPLED: traceflags = "true" else: traceflags = "false" setter.set(carrier, OT_SAMPLED_HEADER, traceflags) baggage = get_all(context) if not baggage: return total_bytes = 0 num_baggage_entries = 0 for header_name, header_value in baggage.items(): if num_baggage_entries >= _MAX_BAGGAGE_ENTRIES: _logger.warning("ot-baggage exceeded the maximum number of list-members") break num_baggage_entries += 1 entry_bytes = len(header_name) + len(header_value) if entry_bytes > _MAX_BAGGAGE_BYTES_PER_ENTRY: _logger.warning( "ot-baggage entry with key `%s` exceeded the maximum number of bytes per list-member", header_name, ) continue if _valid_header_name.fullmatch(header_name) is None or _valid_header_value.fullmatch(header_value) is None: continue if total_bytes + entry_bytes > _MAX_BAGGAGE_TOTAL_BYTES: _logger.warning("ot-baggage exceeded the maximum number of total bytes") break total_bytes += entry_bytes setter.set( carrier, "".join([OT_BAGGAGE_PREFIX, header_name]), header_value, ) @property def fields(self): """Returns a set with the fields set in `inject`. See `opentelemetry.propagators.textmap.TextMapPropagator.fields` """ return { OT_TRACE_ID_HEADER, OT_SPAN_ID_HEADER, OT_SAMPLED_HEADER, } def _extract_first_element( items: Iterable[CarrierT], default: Any = None, ) -> CarrierT | None: if items is None: return default return next(iter(items), None) def _extract_identifier(items: Iterable[CarrierT], validator_pattern, default: int) -> int: header = _extract_first_element(items) if header is None or validator_pattern.fullmatch(header) is None: return default try: return int(header, 16) except ValueError: return default version.py000066400000000000000000000001441527070072300423120ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-ot-trace/src/opentelemetry/propagators/ot_trace# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000003431527070072300346710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-ot-traceasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e propagator/opentelemetry-propagator-ot-trace opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-ot-trace/tests/000077500000000000000000000000001527070072300316515ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-ot-trace/tests/__init__.py000066400000000000000000000000001527070072300337500ustar00rootroot00000000000000test_ot_trace_propagator.py000066400000000000000000000604711527070072300372510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/propagator/opentelemetry-propagator-ot-trace/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from opentelemetry.baggage import get_all, set_baggage from opentelemetry.context import Context from opentelemetry.propagators.ot_trace import ( _MAX_BAGGAGE_BYTES_PER_ENTRY, _MAX_BAGGAGE_ENTRIES, _MAX_BAGGAGE_TOTAL_BYTES, OT_BAGGAGE_PREFIX, OT_SAMPLED_HEADER, OT_SPAN_ID_HEADER, OT_TRACE_ID_HEADER, OTTracePropagator, ) from opentelemetry.propagators.textmap import Getter from opentelemetry.sdk.trace import _Span from opentelemetry.trace import ( INVALID_TRACE_ID, SpanContext, TraceFlags, set_span_in_context, ) from opentelemetry.trace.propagation import get_current_span # pylint: disable=too-many-public-methods class TestOTTracePropagator(TestCase): ot_trace_propagator = OTTracePropagator() def carrier_inject(self, trace_id, span_id, is_remote, trace_flags): carrier = {} self.ot_trace_propagator.inject( carrier, set_span_in_context( _Span( "child", context=SpanContext( trace_id=trace_id, span_id=span_id, is_remote=is_remote, trace_flags=trace_flags, ), ) ), ) return carrier def test_inject_short_trace_id_short_span_id(self): carrier = self.carrier_inject( int("1", 16), int("2", 16), True, TraceFlags.SAMPLED, ) self.assertEqual(carrier[OT_TRACE_ID_HEADER], "1") self.assertEqual(carrier[OT_SPAN_ID_HEADER], "2") def test_inject_trace_id_span_id_true(self): """Test valid trace_id, span_id and sampled true""" carrier = self.carrier_inject( int("80f198ee56343ba864fe8b2a57d3eff7", 16), int("e457b5a2e4d86bd1", 16), True, TraceFlags.SAMPLED, ) self.assertEqual(carrier[OT_TRACE_ID_HEADER], "64fe8b2a57d3eff7") self.assertEqual(carrier[OT_SPAN_ID_HEADER], "e457b5a2e4d86bd1") self.assertEqual(carrier[OT_SAMPLED_HEADER], "true") def test_inject_trace_id_span_id_false(self): """Test valid trace_id, span_id and sampled true""" carrier = self.carrier_inject( int("80f198ee56343ba864fe8b2a57d3eff7", 16), int("e457b5a2e4d86bd1", 16), False, TraceFlags.DEFAULT, ) self.assertEqual(carrier[OT_TRACE_ID_HEADER], "64fe8b2a57d3eff7") self.assertEqual(carrier[OT_SPAN_ID_HEADER], "e457b5a2e4d86bd1") self.assertEqual(carrier[OT_SAMPLED_HEADER], "false") def test_inject_truncate_traceid(self): """Test that traceid is truncated to 64 bits""" self.assertEqual( self.carrier_inject( int("80f198ee56343ba864fe8b2a57d3eff7", 16), int("e457b5a2e4d86bd1", 16), True, TraceFlags.DEFAULT, )[OT_TRACE_ID_HEADER], "64fe8b2a57d3eff7", ) def test_inject_sampled_true(self): """Test that sampled true trace flags are injected""" self.assertEqual( self.carrier_inject( int("80f198ee56343ba864fe8b2a57d3eff7", 16), int("e457b5a2e4d86bd1", 16), True, TraceFlags.SAMPLED, )[OT_SAMPLED_HEADER], "true", ) def test_inject_sampled_false(self): """Test that sampled false trace flags are injected""" self.assertEqual( self.carrier_inject( int("80f198ee56343ba864fe8b2a57d3eff7", 16), int("e457b5a2e4d86bd1", 16), True, TraceFlags.DEFAULT, )[OT_SAMPLED_HEADER], "false", ) def test_inject_invalid_trace_id(self): """Test that no attributes are injected if the trace_id is invalid""" self.assertEqual( self.carrier_inject( INVALID_TRACE_ID, int("e457b5a2e4d86bd1", 16), True, TraceFlags.SAMPLED, ), {}, ) def test_inject_set_baggage(self): """Test that baggage is set""" carrier = {} self.ot_trace_propagator.inject( carrier, set_baggage( "key", "value", context=set_span_in_context( _Span( "child", SpanContext( trace_id=int("80f198ee56343ba864fe8b2a57d3eff7", 16), span_id=int("e457b5a2e4d86bd1", 16), is_remote=True, trace_flags=TraceFlags.SAMPLED, ), ) ), ), ) self.assertEqual(carrier["".join([OT_BAGGAGE_PREFIX, "key"])], "value") def test_inject_invalid_baggage_keys(self): """Test that invalid baggage keys are not set""" carrier = {} self.ot_trace_propagator.inject( carrier, set_baggage( "(", "value", context=set_span_in_context( _Span( "child", SpanContext( trace_id=int("80f198ee56343ba864fe8b2a57d3eff7", 16), span_id=int("e457b5a2e4d86bd1", 16), is_remote=True, trace_flags=TraceFlags.SAMPLED, ), ) ), ), ) self.assertNotIn("".join([OT_BAGGAGE_PREFIX, "!"]), carrier.keys()) def test_inject_invalid_baggage_values(self): """Test that invalid baggage values are not set""" carrier = {} self.ot_trace_propagator.inject( carrier, set_baggage( "key", "α", context=set_span_in_context( _Span( "child", SpanContext( trace_id=int("80f198ee56343ba864fe8b2a57d3eff7", 16), span_id=int("e457b5a2e4d86bd1", 16), is_remote=True, trace_flags=TraceFlags.SAMPLED, ), ) ), ), ) self.assertNotIn("".join([OT_BAGGAGE_PREFIX, "key"]), carrier.keys()) def test_inject_baggage_count_capped_at_max(self): """Number of ot-baggage-* entries injected must be capped, so oversized contexts are not pushed to downstream services.""" context = set_span_in_context( _Span( "child", SpanContext( trace_id=int("80f198ee56343ba864fe8b2a57d3eff7", 16), span_id=int("e457b5a2e4d86bd1", 16), is_remote=True, trace_flags=TraceFlags.SAMPLED, ), ) ) for idx in range(_MAX_BAGGAGE_ENTRIES + 50): context = set_baggage(f"k{idx}", f"v{idx}", context=context) carrier = {} with self.assertLogs("opentelemetry.propagators.ot_trace", level="WARNING") as cm: self.ot_trace_propagator.inject(carrier, context) injected = [k for k in carrier if k.startswith(OT_BAGGAGE_PREFIX)] self.assertEqual(len(injected), _MAX_BAGGAGE_ENTRIES) self.assertTrue(any("maximum number of list-members" in m for m in cm.output)) def test_inject_baggage_per_entry_byte_cap(self): """Per-entry byte length (key + value) must be capped on inject.""" context = set_span_in_context( _Span( "child", SpanContext( trace_id=int("80f198ee56343ba864fe8b2a57d3eff7", 16), span_id=int("e457b5a2e4d86bd1", 16), is_remote=True, trace_flags=TraceFlags.SAMPLED, ), ) ) context = set_baggage("ok", "fits", context=context) context = set_baggage("big", "a" * (_MAX_BAGGAGE_BYTES_PER_ENTRY + 1), context=context) carrier = {} with self.assertLogs("opentelemetry.propagators.ot_trace", level="WARNING") as cm: self.ot_trace_propagator.inject(carrier, context) self.assertIn("".join([OT_BAGGAGE_PREFIX, "ok"]), carrier) self.assertNotIn("".join([OT_BAGGAGE_PREFIX, "big"]), carrier) self.assertTrue(any("bytes per list-member" in m for m in cm.output)) def test_inject_baggage_total_byte_cap(self): """Cumulative byte length across all entries must be capped on inject.""" context = set_span_in_context( _Span( "child", SpanContext( trace_id=int("80f198ee56343ba864fe8b2a57d3eff7", 16), span_id=int("e457b5a2e4d86bd1", 16), is_remote=True, trace_flags=TraceFlags.SAMPLED, ), ) ) chunk_val = "a" * (_MAX_BAGGAGE_TOTAL_BYTES // 3) context = set_baggage("k1", chunk_val, context=context) context = set_baggage("k2", chunk_val, context=context) context = set_baggage("k3", chunk_val, context=context) carrier = {} with self.assertLogs("opentelemetry.propagators.ot_trace", level="WARNING") as cm: self.ot_trace_propagator.inject(carrier, context) injected = [k for k in carrier if k.startswith(OT_BAGGAGE_PREFIX)] self.assertEqual(len(injected), 2) self.assertIn("".join([OT_BAGGAGE_PREFIX, "k1"]), carrier) self.assertIn("".join([OT_BAGGAGE_PREFIX, "k2"]), carrier) self.assertNotIn("".join([OT_BAGGAGE_PREFIX, "k3"]), carrier) self.assertTrue(any("maximum number of total bytes" in m for m in cm.output)) def test_inject_baggage_oversized_flood_is_bounded(self): """A flood of oversized entries after the cap must not keep the inject loop iterating: oversized entries count toward the cap, so the loop stops after _MAX_BAGGAGE_ENTRIES entries are processed.""" context = set_span_in_context( _Span( "child", SpanContext( trace_id=int("80f198ee56343ba864fe8b2a57d3eff7", 16), span_id=int("e457b5a2e4d86bd1", 16), is_remote=True, trace_flags=TraceFlags.SAMPLED, ), ) ) oversized_value = "a" * (_MAX_BAGGAGE_BYTES_PER_ENTRY + 1) for idx in range(_MAX_BAGGAGE_ENTRIES + 50): context = set_baggage(f"big{idx}", oversized_value, context=context) carrier = {} with self.assertLogs("opentelemetry.propagators.ot_trace", level="WARNING") as cm: self.ot_trace_propagator.inject(carrier, context) injected = [k for k in carrier if k.startswith(OT_BAGGAGE_PREFIX)] self.assertEqual(len(injected), 0) # _MAX_BAGGAGE_ENTRIES per-entry warnings + 1 count-exceeded warning self.assertEqual(len(cm.output), _MAX_BAGGAGE_ENTRIES + 1) self.assertTrue(any("maximum number of list-members" in m for m in cm.output)) def test_inject_baggage_invalid_flood_is_bounded(self): """A flood of invalid entries after the cap must not keep the inject loop iterating: invalid entries count toward the cap, so the loop stops after _MAX_BAGGAGE_ENTRIES entries are processed.""" context = set_span_in_context( _Span( "child", SpanContext( trace_id=int("80f198ee56343ba864fe8b2a57d3eff7", 16), span_id=int("e457b5a2e4d86bd1", 16), is_remote=True, trace_flags=TraceFlags.SAMPLED, ), ) ) for idx in range(_MAX_BAGGAGE_ENTRIES + 50): # Invalid header name containing illegal characters (space) context = set_baggage(f"bad key {idx}", f"val{idx}", context=context) carrier = {} with self.assertLogs("opentelemetry.propagators.ot_trace", level="WARNING") as cm: self.ot_trace_propagator.inject(carrier, context) injected = [k for k in carrier if k.startswith(OT_BAGGAGE_PREFIX)] self.assertEqual(len(injected), 0) self.assertEqual(len(cm.output), 1) self.assertTrue(any("maximum number of list-members" in m for m in cm.output)) def test_extract_trace_id_span_id_sampled_true(self): """Test valid trace_id, span_id and sampled true""" span_context = get_current_span( self.ot_trace_propagator.extract( { OT_TRACE_ID_HEADER: "80f198ee56343ba864fe8b2a57d3eff7", OT_SPAN_ID_HEADER: "e457b5a2e4d86bd1", OT_SAMPLED_HEADER: "true", }, ) ).get_span_context() self.assertEqual(hex(span_context.trace_id)[2:], "80f198ee56343ba864fe8b2a57d3eff7") self.assertEqual(hex(span_context.span_id)[2:], "e457b5a2e4d86bd1") self.assertTrue(span_context.is_remote) self.assertEqual(span_context.trace_flags, TraceFlags.SAMPLED) self.assertIsInstance(get_current_span().get_span_context().trace_flags, TraceFlags) def test_extract_trace_id_span_id_sampled_false(self): """Test valid trace_id, span_id and sampled false""" span_context = get_current_span( self.ot_trace_propagator.extract( { OT_TRACE_ID_HEADER: "80f198ee56343ba864fe8b2a57d3eff7", OT_SPAN_ID_HEADER: "e457b5a2e4d86bd1", OT_SAMPLED_HEADER: "false", }, ) ).get_span_context() self.assertEqual(hex(span_context.trace_id)[2:], "80f198ee56343ba864fe8b2a57d3eff7") self.assertEqual(hex(span_context.span_id)[2:], "e457b5a2e4d86bd1") self.assertTrue(span_context.is_remote) self.assertEqual(span_context.trace_flags, TraceFlags.DEFAULT) self.assertIsInstance(get_current_span().get_span_context().trace_flags, TraceFlags) def test_extract_invalid_trace_header_to_explict_ctx(self): invalid_headers = [ ("abc123!", "e457b5a2e4d86bd1"), # malformed trace id ("64fe8b2a57d3eff7", "abc123!"), # malformed span id ("0" * 32, "e457b5a2e4d86bd1"), # invalid trace id ("64fe8b2a57d3eff7", "0" * 16), # invalid span id ] for trace_id, span_id in invalid_headers: with self.subTest(trace_id=trace_id, span_id=span_id): orig_ctx = Context({"k1": "v1"}) ctx = self.ot_trace_propagator.extract( { OT_TRACE_ID_HEADER: trace_id, OT_SPAN_ID_HEADER: span_id, OT_SAMPLED_HEADER: "false", }, orig_ctx, ) self.assertDictEqual(orig_ctx, ctx) def test_extract_invalid_trace_header_to_implicit_ctx(self): invalid_headers = [ ("abc123!", "e457b5a2e4d86bd1"), # malformed trace id ("64fe8b2a57d3eff7", "abc123!"), # malformed span id ("0" * 32, "e457b5a2e4d86bd1"), # invalid trace id ("64fe8b2a57d3eff7", "0" * 16), # invalid span id ] for trace_id, span_id in invalid_headers: with self.subTest(trace_id=trace_id, span_id=span_id): ctx = self.ot_trace_propagator.extract( { OT_TRACE_ID_HEADER: trace_id, OT_SPAN_ID_HEADER: span_id, OT_SAMPLED_HEADER: "false", } ) self.assertDictEqual(Context(), ctx) def test_extract_baggage(self): """Test baggage extraction""" context = self.ot_trace_propagator.extract( { OT_TRACE_ID_HEADER: "64fe8b2a57d3eff7", OT_SPAN_ID_HEADER: "e457b5a2e4d86bd1", OT_SAMPLED_HEADER: "false", "".join([OT_BAGGAGE_PREFIX, "abc"]): "abc", "".join([OT_BAGGAGE_PREFIX, "def"]): "def", }, ) span_context = get_current_span(context).get_span_context() self.assertEqual(hex(span_context.trace_id)[2:], "64fe8b2a57d3eff7") self.assertEqual(hex(span_context.span_id)[2:], "e457b5a2e4d86bd1") self.assertTrue(span_context.is_remote) self.assertEqual(span_context.trace_flags, TraceFlags.DEFAULT) baggage = get_all(context) self.assertEqual(baggage["abc"], "abc") self.assertEqual(baggage["def"], "def") def test_extract_empty_to_explicit_ctx(self): """Test extraction when no headers are present""" orig_ctx = Context({"k1": "v1"}) ctx = self.ot_trace_propagator.extract({}, orig_ctx) self.assertDictEqual(orig_ctx, ctx) def test_extract_empty_to_implicit_ctx(self): ctx = self.ot_trace_propagator.extract({}) self.assertDictEqual(Context(), ctx) def test_extract_baggage_count_capped_at_max(self): """Number of ot-baggage-* entries recorded must be capped. Mirrors W3CBaggagePropagator and OTel SDK baggage limits (180 list-members). """ carrier = { OT_TRACE_ID_HEADER: "64fe8b2a57d3eff7", OT_SPAN_ID_HEADER: "e457b5a2e4d86bd1", OT_SAMPLED_HEADER: "false", } # Construct (cap + 50) ot-baggage-* entries. for idx in range(_MAX_BAGGAGE_ENTRIES + 50): carrier["".join([OT_BAGGAGE_PREFIX, f"k{idx}"])] = f"v{idx}" with self.assertLogs("opentelemetry.propagators.ot_trace", level="WARNING") as cm: context = self.ot_trace_propagator.extract(carrier) baggage = get_all(context) self.assertEqual(len(baggage), _MAX_BAGGAGE_ENTRIES) self.assertTrue(any("exceeded the maximum number" in m for m in cm.output)) def test_extract_baggage_per_entry_byte_cap(self): """Per-entry byte length (key + value) must be capped.""" carrier = { OT_TRACE_ID_HEADER: "64fe8b2a57d3eff7", OT_SPAN_ID_HEADER: "e457b5a2e4d86bd1", OT_SAMPLED_HEADER: "false", "".join([OT_BAGGAGE_PREFIX, "ok"]): "fits", "".join([OT_BAGGAGE_PREFIX, "big"]): "a" * (_MAX_BAGGAGE_BYTES_PER_ENTRY + 1), # A long key alone (with prefix stripped) also trips the cap. "".join([OT_BAGGAGE_PREFIX, "k" * (_MAX_BAGGAGE_BYTES_PER_ENTRY + 1)]): "v", } with self.assertLogs("opentelemetry.propagators.ot_trace", level="WARNING") as cm: context = self.ot_trace_propagator.extract(carrier) baggage = get_all(context) self.assertIn("ok", baggage) self.assertNotIn("big", baggage) self.assertEqual(len(baggage), 1) self.assertTrue(any("bytes per list-member" in m for m in cm.output)) def test_extract_baggage_total_byte_cap(self): """Cumulative byte length across all entries must be capped on extract.""" chunk_val = "a" * (_MAX_BAGGAGE_TOTAL_BYTES // 3) carrier = { OT_TRACE_ID_HEADER: "64fe8b2a57d3eff7", OT_SPAN_ID_HEADER: "e457b5a2e4d86bd1", OT_SAMPLED_HEADER: "false", "".join([OT_BAGGAGE_PREFIX, "k1"]): chunk_val, "".join([OT_BAGGAGE_PREFIX, "k2"]): chunk_val, "".join([OT_BAGGAGE_PREFIX, "k3"]): chunk_val, } with self.assertLogs("opentelemetry.propagators.ot_trace", level="WARNING") as cm: context = self.ot_trace_propagator.extract(carrier) baggage = get_all(context) self.assertIn("k1", baggage) self.assertIn("k2", baggage) self.assertNotIn("k3", baggage) self.assertEqual(len(baggage), 2) self.assertTrue(any("maximum number of total bytes" in m for m in cm.output)) def test_extract_baggage_existing_context_counts_toward_cap(self): """Pre-existing context baggage items count toward the entry cap.""" context = Context() for idx in range(10): context = set_baggage(f"init{idx}", f"v{idx}", context=context) carrier = { OT_TRACE_ID_HEADER: "64fe8b2a57d3eff7", OT_SPAN_ID_HEADER: "e457b5a2e4d86bd1", OT_SAMPLED_HEADER: "false", } # Offer 180 new entries from carrier for idx in range(_MAX_BAGGAGE_ENTRIES): carrier["".join([OT_BAGGAGE_PREFIX, f"k{idx}"])] = f"v{idx}" with self.assertLogs("opentelemetry.propagators.ot_trace", level="WARNING") as cm: extracted_context = self.ot_trace_propagator.extract(carrier, context=context) baggage = get_all(extracted_context) self.assertEqual(len(baggage), _MAX_BAGGAGE_ENTRIES) for idx in range(10): self.assertIn(f"init{idx}", baggage) self.assertTrue(any("maximum number of list-members" in m for m in cm.output)) def test_extract_baggage_oversized_flood_is_bounded(self): """A flood of oversized entries after the cap must not keep the extract loop iterating: oversized entries count toward the cap, so the loop stops after _MAX_BAGGAGE_ENTRIES entries are processed. Covers the "180 valid + many oversized" attack: without counting oversized entries the loop would walk every header. """ oversized_value = "a" * (_MAX_BAGGAGE_BYTES_PER_ENTRY + 1) flood = 100_000 class CountingGetter(Getter): def __init__(self): self.examined = 0 def get(self, carrier, key): if key.startswith(OT_BAGGAGE_PREFIX): self.examined += 1 return [carrier[key]] return [carrier[key]] if key in carrier else None def keys(self, carrier): return list(carrier.keys()) carrier = { OT_TRACE_ID_HEADER: "64fe8b2a57d3eff7", OT_SPAN_ID_HEADER: "e457b5a2e4d86bd1", OT_SAMPLED_HEADER: "false", } # _MAX_BAGGAGE_ENTRIES valid entries first ... for idx in range(_MAX_BAGGAGE_ENTRIES): carrier["".join([OT_BAGGAGE_PREFIX, f"k{idx}"])] = f"v{idx}" # ... then a flood of oversized entries. for idx in range(flood): carrier["".join([OT_BAGGAGE_PREFIX, f"big{idx}"])] = oversized_value getter = CountingGetter() with self.assertLogs("opentelemetry.propagators.ot_trace", level="WARNING") as cm: context = self.ot_trace_propagator.extract(carrier, getter=getter) baggage = get_all(context) # Only the valid entries are recorded. self.assertEqual(len(baggage), _MAX_BAGGAGE_ENTRIES) # The loop hard-breaks after the cap: at most cap + 1 entries are # ever examined regardless of how many oversized ones follow. self.assertLessEqual(getter.examined, _MAX_BAGGAGE_ENTRIES + 1) self.assertTrue(any("maximum number of list-members" in m for m in cm.output)) def test_extract_baggage_none_value_does_not_count_toward_cap(self): """ot-baggage-* entries with no value are skipped silently and do not count toward the entry cap.""" class NoneGetter(Getter): # Returns None for every ot-baggage-* key, mimicking a carrier # whose getter cannot resolve the header (e.g. drained iterator). def get(self, carrier, key): if key.startswith(OT_BAGGAGE_PREFIX): return None return [carrier[key]] if key in carrier else None def keys(self, carrier): return list(carrier.keys()) carrier = { OT_TRACE_ID_HEADER: "64fe8b2a57d3eff7", OT_SPAN_ID_HEADER: "e457b5a2e4d86bd1", OT_SAMPLED_HEADER: "false", } # 5 ot-baggage-* keys whose values resolve to None. for idx in range(5): carrier["".join([OT_BAGGAGE_PREFIX, f"k{idx}"])] = "ignored" context = self.ot_trace_propagator.extract(carrier, getter=NoneGetter()) baggage = get_all(context) self.assertEqual(baggage, {}) opentelemetry-python-contrib-0.66b2/pyproject.toml000066400000000000000000000310411527070072300224600ustar00rootroot00000000000000# Used for local development, not to be published. [project] name = "opentelemetry-python-contrib" version = "0.0.0" # This is not used. requires-python = ">=3.10" dependencies = [ "opentelemetry-api", "opentelemetry-sdk", "opentelemetry-semantic-conventions", "opentelemetry-instrumentation", "opentelemetry-distro", "opentelemetry-test-utils", "opentelemetry-exporter-prometheus-remote-write", "opentelemetry-exporter-richconsole", "opentelemetry-instrumentation-aio-pika[instruments]", "opentelemetry-instrumentation-aiohttp-client[instruments]", "opentelemetry-instrumentation-aiohttp-server[instruments]", "opentelemetry-instrumentation-aiokafka[instruments]", "opentelemetry-instrumentation-aiopg[instruments]", "opentelemetry-instrumentation-asgi[instruments]", "opentelemetry-instrumentation-asyncio", "opentelemetry-instrumentation-asyncpg[instruments]", "opentelemetry-instrumentation-aws-lambda[instruments]", "opentelemetry-instrumentation-boto3sqs[instruments]", "opentelemetry-instrumentation-botocore[instruments-any]", "opentelemetry-instrumentation-cassandra[instruments]", "opentelemetry-instrumentation-celery[instruments]", "opentelemetry-instrumentation-click[instruments]", "opentelemetry-instrumentation-confluent-kafka[instruments]", "opentelemetry-instrumentation-dbapi", "opentelemetry-instrumentation-django[instruments]", "opentelemetry-instrumentation-falcon[instruments]", "opentelemetry-instrumentation-fastapi[instruments]", "opentelemetry-instrumentation-flask[instruments]", "opentelemetry-instrumentation-grpc[instruments]", "opentelemetry-instrumentation-httpx[instruments-any]", "opentelemetry-instrumentation-jinja2[instruments]", "opentelemetry-instrumentation-kafka-python[instruments]", "opentelemetry-instrumentation-logging", "opentelemetry-instrumentation-mysql[instruments]", "opentelemetry-instrumentation-mysqlclient[instruments]", "opentelemetry-instrumentation-pika[instruments]", "opentelemetry-instrumentation-psycopg[instruments]", "opentelemetry-instrumentation-psycopg2[instruments]", "opentelemetry-instrumentation-psycopg2[instruments-any]", "opentelemetry-instrumentation-pymemcache[instruments]", "opentelemetry-instrumentation-pymongo[instruments]", "opentelemetry-instrumentation-pymysql[instruments]", "opentelemetry-instrumentation-pyramid[instruments]", "opentelemetry-instrumentation-redis[instruments]", "opentelemetry-instrumentation-remoulade[instruments]", "opentelemetry-instrumentation-requests[instruments]", "opentelemetry-instrumentation-sqlalchemy[instruments]", "opentelemetry-instrumentation-sqlite3", "opentelemetry-instrumentation-structlog[instruments]", "opentelemetry-instrumentation-system-metrics", "opentelemetry-instrumentation-threading", "opentelemetry-instrumentation-tornado", "opentelemetry-instrumentation-tortoiseorm[instruments]", "opentelemetry-instrumentation-urllib", "opentelemetry-instrumentation-urllib3[instruments]", "opentelemetry-instrumentation-wsgi", "opentelemetry-propagator-ot-trace", "opentelemetry-propagator-aws-xray", "opentelemetry-util-http", "opentelemetry-util-genai", "opentelemetry-instrumentation-vertexai[instruments]", "opentelemetry-instrumentation-openai-v2[instruments]", ] # https://docs.astral.sh/uv/reference/settings/ [tool.uv] package = false # https://docs.astral.sh/uv/reference/settings/#package required-version = ">=0.12.1" # https://docs.astral.sh/uv/reference/settings/#sources [tool.uv.sources] opentelemetry-api = { git = "https://github.com/open-telemetry/opentelemetry-python", branch = "main", subdirectory = "opentelemetry-api" } opentelemetry-sdk = { git = "https://github.com/open-telemetry/opentelemetry-python", branch = "main", subdirectory = "opentelemetry-sdk" } opentelemetry-semantic-conventions = { git = "https://github.com/open-telemetry/opentelemetry-python", branch = "main", subdirectory = "opentelemetry-semantic-conventions" } opentelemetry-test-utils = { git = "https://github.com/open-telemetry/opentelemetry-python", branch = "main", subdirectory = "tests/opentelemetry-test-utils" } opentelemetry-exporter-otlp = { git = "https://github.com/open-telemetry/opentelemetry-python", branch = "main", subdirectory = "exporter/opentelemetry-exporter-otlp" } opentelemetry-instrumentation = { workspace = true } opentelemetry-distro = { workspace = true } opentelemetry-exporter-prometheus-remote-write = { workspace = true } opentelemetry-exporter-richconsole = { workspace = true } opentelemetry-instrumentation-aio-pika = { workspace = true } opentelemetry-instrumentation-aiohttp-client = { workspace = true } opentelemetry-instrumentation-aiohttp-server = { workspace = true } opentelemetry-instrumentation-aiokafka = { workspace = true } opentelemetry-instrumentation-aiopg = { workspace = true } opentelemetry-instrumentation-asgi = { workspace = true } opentelemetry-instrumentation-asyncio = { workspace = true } opentelemetry-instrumentation-asyncpg = { workspace = true } opentelemetry-instrumentation-aws-lambda = { workspace = true } opentelemetry-instrumentation-boto3sqs = { workspace = true } opentelemetry-instrumentation-botocore = { workspace = true } opentelemetry-instrumentation-cassandra = { workspace = true } opentelemetry-instrumentation-celery = { workspace = true } opentelemetry-instrumentation-click = { workspace = true } opentelemetry-instrumentation-confluent-kafka = { workspace = true } opentelemetry-instrumentation-dbapi = { workspace = true } opentelemetry-instrumentation-django = { workspace = true } opentelemetry-instrumentation-falcon = { workspace = true } opentelemetry-instrumentation-fastapi = { workspace = true } opentelemetry-instrumentation-flask = { workspace = true } opentelemetry-instrumentation-grpc = { workspace = true } opentelemetry-instrumentation-httpx = { workspace = true } opentelemetry-instrumentation-jinja2 = { workspace = true } opentelemetry-instrumentation-kafka-python = { workspace = true } opentelemetry-instrumentation-logging = { workspace = true } opentelemetry-instrumentation-mysql = { workspace = true } opentelemetry-instrumentation-mysqlclient = { workspace = true } opentelemetry-instrumentation-pika = { workspace = true } opentelemetry-instrumentation-psycopg = { workspace = true } opentelemetry-instrumentation-psycopg2 = { workspace = true } opentelemetry-instrumentation-pymemcache = { workspace = true } opentelemetry-instrumentation-pymongo = { workspace = true } opentelemetry-instrumentation-pymysql = { workspace = true } opentelemetry-instrumentation-pyramid = { workspace = true } opentelemetry-instrumentation-redis = { workspace = true } opentelemetry-instrumentation-remoulade = { workspace = true } opentelemetry-instrumentation-requests = { workspace = true } opentelemetry-instrumentation-sqlalchemy = { workspace = true } opentelemetry-instrumentation-sqlite3 = { workspace = true } opentelemetry-instrumentation-structlog = { workspace = true } opentelemetry-instrumentation-system-metrics = { workspace = true } opentelemetry-instrumentation-threading = { workspace = true } opentelemetry-instrumentation-tornado = { workspace = true } opentelemetry-instrumentation-tortoiseorm = { workspace = true } opentelemetry-instrumentation-urllib = { workspace = true } opentelemetry-instrumentation-urllib3 = { workspace = true } opentelemetry-instrumentation-wsgi = { workspace = true } opentelemetry-propagator-ot-trace = { workspace = true } opentelemetry-propagator-aws-xray = { workspace = true } opentelemetry-util-http = { workspace = true } opentelemetry-util-genai = { workspace = true } opentelemetry-instrumentation-vertexai = { workspace = true } opentelemetry-instrumentation-openai-v2 = { workspace = true } # https://docs.astral.sh/uv/reference/settings/#workspace [tool.uv.workspace] members = [ "instrumentation/*", "instrumentation-genai/*", "exporter/*", "opentelemetry-instrumentation", "opentelemetry-distro", "propagator/*", "util/*", ] [tool.ruff] # https://docs.astral.sh/ruff/configuration/ target-version = "py310" line-length = 120 force-exclude = true extend-exclude = ["_template", "*_pb2*.py*"] output-format = "concise" extension = { added = "markdown", changed = "markdown", deprecated = "markdown", removed = "markdown", fixed = "markdown" } [tool.ruff.lint] # https://docs.astral.sh/ruff/linter/#rule-selection # pylint: https://github.com/astral-sh/ruff/issues/970 extend-select = [ "I", # isort "F", # pyflakes "E", # pycodestyle errors "W", # pycodestyle warnings "PLC", # pylint convention "PLE", # pylint error "Q", # flake8-quotes "A", # flake8-builtins "TID", # flake8-tidy-imports ] # Ignore rules from the default ruleset that don't have auto-fixes available and need to be # manually resolved, plus E501 and RUF023 which are intentionally ignored. ignore = [ "ASYNC251", "B008", "B010", "B017", "B018", "B020", "B023", "B039", "BLE001", "C401", "C405", "C408", "C417", "DTZ003", "DTZ005", "DTZ006", "DTZ007", "DTZ901", "E501", "FLY002", "G201", "ISC004", "LOG014", "LOG015", "PERF102", "PERF402", "PIE794", "PIE796", "PLW0602", "PLW1508", "PT014", "PYI034", "PYI036", "PYI063", "RUF009", "RUF012", "RUF013", "RUF015", "RUF023", "RUF032", "S102", "S110", "SIM102", "SIM103", "SIM113", "SIM115", "SIM117", "SIM118", "TRY002", "TRY004", "TRY201", "TRY401", "UP028", "UP031", ] [tool.ruff.lint.per-file-ignores] "docs/**/*.*" = ["A001"] [tool.ruff.lint.isort] detect-same-package = false # to not consider instrumentation packages as first-party known-first-party = ["opentelemetry"] known-third-party = [ "psutil", "pytest", "redis", "redis_opentracing", "opencensus", ] [tool.ruff.format] docstring-code-format = true # https://github.com/microsoft/pyright/blob/main/docs/configuration.md#type-check-rule-overrides [tool.pyright] typeCheckingMode = "strict" reportUnnecessaryTypeIgnoreComment = true reportMissingTypeStubs = false pythonVersion = "3.10" reportPrivateUsage = false # Ignore private attributes added by instrumentation packages. # Add progressively instrumentation packages here. include = [ "instrumentation/opentelemetry-instrumentation-aiohttp-client", "instrumentation/opentelemetry-instrumentation-aiokafka", "instrumentation/opentelemetry-instrumentation-asyncclick", "instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi", "instrumentation/opentelemetry-instrumentation-httpx", "instrumentation/opentelemetry-instrumentation-threading", "instrumentation-genai/opentelemetry-instrumentation-vertexai", "util/opentelemetry-util-genai", "exporter/opentelemetry-exporter-credential-provider-gcp", "instrumentation/opentelemetry-instrumentation-aiohttp-client", "opamp/opentelemetry-opamp-client", "sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace", "opentelemetry-instrumentation/src/opentelemetry/instrumentation/_labeler", ] # We should also add type hints to the test suite - It helps on finding bugs. # We are excluding for now because it's easier, and more important to add to the instrumentation packages. exclude = [ "instrumentation/opentelemetry-instrumentation-aiokafka/tests/**/*.py", "instrumentation/opentelemetry-instrumentation-asyncclick/tests/**/*.py", "instrumentation/opentelemetry-instrumentation-threading/tests/**", "instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/**/*.py", "instrumentation-genai/opentelemetry-instrumentation-vertexai/examples/**/*.py", "util/opentelemetry-util-genai/tests/**/*.py", "instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/**/*.py", "instrumentation/opentelemetry-instrumentation-httpx/tests/**/*.py", "opamp/opentelemetry-opamp-client/tests/**/*.py", "opamp/opentelemetry-opamp-client/src/opentelemetry/**/proto/**", ] [tool.towncrier] directory = ".changelog" filename = "CHANGELOG.md" start_string = "\n" template = "scripts/changelog_template.j2" issue_format = "[#{issue}](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/{issue})" wrap = true issue_pattern = "^(\\d+)" # NOTE: remember to update the changelog workflow if these types change [[tool.towncrier.type]] directory = "added" name = "Added" showcontent = true [[tool.towncrier.type]] directory = "changed" name = "Changed" showcontent = true [[tool.towncrier.type]] directory = "deprecated" name = "Deprecated" showcontent = true [[tool.towncrier.type]] directory = "removed" name = "Removed" showcontent = true [[tool.towncrier.type]] directory = "fixed" name = "Fixed" showcontent = true [dependency-groups] dev = [ "tox", "tox-uv>=1", "pre-commit", "towncrier", ] opentelemetry-python-contrib-0.66b2/pytest.ini000066400000000000000000000002111527070072300215700ustar00rootroot00000000000000[pytest] addopts = -rs -v log_cli = true log_cli_level = warning testpaths = instrumentation/opentelemetry-instrumentation-falcon/tests/ opentelemetry-python-contrib-0.66b2/resource/000077500000000000000000000000001527070072300213745ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/000077500000000000000000000000001527070072300310505ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/.changelog/000077500000000000000000000000001527070072300330555ustar00rootroot00000000000000.gitignore000066400000000000000000000000141527070072300347610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/.changelog!.gitignore 4973.changed000066400000000000000000000001171527070072300347160ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/.changelog`opentelemetry-resource-detector-azure`: add owners JacksonWeber and rads-1996 5012.added000066400000000000000000000001031527070072300343420ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/.changelog`opentelemetry-resource-detector-azure`: add AKS resource detector opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/CHANGELOG.md000066400000000000000000000030061527070072300326600ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Version 0.2.0 (2026-08-20) ### Changed - Ensure consistently use of suppress_instrumentation utils ([#2590](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2590)) - Update `opentelemetry-instrumentation` version to 0.44b0 ([#2961](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2961)) ## Version 0.1.5 (2024-05-16) - Ignore vm detector if already in other rps ([#2456](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2456)) - Implement functions resource detector ([#2523](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2523)) ## Version 0.1.4 (2024-04-05) - Fix windows tests/suppress instrumentation for urllib call ([#2178](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2178)) ## Version 0.1.3 (2024-01-25) - Change meta data service timeout to 200ms ([#2387](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2387)) ## Version 0.1.1 (2024-01-10) - Initial CHANGELOG.md entry opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/LICENSE000066400000000000000000000261351527070072300320640ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/MANIFEST.rst000066400000000000000000000002501527070072300330050ustar00rootroot00000000000000graft src graft tests global-exclude *.pyc global-exclude *.pyo global-exclude __pycache__/* include CHANGELOG.md include MANIFEST.in include README.rst include LICENSEopentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/README.rst000066400000000000000000000133311527070072300325400ustar00rootroot00000000000000OpenTelemetry Resource detectors for Azure ========================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-resource-detector-azure.svg :target: https://pypi.org/project/opentelemetry-resource-detector-azure/ This library contains OpenTelemetry `Resource Detectors `_ for the following Azure resources: * `Azure Kubernetes Service `_ * `Azure App Service `_ * `Azure Virtual Machines `_ * `Azure Functions (Experimental) `_ Installation ------------ :: pip install opentelemetry-resource-detector-azure --------------------------- Usage example for ``opentelemetry-resource-detector-azure`` .. code-block:: python from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.resource.detector.azure import ( AzureAKSResourceDetector, AzureAppServiceResourceDetector, AzureVMResourceDetector, ) from opentelemetry.sdk.resources import get_aggregated_resources trace.set_tracer_provider( TracerProvider( resource=get_aggregated_resources( [ AzureAKSResourceDetector(), AzureAppServiceResourceDetector(), AzureVMResourceDetector(), ] ), ) ) Mappings -------- The Azure Kubernetes Service Resource Detector reads the cluster resource ID from the ``CLUSTER_RESOURCE_ID`` environment variable or from a mounted ``aks-cluster-metadata`` ConfigMap at ``/etc/kubernetes/aks-cluster-metadata``. It sets the following Resource Attributes: * ``cloud.platform`` set to ``azure.aks``. * ``cloud.provider`` set to ``azure``. * ``cloud.resource_id`` set to the full Azure Resource Manager cluster resource ID. The native AKS ConfigMap is named ``aks-cluster-metadata`` and contains a ``clusterResourceId`` key. It can be exposed to a pod as an environment variable: .. code-block:: yaml env: - name: CLUSTER_RESOURCE_ID valueFrom: configMapKeyRef: name: aks-cluster-metadata key: clusterResourceId Alternatively, mount the ConfigMap as a volume: .. code-block:: yaml volumes: - name: aks-cluster-metadata configMap: name: aks-cluster-metadata volumeMounts: - name: aks-cluster-metadata mountPath: /etc/kubernetes/aks-cluster-metadata Kubernetes resolves ConfigMap references within the pod's namespace. Because the native ConfigMap is in ``kube-public``, copy it into the workload namespace or use tooling such as an init container to expose its value through one of the supported locations. The Azure App Service Resource Detector sets the following Resource Attributes: * ``service.name`` set to the value of the ``WEBSITE_SITE_NAME`` environment variable. * ``cloud.platform`` set to ``azure_app_service``. * ``cloud.provider`` set to ``azure``. * ``cloud.resource_id`` set using the ``WEBSITE_RESOURCE_GROUP``, ``WEBSITE_OWNER_NAME``, and ``WEBSITE_SITE_NAME`` environment variables. * ``cloud.region`` set to the value of the ``REGION_NAME`` environment variable. * ``deployment.environment`` set to the value of the ``WEBSITE_SLOT_NAME`` environment variable. * ``host.id`` set to the value of the ``WEBSITE_HOSTNAME`` environment variable. * ``service.instance.id`` set to the value of the ``WEBSITE_INSTANCE_ID`` environment variable. * ``azure.app.service.stamp`` set to the value of the ``WEBSITE_HOME_STAMPNAME`` environment variable. The Azure VM Resource Detector sets the following Resource Attributes according to the response from the `Azure Metadata Service `_: * ``azure.vm.scaleset.name`` set to the value of the ``vmScaleSetName`` field. * ``azure.vm.sku`` set to the value of the ``sku`` field. * ``cloud.platform`` set to the value of the ``azure_vm``. * ``cloud.provider`` set to the value of the ``azure``. * ``cloud.region`` set to the value of the ``location`` field. * ``cloud.resource_id`` set to the value of the ``resourceId`` field. * ``host.id`` set to the value of the ``vmId`` field. * ``host.name`` set to the value of the ``name`` field. * ``host.type`` set to the value of the ``vmSize`` field. * ``os.type`` set to the value of the ``osType`` field. * ``os.version`` set to the value of the ``version`` field. * ``service.instance.id`` set to the value of the ``vmId`` field. The Azure Functions Resource Detector is currently experimental. It sets the following Resource Attributes: * ``service.name`` set to the value of the ``WEBSITE_SITE_NAME`` environment variable. * ``process.id`` set to the process ID collected from the running process. * ``cloud.platform`` set to ``azure_functions``. * ``cloud.provider`` set to ``azure``. * ``cloud.resource_id`` set using the ``WEBSITE_RESOURCE_GROUP``, ``WEBSITE_OWNER_NAME``, and ``WEBSITE_SITE_NAME`` environment variables. * ``cloud.region`` set to the value of the ``REGION_NAME`` environment variable. * ``faas.instance`` set to the value of the ``WEBSITE_INSTANCE_ID`` environment variable. * ``faas.max_memory`` set to the value of the ``WEBSITE_MEMORY_LIMIT_MB`` environment variable. For more information, see the `Semantic Conventions for Cloud Resource Attributes `_. References ---------- * `OpenTelemetry Project `_opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/pyproject.toml000066400000000000000000000047241527070072300337730ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-resource-detector-azure" dynamic = ["version"] description = "Azure Resource Detector for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-sdk ~= 1.21", "opentelemetry-instrumentation ~= 0.44b0", ] [project.entry-points.opentelemetry_resource_detector] azure_aks = "opentelemetry.resource.detector.azure.aks:AzureAKSResourceDetector" azure_app_service = "opentelemetry.resource.detector.azure.app_service:AzureAppServiceResourceDetector" azure_functions = "opentelemetry.resource.detector.azure.functions:AzureFunctionsResourceDetector" azure_vm = "opentelemetry.resource.detector.azure.vm:AzureVMResourceDetector" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/resource/opentelemetry-resource-detector-azure" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/resource/detector/azure/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] [tool.towncrier] directory = ".changelog" filename = "CHANGELOG.md" start_string = "\n" template = "../../scripts/changelog_template.j2" issue_format = "[#{issue}](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/{issue})" wrap = true issue_pattern = "^(\\d+)" [[tool.towncrier.type]] directory = "added" name = "Added" showcontent = true [[tool.towncrier.type]] directory = "changed" name = "Changed" showcontent = true [[tool.towncrier.type]] directory = "deprecated" name = "Deprecated" showcontent = true [[tool.towncrier.type]] directory = "removed" name = "Removed" showcontent = true [[tool.towncrier.type]] directory = "fixed" name = "Fixed" showcontent = true opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/src/000077500000000000000000000000001527070072300316375ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300344545ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/srcresource/000077500000000000000000000000001527070072300363035ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/src/opentelemetrydetector/000077500000000000000000000000001527070072300401145ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resourceazure/000077500000000000000000000000001527070072300412425ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector__init__.py000066400000000000000000000007751527070072300433640ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=import-error from .aks import AzureAKSResourceDetector from .app_service import AzureAppServiceResourceDetector from .functions import AzureFunctionsResourceDetector from .version import __version__ from .vm import AzureVMResourceDetector __all__ = [ "AzureAKSResourceDetector", "AzureAppServiceResourceDetector", "AzureFunctionsResourceDetector", "AzureVMResourceDetector", "__version__", ] _constants.py000066400000000000000000000041701527070072300437710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.semconv.resource import ResourceAttributes # cSpell:disable # Azure Kubernetes _AKS_ARM_NAMESPACE_ID = "AKS_ARM_NAMESPACE_ID" _AKS_CLUSTER_RESOURCE_ID = "CLUSTER_RESOURCE_ID" _AKS_CLUSTER_RESOURCE_ID_KEY = "clusterResourceId" _AKS_METADATA_FILE_PATH = "/etc/kubernetes/aks-cluster-metadata" # AppService _AZURE_APP_SERVICE_STAMP_RESOURCE_ATTRIBUTE = "azure.app.service.stamp" _REGION_NAME = "REGION_NAME" _WEBSITE_HOME_STAMPNAME = "WEBSITE_HOME_STAMPNAME" _WEBSITE_HOSTNAME = "WEBSITE_HOSTNAME" _WEBSITE_INSTANCE_ID = "WEBSITE_INSTANCE_ID" _WEBSITE_OWNER_NAME = "WEBSITE_OWNER_NAME" _WEBSITE_RESOURCE_GROUP = "WEBSITE_RESOURCE_GROUP" _WEBSITE_SITE_NAME = "WEBSITE_SITE_NAME" _WEBSITE_SLOT_NAME = "WEBSITE_SLOT_NAME" _APP_SERVICE_ATTRIBUTE_ENV_VARS = { ResourceAttributes.CLOUD_REGION: _REGION_NAME, ResourceAttributes.DEPLOYMENT_ENVIRONMENT: _WEBSITE_SLOT_NAME, ResourceAttributes.HOST_ID: _WEBSITE_HOSTNAME, ResourceAttributes.SERVICE_INSTANCE_ID: _WEBSITE_INSTANCE_ID, _AZURE_APP_SERVICE_STAMP_RESOURCE_ATTRIBUTE: _WEBSITE_HOME_STAMPNAME, } # Functions _FUNCTIONS_WORKER_RUNTIME = "FUNCTIONS_WORKER_RUNTIME" _WEBSITE_MEMORY_LIMIT_MB = "WEBSITE_MEMORY_LIMIT_MB" _FUNCTIONS_ATTRIBUTE_ENV_VARS = { ResourceAttributes.FAAS_INSTANCE: _WEBSITE_INSTANCE_ID, ResourceAttributes.FAAS_MAX_MEMORY: _WEBSITE_MEMORY_LIMIT_MB, } # Vm _AZURE_VM_METADATA_ENDPOINT = "http://169.254.169.254/metadata/instance/compute?api-version=2021-12-13&format=json" _AZURE_VM_SCALE_SET_NAME_ATTRIBUTE = "azure.vm.scaleset.name" _AZURE_VM_SKU_ATTRIBUTE = "azure.vm.sku" _EXPECTED_AZURE_AMS_ATTRIBUTES = [ _AZURE_VM_SCALE_SET_NAME_ATTRIBUTE, _AZURE_VM_SKU_ATTRIBUTE, ResourceAttributes.CLOUD_PLATFORM, ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CLOUD_REGION, ResourceAttributes.CLOUD_RESOURCE_ID, ResourceAttributes.HOST_ID, ResourceAttributes.HOST_NAME, ResourceAttributes.HOST_TYPE, ResourceAttributes.OS_TYPE, ResourceAttributes.OS_VERSION, ResourceAttributes.SERVICE_INSTANCE_ID, ] # cSpell:enable _utils.py000066400000000000000000000027351527070072300431220ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from os import environ from pathlib import Path from ._constants import ( _AKS_ARM_NAMESPACE_ID, _AKS_CLUSTER_RESOURCE_ID, _AKS_METADATA_FILE_PATH, _FUNCTIONS_WORKER_RUNTIME, _WEBSITE_OWNER_NAME, _WEBSITE_RESOURCE_GROUP, _WEBSITE_SITE_NAME, ) def _is_on_aks() -> bool: return ( environ.get(_AKS_ARM_NAMESPACE_ID) is not None or environ.get(_AKS_CLUSTER_RESOURCE_ID) is not None or Path(_AKS_METADATA_FILE_PATH).exists() ) def _is_on_app_service() -> bool: return environ.get(_WEBSITE_SITE_NAME) is not None def _is_on_functions() -> bool: return environ.get(_FUNCTIONS_WORKER_RUNTIME) is not None def _can_ignore_vm_detect() -> bool: return _is_on_aks() or _is_on_app_service() or _is_on_functions() def _get_azure_resource_uri() -> str | None: website_site_name = environ.get(_WEBSITE_SITE_NAME) website_resource_group = environ.get(_WEBSITE_RESOURCE_GROUP) website_owner_name = environ.get(_WEBSITE_OWNER_NAME) subscription_id = website_owner_name if website_owner_name and "+" in website_owner_name: subscription_id = website_owner_name[0 : website_owner_name.index("+")] if not (website_site_name and website_resource_group and subscription_id): return None return f"/subscriptions/{subscription_id}/resourceGroups/{website_resource_group}/providers/Microsoft.Web/sites/{website_site_name}" aks.py000066400000000000000000000050651527070072300424000ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from logging import getLogger from os import environ from pathlib import Path from opentelemetry.sdk.resources import Resource, ResourceDetector from ._constants import ( _AKS_CLUSTER_RESOURCE_ID, _AKS_CLUSTER_RESOURCE_ID_KEY, _AKS_METADATA_FILE_PATH, ) _logger = getLogger(__name__) _CLOUD_ACCOUNT_ID = "cloud.account.id" _CLOUD_PLATFORM = "cloud.platform" _CLOUD_PROVIDER = "cloud.provider" _CLOUD_RESOURCE_ID = "cloud.resource_id" def _extract_subscription_id(resource_id: str) -> str | None: segments = resource_id.split("/") for index, segment in enumerate(segments): if segment.lower() == "subscriptions" and index < len(segments) - 1: return segments[index + 1] or None return None def _parse_aks_metadata(content: str) -> str | None: keyed_resource_id: str | None = None bare_values: list[str] = [] for line in content.splitlines(): stripped_line = line.strip().lstrip("\ufeff") if not stripped_line or stripped_line.startswith("#"): continue key, separator, value = stripped_line.partition("=") if not separator: bare_values.append(stripped_line) elif key.strip() == _AKS_CLUSTER_RESOURCE_ID_KEY and value.strip(): keyed_resource_id = value.strip() if keyed_resource_id: return keyed_resource_id if len(bare_values) == 1: return bare_values[0] return None def _get_aks_metadata_from_file() -> str | None: metadata_path = Path(_AKS_METADATA_FILE_PATH) try: if metadata_path.is_dir(): metadata_path = metadata_path / _AKS_CLUSTER_RESOURCE_ID_KEY content = metadata_path.read_text(encoding="utf-8") except (OSError, UnicodeError): _logger.debug( "Failed to read AKS metadata from %s", metadata_path, exc_info=True, ) return None return _parse_aks_metadata(content) class AzureAKSResourceDetector(ResourceDetector): def detect(self) -> Resource: resource_id = environ.get(_AKS_CLUSTER_RESOURCE_ID) or _get_aks_metadata_from_file() if not resource_id: return Resource({}) attributes = { _CLOUD_PROVIDER: "azure", _CLOUD_PLATFORM: "azure.aks", _CLOUD_RESOURCE_ID: resource_id, } subscription_id = _extract_subscription_id(resource_id) if subscription_id: attributes[_CLOUD_ACCOUNT_ID] = subscription_id return Resource(attributes) app_service.py000066400000000000000000000027441527070072300441230ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from os import environ from opentelemetry.resource.detector.azure._utils import ( _get_azure_resource_uri, _is_on_functions, ) from opentelemetry.sdk.resources import Resource, ResourceDetector from opentelemetry.semconv.resource import ( CloudPlatformValues, CloudProviderValues, ResourceAttributes, ) from ._constants import _APP_SERVICE_ATTRIBUTE_ENV_VARS, _WEBSITE_SITE_NAME class AzureAppServiceResourceDetector(ResourceDetector): def detect(self) -> Resource: attributes = {} website_site_name = environ.get(_WEBSITE_SITE_NAME) if website_site_name: # Functions resource detector takes priority with `service.name` and `cloud.platform` if not _is_on_functions(): attributes[ResourceAttributes.SERVICE_NAME] = website_site_name attributes[ResourceAttributes.CLOUD_PLATFORM] = CloudPlatformValues.AZURE_APP_SERVICE.value attributes[ResourceAttributes.CLOUD_PROVIDER] = CloudProviderValues.AZURE.value azure_resource_uri = _get_azure_resource_uri() if azure_resource_uri: attributes[ResourceAttributes.CLOUD_RESOURCE_ID] = azure_resource_uri for key, env_var in _APP_SERVICE_ATTRIBUTE_ENV_VARS.items(): value = environ.get(env_var) if value: attributes[key] = value return Resource(attributes) functions.py000066400000000000000000000035321527070072300436270ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from os import environ, getpid from opentelemetry.resource.detector.azure._utils import ( _get_azure_resource_uri, _is_on_functions, ) from opentelemetry.sdk.resources import Resource, ResourceDetector from opentelemetry.semconv.resource import ( CloudPlatformValues, CloudProviderValues, ResourceAttributes, ) from ._constants import ( _FUNCTIONS_ATTRIBUTE_ENV_VARS, _REGION_NAME, _WEBSITE_SITE_NAME, ) class AzureFunctionsResourceDetector(ResourceDetector): def detect(self) -> Resource: attributes = {} if _is_on_functions(): website_site_name = environ.get(_WEBSITE_SITE_NAME) if website_site_name: attributes[ResourceAttributes.SERVICE_NAME] = website_site_name attributes[ResourceAttributes.PROCESS_PID] = getpid() attributes[ResourceAttributes.CLOUD_PROVIDER] = CloudProviderValues.AZURE.value attributes[ResourceAttributes.CLOUD_PLATFORM] = CloudPlatformValues.AZURE_FUNCTIONS.value cloud_region = environ.get(_REGION_NAME) if cloud_region: attributes[ResourceAttributes.CLOUD_REGION] = cloud_region azure_resource_uri = _get_azure_resource_uri() if azure_resource_uri: attributes[ResourceAttributes.CLOUD_RESOURCE_ID] = azure_resource_uri for key, env_var in _FUNCTIONS_ATTRIBUTE_ENV_VARS.items(): value = environ.get(env_var) if value: if key == ResourceAttributes.FAAS_MAX_MEMORY: try: value = int(value) except ValueError: continue attributes[key] = value return Resource(attributes) version.py000066400000000000000000000001471527070072300433030ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.3.0.dev" vm.py000066400000000000000000000061201527070072300422350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from json import loads from logging import getLogger from urllib.error import URLError from urllib.request import Request, urlopen from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.sdk.resources import Resource, ResourceDetector from opentelemetry.semconv.resource import ( CloudPlatformValues, CloudProviderValues, ResourceAttributes, ) from ._constants import ( _AZURE_VM_METADATA_ENDPOINT, _AZURE_VM_SCALE_SET_NAME_ATTRIBUTE, _AZURE_VM_SKU_ATTRIBUTE, _EXPECTED_AZURE_AMS_ATTRIBUTES, ) from ._utils import _can_ignore_vm_detect _logger = getLogger(__name__) class AzureVMResourceDetector(ResourceDetector): # pylint: disable=no-self-use def detect(self) -> "Resource": attributes = {} if not _can_ignore_vm_detect(): with suppress_instrumentation(): metadata_json = _get_azure_vm_metadata() if not metadata_json: return Resource(attributes) for attribute_key in _EXPECTED_AZURE_AMS_ATTRIBUTES: attributes[attribute_key] = _get_attribute_from_metadata(metadata_json, attribute_key) return Resource(attributes) def _get_azure_vm_metadata(): request = Request(_AZURE_VM_METADATA_ENDPOINT) request.add_header("Metadata", "True") try: # VM metadata service should not take more than 200ms on success case with urlopen(request, timeout=0.2) as response: return loads(response.read()) except URLError: # Not on Azure VM return None except Exception as e: # pylint: disable=broad-except,invalid-name _logger.exception("Failed to receive Azure VM metadata: %s", e) return None def _get_attribute_from_metadata(metadata_json, attribute_key): ams_value = "" if attribute_key == _AZURE_VM_SCALE_SET_NAME_ATTRIBUTE: ams_value = metadata_json["vmScaleSetName"] elif attribute_key == _AZURE_VM_SKU_ATTRIBUTE: ams_value = metadata_json["sku"] elif attribute_key == ResourceAttributes.CLOUD_PLATFORM: ams_value = CloudPlatformValues.AZURE_VM.value elif attribute_key == ResourceAttributes.CLOUD_PROVIDER: ams_value = CloudProviderValues.AZURE.value elif attribute_key == ResourceAttributes.CLOUD_REGION: ams_value = metadata_json["location"] elif attribute_key == ResourceAttributes.CLOUD_RESOURCE_ID: ams_value = metadata_json["resourceId"] elif attribute_key in ( ResourceAttributes.HOST_ID, ResourceAttributes.SERVICE_INSTANCE_ID, ): ams_value = metadata_json["vmId"] elif attribute_key == ResourceAttributes.HOST_NAME: ams_value = metadata_json["name"] elif attribute_key == ResourceAttributes.HOST_TYPE: ams_value = metadata_json["vmSize"] elif attribute_key == ResourceAttributes.OS_TYPE: ams_value = metadata_json["osType"] elif attribute_key == ResourceAttributes.OS_VERSION: ams_value = metadata_json["version"] return ams_value test-requirements-0.txt000066400000000000000000000006771527070072300354010ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azureasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 opentelemetry-api==1.21 # when updating, also update in pyproject.toml opentelemetry-sdk==1.21 # when updating, also update in pyproject.toml opentelemetry-instrumentation==0.44b0 # when updating, also update in pyproject.toml -e resource/opentelemetry-resource-detector-azure test-requirements-1.txt000066400000000000000000000005151527070072300353710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azureasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 # test with the latest version of opentelemetry-api, sdk, and semantic conventions -e opentelemetry-instrumentation -e resource/opentelemetry-resource-detector-azure opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/tests/000077500000000000000000000000001527070072300322125ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/tests/__init__.py000066400000000000000000000001141527070072300343170ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/tests/test_aks.py000066400000000000000000000164341527070072300344110ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from collections.abc import Mapping from pathlib import Path from tempfile import TemporaryDirectory from unittest.mock import patch from opentelemetry.resource.detector.azure._utils import _is_on_aks from opentelemetry.resource.detector.azure.aks import ( AzureAKSResourceDetector, ) from opentelemetry.resource.detector.azure.vm import AzureVMResourceDetector TEST_RESOURCE_ID = ( "/subscriptions/test-sub/resourceGroups/test-rg/providers/" "Microsoft.ContainerService/managedClusters/test-aks-cluster" ) class TestAzureAKSResourceDetector(unittest.TestCase): @patch.dict("os.environ", {"CLUSTER_RESOURCE_ID": TEST_RESOURCE_ID}, clear=True) def test_detects_aks_from_environment(self) -> None: attributes = AzureAKSResourceDetector().detect().attributes self.assertEqual(attributes["cloud.provider"], "azure") self.assertEqual(attributes["cloud.platform"], "azure.aks") self.assertEqual(attributes["cloud.resource_id"], TEST_RESOURCE_ID) self.assertEqual(attributes["cloud.account.id"], "test-sub") self.assertNotIn("k8s.cluster.name", attributes) self.assertIsInstance(attributes["cloud.provider"], str) self.assertIsInstance(attributes["cloud.platform"], str) self.assertIsInstance(attributes["cloud.resource_id"], str) self.assertIsInstance(attributes["cloud.account.id"], str) @patch.dict( "os.environ", { "CLUSTER_RESOURCE_ID": ( "/Subscriptions/test-sub/resourceGroups/test-rg/providers/" "Microsoft.ContainerService/ManagedClusters/my-cluster" ) }, clear=True, ) def test_subscription_segment_is_case_insensitive(self) -> None: attributes = AzureAKSResourceDetector().detect().attributes self.assertEqual(attributes["cloud.account.id"], "test-sub") @patch.dict("os.environ", {"CLUSTER_RESOURCE_ID": "standalone-name"}, clear=True) def test_omits_account_id_without_subscription_segment(self) -> None: attributes = AzureAKSResourceDetector().detect().attributes self.assertNotIn("cloud.account.id", attributes) @patch.dict("os.environ", {}, clear=True) @patch( "opentelemetry.resource.detector.azure.aks._AKS_METADATA_FILE_PATH", "/missing/aks-cluster-metadata", ) def test_returns_empty_resource_outside_aks(self) -> None: resource = AzureAKSResourceDetector().detect() self.assertEqual(resource.attributes, {}) @patch.dict("os.environ", {}, clear=True) def test_detects_aks_from_configmap_volume(self) -> None: with TemporaryDirectory() as directory: metadata_path = Path(directory) / "aks-cluster-metadata" metadata_path.mkdir() (metadata_path / "clusterResourceId").write_text(f"{TEST_RESOURCE_ID}\n", encoding="utf-8") with patch( "opentelemetry.resource.detector.azure.aks._AKS_METADATA_FILE_PATH", str(metadata_path), ): attributes = AzureAKSResourceDetector().detect().attributes self.assertEqual(attributes["cloud.resource_id"], TEST_RESOURCE_ID) @patch.dict("os.environ", {}, clear=True) def test_detects_aks_from_subpath_mount(self) -> None: with TemporaryDirectory() as directory: metadata_path = Path(directory) / "aks-cluster-metadata" metadata_path.write_text(f"{TEST_RESOURCE_ID}\n", encoding="utf-8") with patch( "opentelemetry.resource.detector.azure.aks._AKS_METADATA_FILE_PATH", str(metadata_path), ): attributes = AzureAKSResourceDetector().detect().attributes self.assertEqual(attributes["cloud.resource_id"], TEST_RESOURCE_ID) @patch.dict("os.environ", {}, clear=True) def test_detects_aks_from_key_value_file(self) -> None: content = f"\ufeff# AKS metadata\r\nclusterResourceId={TEST_RESOURCE_ID}\r\n" attributes = self._detect_from_file(content) self.assertEqual(attributes["cloud.resource_id"], TEST_RESOURCE_ID) @patch.dict("os.environ", {}, clear=True) def test_explicit_key_wins_over_bare_lines(self) -> None: content = f"clusterResourceId={TEST_RESOURCE_ID}\nstray-token\n// not a supported comment\n" attributes = self._detect_from_file(content) self.assertEqual(attributes["cloud.resource_id"], TEST_RESOURCE_ID) @patch.dict("os.environ", {}, clear=True) def test_ignores_ambiguous_bare_values(self) -> None: attributes = self._detect_from_file(f"{TEST_RESOURCE_ID}\nstray-token\n") self.assertEqual(attributes, {}) @patch.dict("os.environ", {}, clear=True) def test_ignores_configmap_volume_without_resource_id(self) -> None: with TemporaryDirectory() as directory: metadata_path = Path(directory) / "aks-cluster-metadata" metadata_path.mkdir() (metadata_path / "somethingElse").write_text("value\n", encoding="utf-8") with patch( "opentelemetry.resource.detector.azure.aks._AKS_METADATA_FILE_PATH", str(metadata_path), ): attributes = AzureAKSResourceDetector().detect().attributes self.assertEqual(attributes, {}) @patch.dict( "os.environ", { "CLUSTER_RESOURCE_ID": ( "/subscriptions/test-sub/resourceGroups/test-rg/providers/" "Microsoft.ContainerService/managedClusters/from-env" ) }, clear=True, ) def test_environment_takes_precedence_over_file(self) -> None: attributes = self._detect_from_file(TEST_RESOURCE_ID) self.assertEqual( attributes["cloud.resource_id"], "/subscriptions/test-sub/resourceGroups/test-rg/providers/" "Microsoft.ContainerService/managedClusters/from-env", ) @patch.dict("os.environ", {"CLUSTER_RESOURCE_ID": TEST_RESOURCE_ID}, clear=True) @patch("opentelemetry.resource.detector.azure.vm.urlopen") def test_vm_detection_is_skipped_on_aks(self, mock_urlopen) -> None: resource = AzureVMResourceDetector().detect() self.assertEqual(resource.attributes, {}) mock_urlopen.assert_not_called() @patch.dict("os.environ", {}, clear=True) def test_mounted_metadata_marks_environment_as_aks(self) -> None: with TemporaryDirectory() as directory: metadata_path = Path(directory) / "aks-cluster-metadata" metadata_path.write_text(TEST_RESOURCE_ID, encoding="utf-8") with patch( "opentelemetry.resource.detector.azure._utils._AKS_METADATA_FILE_PATH", str(metadata_path), ): self.assertTrue(_is_on_aks()) @staticmethod def _detect_from_file(content: str) -> Mapping[str, object]: with TemporaryDirectory() as directory: metadata_path = Path(directory) / "aks-cluster-metadata" metadata_path.write_text(content, encoding="utf-8") with patch( "opentelemetry.resource.detector.azure.aks._AKS_METADATA_FILE_PATH", str(metadata_path), ): return AzureAKSResourceDetector().detect().attributes test_app_service.py000066400000000000000000000157201527070072300360510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from unittest.mock import patch # pylint: disable=no-name-in-module from opentelemetry.resource.detector.azure.app_service import ( AzureAppServiceResourceDetector, ) TEST_WEBSITE_SITE_NAME = "TEST_WEBSITE_SITE_NAME" TEST_REGION_NAME = "TEST_REGION_NAME" TEST_WEBSITE_SLOT_NAME = "TEST_WEBSITE_SLOT_NAME" TEST_WEBSITE_HOSTNAME = "TEST_WEBSITE_HOSTNAME" TEST_WEBSITE_INSTANCE_ID = "TEST_WEBSITE_INSTANCE_ID" TEST_WEBSITE_HOME_STAMPNAME = "TEST_WEBSITE_HOME_STAMPNAME" TEST_WEBSITE_RESOURCE_GROUP = "TEST_WEBSITE_RESOURCE_GROUP" TEST_WEBSITE_OWNER_NAME = "TEST_WEBSITE_OWNER_NAME" class TestAzureAppServiceResourceDetector(unittest.TestCase): @patch.dict( "os.environ", { "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, "REGION_NAME": TEST_REGION_NAME, "WEBSITE_SLOT_NAME": TEST_WEBSITE_SLOT_NAME, "WEBSITE_HOSTNAME": TEST_WEBSITE_HOSTNAME, "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, "WEBSITE_HOME_STAMPNAME": TEST_WEBSITE_HOME_STAMPNAME, "WEBSITE_RESOURCE_GROUP": TEST_WEBSITE_RESOURCE_GROUP, "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, }, clear=True, ) def test_on_app_service(self): resource = AzureAppServiceResourceDetector().detect() attributes = resource.attributes self.assertEqual(attributes["service.name"], TEST_WEBSITE_SITE_NAME) self.assertEqual(attributes["cloud.provider"], "azure") self.assertEqual(attributes["cloud.platform"], "azure_app_service") self.assertEqual( attributes["cloud.resource_id"], f"/subscriptions/{TEST_WEBSITE_OWNER_NAME}/resourceGroups/{TEST_WEBSITE_RESOURCE_GROUP}/providers/Microsoft.Web/sites/{TEST_WEBSITE_SITE_NAME}", ) self.assertEqual(attributes["cloud.region"], TEST_REGION_NAME) self.assertEqual(attributes["deployment.environment"], TEST_WEBSITE_SLOT_NAME) self.assertEqual(attributes["host.id"], TEST_WEBSITE_HOSTNAME) self.assertEqual(attributes["service.instance.id"], TEST_WEBSITE_INSTANCE_ID) self.assertEqual(attributes["azure.app.service.stamp"], TEST_WEBSITE_HOME_STAMPNAME) @patch.dict( "os.environ", { "FUNCTIONS_WORKER_RUNTIME": "1", "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, "REGION_NAME": TEST_REGION_NAME, "WEBSITE_SLOT_NAME": TEST_WEBSITE_SLOT_NAME, "WEBSITE_HOSTNAME": TEST_WEBSITE_HOSTNAME, "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, "WEBSITE_HOME_STAMPNAME": TEST_WEBSITE_HOME_STAMPNAME, "WEBSITE_RESOURCE_GROUP": TEST_WEBSITE_RESOURCE_GROUP, "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, }, clear=True, ) def test_on_app_service_with_functions(self): resource = AzureAppServiceResourceDetector().detect() attributes = resource.attributes self.assertIsNone(attributes.get("service.name")) self.assertEqual(attributes["cloud.provider"], "azure") self.assertIsNone(attributes.get("cloud.platform")) self.assertEqual( attributes["cloud.resource_id"], f"/subscriptions/{TEST_WEBSITE_OWNER_NAME}/resourceGroups/{TEST_WEBSITE_RESOURCE_GROUP}/providers/Microsoft.Web/sites/{TEST_WEBSITE_SITE_NAME}", ) self.assertEqual(attributes["cloud.region"], TEST_REGION_NAME) self.assertEqual(attributes["deployment.environment"], TEST_WEBSITE_SLOT_NAME) self.assertEqual(attributes["host.id"], TEST_WEBSITE_HOSTNAME) self.assertEqual(attributes["service.instance.id"], TEST_WEBSITE_INSTANCE_ID) self.assertEqual(attributes["azure.app.service.stamp"], TEST_WEBSITE_HOME_STAMPNAME) @patch.dict( "os.environ", { "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, "REGION_NAME": TEST_REGION_NAME, "WEBSITE_SLOT_NAME": TEST_WEBSITE_SLOT_NAME, "WEBSITE_HOSTNAME": TEST_WEBSITE_HOSTNAME, "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, "WEBSITE_HOME_STAMPNAME": TEST_WEBSITE_HOME_STAMPNAME, "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, }, clear=True, ) def test_on_app_service_no_resource_group(self): resource = AzureAppServiceResourceDetector().detect() attributes = resource.attributes self.assertEqual(attributes["service.name"], TEST_WEBSITE_SITE_NAME) self.assertEqual(attributes["cloud.provider"], "azure") self.assertEqual(attributes["cloud.platform"], "azure_app_service") self.assertTrue("cloud.resource_id" not in attributes) self.assertEqual(attributes["cloud.region"], TEST_REGION_NAME) self.assertEqual(attributes["deployment.environment"], TEST_WEBSITE_SLOT_NAME) self.assertEqual(attributes["host.id"], TEST_WEBSITE_HOSTNAME) self.assertEqual(attributes["service.instance.id"], TEST_WEBSITE_INSTANCE_ID) self.assertEqual(attributes["azure.app.service.stamp"], TEST_WEBSITE_HOME_STAMPNAME) @patch.dict( "os.environ", { "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, "REGION_NAME": TEST_REGION_NAME, "WEBSITE_SLOT_NAME": TEST_WEBSITE_SLOT_NAME, "WEBSITE_HOSTNAME": TEST_WEBSITE_HOSTNAME, "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, "WEBSITE_HOME_STAMPNAME": TEST_WEBSITE_HOME_STAMPNAME, "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, }, clear=True, ) def test_on_app_service_no_owner(self): resource = AzureAppServiceResourceDetector().detect() attributes = resource.attributes self.assertEqual(attributes["service.name"], TEST_WEBSITE_SITE_NAME) self.assertEqual(attributes["cloud.provider"], "azure") self.assertEqual(attributes["cloud.platform"], "azure_app_service") self.assertTrue("cloud.resource_id" not in attributes) self.assertEqual(attributes["cloud.region"], TEST_REGION_NAME) self.assertEqual(attributes["deployment.environment"], TEST_WEBSITE_SLOT_NAME) self.assertEqual(attributes["host.id"], TEST_WEBSITE_HOSTNAME) self.assertEqual(attributes["service.instance.id"], TEST_WEBSITE_INSTANCE_ID) self.assertEqual(attributes["azure.app.service.stamp"], TEST_WEBSITE_HOME_STAMPNAME) @patch.dict( "os.environ", { "REGION_NAME": TEST_REGION_NAME, "WEBSITE_SLOT_NAME": TEST_WEBSITE_SLOT_NAME, "WEBSITE_HOSTNAME": TEST_WEBSITE_HOSTNAME, "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, "WEBSITE_HOME_STAMPNAME": TEST_WEBSITE_HOME_STAMPNAME, "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, }, clear=True, ) def test_off_app_service(self): resource = AzureAppServiceResourceDetector().detect() self.assertEqual(resource.attributes, {}) test_functions.py000066400000000000000000000100471527070072300355560ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from unittest.mock import patch # pylint: disable=no-name-in-module from opentelemetry.resource.detector.azure.functions import ( AzureFunctionsResourceDetector, ) TEST_WEBSITE_SITE_NAME = "TEST_WEBSITE_SITE_NAME" TEST_REGION_NAME = "TEST_REGION_NAME" TEST_WEBSITE_INSTANCE_ID = "TEST_WEBSITE_INSTANCE_ID" TEST_WEBSITE_RESOURCE_GROUP = "TEST_WEBSITE_RESOURCE_GROUP" TEST_WEBSITE_OWNER_NAME = "TEST_WEBSITE_OWNER_NAME" TEST_WEBSITE_MEMORY_LIMIT_MB = "1024" class TestAzureAppServiceResourceDetector(unittest.TestCase): @patch.dict( "os.environ", { "FUNCTIONS_WORKER_RUNTIME": "1", "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, "REGION_NAME": TEST_REGION_NAME, "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, "WEBSITE_RESOURCE_GROUP": TEST_WEBSITE_RESOURCE_GROUP, "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, "WEBSITE_MEMORY_LIMIT_MB": TEST_WEBSITE_MEMORY_LIMIT_MB, }, clear=True, ) @patch("opentelemetry.resource.detector.azure.functions.getpid") def test_on_functions(self, pid_mock): pid_mock.return_value = 1000 resource = AzureFunctionsResourceDetector().detect() attributes = resource.attributes self.assertEqual(attributes["service.name"], TEST_WEBSITE_SITE_NAME) self.assertEqual(attributes["cloud.provider"], "azure") self.assertEqual(attributes["cloud.platform"], "azure_functions") self.assertEqual(attributes["process.pid"], 1000) self.assertEqual( attributes["cloud.resource_id"], f"/subscriptions/{TEST_WEBSITE_OWNER_NAME}/resourceGroups/{TEST_WEBSITE_RESOURCE_GROUP}/providers/Microsoft.Web/sites/{TEST_WEBSITE_SITE_NAME}", ) self.assertEqual(attributes["cloud.region"], TEST_REGION_NAME) self.assertEqual(attributes["faas.instance"], TEST_WEBSITE_INSTANCE_ID) self.assertEqual(attributes["faas.max_memory"], 1024) @patch.dict( "os.environ", { "FUNCTIONS_WORKER_RUNTIME": "1", "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, "REGION_NAME": TEST_REGION_NAME, "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, "WEBSITE_RESOURCE_GROUP": TEST_WEBSITE_RESOURCE_GROUP, "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, "WEBSITE_MEMORY_LIMIT_MB": "error", }, clear=True, ) @patch("opentelemetry.resource.detector.azure.functions.getpid") def test_on_functions_error_memory(self, pid_mock): pid_mock.return_value = 1000 resource = AzureFunctionsResourceDetector().detect() attributes = resource.attributes self.assertEqual(attributes["service.name"], TEST_WEBSITE_SITE_NAME) self.assertEqual(attributes["cloud.provider"], "azure") self.assertEqual(attributes["cloud.platform"], "azure_functions") self.assertEqual(attributes["process.pid"], 1000) self.assertEqual( attributes["cloud.resource_id"], f"/subscriptions/{TEST_WEBSITE_OWNER_NAME}/resourceGroups/{TEST_WEBSITE_RESOURCE_GROUP}/providers/Microsoft.Web/sites/{TEST_WEBSITE_SITE_NAME}", ) self.assertEqual(attributes["cloud.region"], TEST_REGION_NAME) self.assertEqual(attributes["faas.instance"], TEST_WEBSITE_INSTANCE_ID) self.assertIsNone(attributes.get("faas.max_memory")) @patch.dict( "os.environ", { "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, "REGION_NAME": TEST_REGION_NAME, "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, "WEBSITE_RESOURCE_GROUP": TEST_WEBSITE_RESOURCE_GROUP, "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, "WEBSITE_MEMORY_LIMIT_MB": TEST_WEBSITE_MEMORY_LIMIT_MB, }, clear=True, ) def test_off_app_service(self): resource = AzureFunctionsResourceDetector().detect() self.assertEqual(resource.attributes, {}) opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-azure/tests/test_vm.py000066400000000000000000000324111527070072300342460ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from unittest.mock import patch # pylint: disable=no-name-in-module from opentelemetry.resource.detector.azure.vm import AzureVMResourceDetector LINUX_JSON = """ { "additionalCapabilities": { "hibernationEnabled": "false" }, "azEnvironment": "AzurePublicCloud", "customData": "", "evictionPolicy": "", "extendedLocation": { "name": "", "type": "" }, "host": { "id": "" }, "hostGroup": { "id": "" }, "isHostCompatibilityLayerVm": "true", "licenseType": "", "location": "westus", "name": "examplevmname", "offer": "0001-com-ubuntu-server-focal", "osProfile": { "adminUsername": "azureuser", "computerName": "examplevmname", "disablePasswordAuthentication": "true" }, "osType": "Linux", "placementGroupId": "", "plan": { "name": "", "product": "", "publisher": "" }, "platformFaultDomain": "0", "platformSubFaultDomain": "", "platformUpdateDomain": "0", "priority": "", "provider": "Microsoft.Compute", "publicKeys": [ { "keyData": "ssh-rsa 0", "path": "/home/user/.ssh/authorized_keys0" }, { "keyData": "ssh-rsa 1", "path": "/home/user/.ssh/authorized_keys1" } ], "publisher": "canonical", "resourceGroupName": "macikgo-test-may-23", "resourceId": "/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/resourceGroups/macikgo-test-may-23/providers/Microsoft.Compute/virtualMachines/examplevmname", "securityProfile": { "encryptionAtHost": "false", "secureBootEnabled": "true", "securityType": "TrustedLaunch", "virtualTpmEnabled": "true" }, "sku": "20_04-lts-gen2", "storageProfile": { "dataDisks": [ { "bytesPerSecondThrottle": "979202048", "caching": "None", "createOption": "Empty", "diskCapacityBytes": "274877906944", "diskSizeGB": "1024", "image": { "uri": "" }, "isSharedDisk": "false", "isUltraDisk": "true", "lun": "0", "managedDisk": { "id": "/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/resourceGroups/macikgo-test-may-23/providers/Microsoft.Compute/disks/exampledatadiskname", "storageAccountType": "StandardSSD_LRS" }, "name": "exampledatadiskname", "opsPerSecondThrottle": "65280", "vhd": { "uri": "" }, "writeAcceleratorEnabled": "false" } ], "imageReference": { "id": "", "offer": "0001-com-ubuntu-server-focal", "publisher": "canonical", "sku": "20_04-lts-gen2", "version": "latest" }, "osDisk": { "caching": "ReadWrite", "createOption": "FromImage", "diffDiskSettings": { "option": "" }, "diskSizeGB": "30", "encryptionSettings": { "enabled": "false", "diskEncryptionKey": { "sourceVault": { "id": "/subscriptions/test-source-guid/resourceGroups/testrg/providers/Microsoft.KeyVault/vaults/test-kv" }, "secretUrl": "https://test-disk.vault.azure.net/secrets/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx" }, "keyEncryptionKey": { "sourceVault": { "id": "/subscriptions/test-key-guid/resourceGroups/testrg/providers/Microsoft.KeyVault/vaults/test-kv" }, "keyUrl": "https://test-key.vault.azure.net/secrets/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx" } }, "image": { "uri": "" }, "managedDisk": { "id": "/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/resourceGroups/macikgo-test-may-23/providers/Microsoft.Compute/disks/exampleosdiskname", "storageAccountType": "StandardSSD_LRS" }, "name": "exampledatadiskname", "osType": "Linux", "vhd": { "uri": "" }, "writeAcceleratorEnabled": "false" }, "resourceDisk": { "size": "16384" } }, "subscriptionId": "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx", "tags": "azsecpack:nonprod;platformsettings.host_environment.service.platform_optedin_for_rootcerts:true", "tagsList": [ { "name": "azsecpack", "value": "nonprod" }, { "name": "platformsettings.host_environment.service.platform_optedin_for_rootcerts", "value": "true" } ], "userData": "", "version": "20.04.202307240", "virtualMachineScaleSet": { "id": "/subscriptions/xxxxxxxx-xxxxx-xxx-xxx-xxxx/resourceGroups/resource-group-name/providers/Microsoft.Compute/virtualMachineScaleSets/virtual-machine-scale-set-name" }, "vmId": "02aab8a4-74ef-476e-8182-f6d2ba4166a6", "vmScaleSetName": "crpteste9vflji9", "vmSize": "Standard_A3", "zone": "1" } """ WINDOWS_JSON = """ { "additionalCapabilities": { "hibernationEnabled": "false" }, "azEnvironment": "AzurePublicCloud", "customData": "", "evictionPolicy": "", "extendedLocation": { "name": "", "type": "" }, "host": { "id": "" }, "hostGroup": { "id": "" }, "isHostCompatibilityLayerVm": "true", "licenseType": "Windows_Client", "location": "westus", "name": "examplevmname", "offer": "WindowsServer", "osProfile": { "adminUsername": "azureuser", "computerName": "examplevmname", "disablePasswordAuthentication": "true" }, "osType": "Windows", "placementGroupId": "", "plan": { "name": "", "product": "", "publisher": "" }, "platformFaultDomain": "0", "platformSubFaultDomain": "", "platformUpdateDomain": "0", "priority": "", "provider": "Microsoft.Compute", "publicKeys": [ { "keyData": "ssh-rsa 0", "path": "/home/user/.ssh/authorized_keys0" }, { "keyData": "ssh-rsa 1", "path": "/home/user/.ssh/authorized_keys1" } ], "publisher": "RDFE-Test-Microsoft-Windows-Server-Group", "resourceGroupName": "macikgo-test-may-23", "resourceId": "/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/resourceGroups/macikgo-test-may-23/providers/Microsoft.Compute/virtualMachines/examplevmname", "securityProfile": { "encryptionAtHost": "false", "secureBootEnabled": "true", "securityType": "TrustedLaunch", "virtualTpmEnabled": "true" }, "sku": "2019-Datacenter", "storageProfile": { "dataDisks": [ { "bytesPerSecondThrottle": "979202048", "caching": "None", "createOption": "Empty", "diskCapacityBytes": "274877906944", "diskSizeGB": "1024", "image": { "uri": "" }, "isSharedDisk": "false", "isUltraDisk": "true", "lun": "0", "managedDisk": { "id": "/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/resourceGroups/macikgo-test-may-23/providers/Microsoft.Compute/disks/exampledatadiskname", "storageAccountType": "StandardSSD_LRS" }, "name": "exampledatadiskname", "opsPerSecondThrottle": "65280", "vhd": { "uri": "" }, "writeAcceleratorEnabled": "false" } ], "imageReference": { "id": "", "offer": "WindowsServer", "publisher": "MicrosoftWindowsServer", "sku": "2019-Datacenter", "version": "latest" }, "osDisk": { "caching": "ReadWrite", "createOption": "FromImage", "diffDiskSettings": { "option": "" }, "diskSizeGB": "30", "encryptionSettings": { "enabled": "false", "diskEncryptionKey": { "sourceVault": { "id": "/subscriptions/test-source-guid/resourceGroups/testrg/providers/Microsoft.KeyVault/vaults/test-kv" }, "secretUrl": "https://test-disk.vault.azure.net/secrets/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx" }, "keyEncryptionKey": { "sourceVault": { "id": "/subscriptions/test-key-guid/resourceGroups/testrg/providers/Microsoft.KeyVault/vaults/test-kv" }, "keyUrl": "https://test-key.vault.azure.net/secrets/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx" } }, "image": { "uri": "" }, "managedDisk": { "id": "/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/resourceGroups/macikgo-test-may-23/providers/Microsoft.Compute/disks/exampleosdiskname", "storageAccountType": "StandardSSD_LRS" }, "name": "exampledatadiskname", "osType": "Windows", "vhd": { "uri": "" }, "writeAcceleratorEnabled": "false" }, "resourceDisk": { "size": "16384" } }, "subscriptionId": "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx", "tags": "azsecpack:nonprod;platformsettings.host_environment.service.platform_optedin_for_rootcerts:true", "userData": "Zm9vYmFy", "tagsList": [ { "name": "azsecpack", "value": "nonprod" }, { "name": "platformsettings.host_environment.service.platform_optedin_for_rootcerts", "value": "true" } ], "userData": "", "version": "20.04.202307240", "virtualMachineScaleSet": { "id": "/subscriptions/xxxxxxxx-xxxxx-xxx-xxx-xxxx/resourceGroups/resource-group-name/providers/Microsoft.Compute/virtualMachineScaleSets/virtual-machine-scale-set-name" }, "vmId": "02aab8a4-74ef-476e-8182-f6d2ba4166a6", "vmScaleSetName": "crpteste9vflji9", "vmSize": "Standard_A3", "zone": "1" } """ LINUX_ATTRIBUTES = { "azure.vm.scaleset.name": "crpteste9vflji9", "azure.vm.sku": "20_04-lts-gen2", "cloud.platform": "azure_vm", "cloud.provider": "azure", "cloud.region": "westus", "cloud.resource_id": "/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/resourceGroups/macikgo-test-may-23/providers/Microsoft.Compute/virtualMachines/examplevmname", "host.id": "02aab8a4-74ef-476e-8182-f6d2ba4166a6", "host.name": "examplevmname", "host.type": "Standard_A3", "os.type": "Linux", "os.version": "20.04.202307240", "service.instance.id": "02aab8a4-74ef-476e-8182-f6d2ba4166a6", } WINDOWS_ATTRIBUTES = { "azure.vm.scaleset.name": "crpteste9vflji9", "azure.vm.sku": "2019-Datacenter", "cloud.platform": "azure_vm", "cloud.provider": "azure", "cloud.region": "westus", "cloud.resource_id": "/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/resourceGroups/macikgo-test-may-23/providers/Microsoft.Compute/virtualMachines/examplevmname", "host.id": "02aab8a4-74ef-476e-8182-f6d2ba4166a6", "host.name": "examplevmname", "host.type": "Standard_A3", "os.type": "Windows", "os.version": "20.04.202307240", "service.instance.id": "02aab8a4-74ef-476e-8182-f6d2ba4166a6", } class TestAzureVMResourceDetector(unittest.TestCase): @patch("opentelemetry.resource.detector.azure.vm.urlopen") def test_linux(self, mock_urlopen): mock_urlopen.return_value.__enter__.return_value.read.return_value = LINUX_JSON attributes = AzureVMResourceDetector().detect().attributes for attribute_key, attribute_value in LINUX_ATTRIBUTES.items(): self.assertEqual(attributes[attribute_key], attribute_value) @patch("opentelemetry.resource.detector.azure.vm.urlopen") def test_windows(self, mock_urlopen): mock_urlopen.return_value.__enter__.return_value.read.return_value = WINDOWS_JSON attributes = AzureVMResourceDetector().detect().attributes for attribute_key, attribute_value in WINDOWS_ATTRIBUTES.items(): self.assertEqual(attributes[attribute_key], attribute_value) @patch("opentelemetry.resource.detector.azure.vm._can_ignore_vm_detect") @patch("opentelemetry.resource.detector.azure.vm.urlopen") def test_in_another_rp(self, mock_urlopen, detect_mock): mock_urlopen.return_value.__enter__.return_value.read.return_value = LINUX_JSON detect_mock.return_value = True attributes = AzureVMResourceDetector().detect().attributes self.assertEqual(attributes, {}) opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-containerid/000077500000000000000000000000001527070072300322215ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-containerid/LICENSE000066400000000000000000000261351527070072300332350ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. MANIFEST.rst000066400000000000000000000002501527070072300340770ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-containeridgraft src graft tests global-exclude *.pyc global-exclude *.pyo global-exclude __pycache__/* include CHANGELOG.md include MANIFEST.in include README.rst include LICENSEopentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-containerid/README.rst000066400000000000000000000017521527070072300337150ustar00rootroot00000000000000OpenTelemetry Resource detectors for containers ========================================================== |pypi| .. |pypi| image:: TODO :target: TODO This library provides custom resource detector for container platforms Installation ------------ :: pip install opentelemetry-resource-detector-containerid --------------------------- Usage example for `opentelemetry-resource-detector-containerid` .. code-block:: python from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.resource.detector.containerid import ( ContainerResourceDetector, ) from opentelemetry.sdk.resources import get_aggregated_resources trace.set_tracer_provider( TracerProvider( resource=get_aggregated_resources( [ ContainerResourceDetector(), ] ), ) ) References ---------- * `OpenTelemetry Project `_ pyproject.toml000066400000000000000000000027501527070072300350620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-containerid[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-resource-detector-containerid" dynamic = ["version"] description = "Container Resource Detector for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-sdk ~= 1.12", ] [project.entry-points.opentelemetry_resource_detector] containerid = "opentelemetry.resource.detector.containerid:ContainerResourceDetector" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/resource/opentelemetry-resource-detector-containerid" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/resource/detector/containerid/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-containerid/src/000077500000000000000000000000001527070072300330105ustar00rootroot00000000000000opentelemetry/000077500000000000000000000000001527070072300356255ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-containerid/srcresource/000077500000000000000000000000001527070072300374545ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-containerid/src/opentelemetrydetector/000077500000000000000000000000001527070072300412655ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-containerid/src/opentelemetry/resourcecontainerid/000077500000000000000000000000001527070072300435645ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-containerid/src/opentelemetry/resource/detector__init__.py000066400000000000000000000050741527070072300457030ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-containerid/src/opentelemetry/resource/detector/containerid# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import re from logging import getLogger from opentelemetry.sdk.resources import Resource, ResourceDetector from opentelemetry.semconv.resource import ResourceAttributes logger = getLogger(__name__) _DEFAULT_CGROUP_V1_PATH = "/proc/self/cgroup" _DEFAULT_CGROUP_V2_PATH = "/proc/self/mountinfo" _CONTAINER_ID_LENGTH = 64 def _get_container_id_v1(): container_id = None try: with open(_DEFAULT_CGROUP_V1_PATH, encoding="utf8") as container_info_file: for raw_line in container_info_file: line = raw_line.strip() match = re.search(r"^.*/(?:.*[-:])?([0-9a-f]+)(?:\.|\s*$)", line) if match is not None: container_id = match.group(1) break except FileNotFoundError as exception: logger.debug("Failed to get container id. Exception: %s", exception) return container_id def _get_container_id_v2(): container_id = None try: with open(_DEFAULT_CGROUP_V2_PATH, encoding="utf8") as container_info_file: for raw_line in container_info_file: line = raw_line.strip() if any(key_word in line for key_word in ["containers", "hostname"]): container_id_list = [id_ for id_ in line.split("/") if len(id_) == _CONTAINER_ID_LENGTH] if len(container_id_list) > 0: container_id = container_id_list[0] break except FileNotFoundError as exception: logger.debug("Failed to get container id. Exception: %s", exception) return container_id def _get_container_id(): return _get_container_id_v1() or _get_container_id_v2() class ContainerResourceDetector(ResourceDetector): """Detects container.id only available when app is running inside the docker container and return it in a Resource """ def detect(self) -> "Resource": try: container_id = _get_container_id() resource = Resource.get_empty() if container_id: resource = resource.merge(Resource({ResourceAttributes.CONTAINER_ID: container_id})) return resource # pylint: disable=broad-except except Exception as exception: logger.warning( "%s Resource Detection failed silently: %s", self.__class__.__name__, exception, ) if self.raise_on_error: raise exception return Resource.get_empty() version.py000066400000000000000000000001441527070072300456220ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-containerid/src/opentelemetry/resource/detector/containerid# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" test-requirements.txt000066400000000000000000000003531527070072300364040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-containeridasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 -e resource/opentelemetry-resource-detector-containerid opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-containerid/tests/000077500000000000000000000000001527070072300333635ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300354030ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-containerid/teststest_container.py000066400000000000000000000214101527070072300366750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resource-detector-containerid/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest.mock import mock_open, patch from opentelemetry import trace as trace_api from opentelemetry.resource.detector.containerid import ( ContainerResourceDetector, ) from opentelemetry.sdk.resources import get_aggregated_resources from opentelemetry.semconv.resource import ResourceAttributes from opentelemetry.test.test_base import TestBase from opentelemetry.util._importlib_metadata import entry_points MockContainerResourceAttributes = { ResourceAttributes.CONTAINER_ID: "7be92808767a667f35c8505cbf40d14e931ef6db5b0210329cf193b15ba9d605", } class ContainerResourceDetectorTest(TestBase): @patch( "builtins.open", new_callable=mock_open, read_data=f"""14:name=systemd:/docker/{MockContainerResourceAttributes[ResourceAttributes.CONTAINER_ID]} 13:rdma:/ 12:pids:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 11:hugetlb:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 10:net_prio:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 9:perf_event:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 8:net_cls:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 7:freezer:/docker/ 6:devices:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 5:memory:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 4:blkio:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 3:cpuacct:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 2:cpu:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 1:cpuset:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked """, ) def test_container_id_detect_from_cgroup_file(self, mock_cgroup_file): actual = ContainerResourceDetector().detect() self.assertDictEqual(actual.attributes.copy(), MockContainerResourceAttributes) @patch( "builtins.open", new_callable=mock_open, read_data=f"""0::/system.slice/docker-{MockContainerResourceAttributes[ResourceAttributes.CONTAINER_ID]}.scope """, ) def test_container_id_detect_from_cgroup_file_with_suffix(self, mock_cgroup_file): actual = ContainerResourceDetector().detect() self.assertDictEqual(actual.attributes.copy(), MockContainerResourceAttributes) @patch( "opentelemetry.resource.detector.containerid._get_container_id_v1", return_value=None, ) @patch( "builtins.open", new_callable=mock_open, read_data=f""" 608 607 0:183 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw 609 607 0:184 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 610 609 0:185 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 611 607 0:186 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro 612 611 0:29 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw 613 609 0:182 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw 614 609 0:187 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k 615 607 254:1 /docker/containers/{MockContainerResourceAttributes[ResourceAttributes.CONTAINER_ID]}/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw 616 607 254:1 /docker/containers/{MockContainerResourceAttributes[ResourceAttributes.CONTAINER_ID]}/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw 617 607 254:1 /docker/containers/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked/hosts /etc/hosts rw,relatime - ext4 /dev/vda1 rw 618 607 0:131 /Users/sankmeht/development/otel/opentelemetry-python /development/otel/opentelemetry-python rw,nosuid,nodev,relatime - fuse.grpcfuse grpcfuse rw,user_id=0,group_id=0,allow_other,max_read=1048576 619 607 0:131 /Users/sankmeht/development/otel/opentelemetry-python-contrib /development/otel/opentelemetry-python-contrib rw,nosuid,nodev,relatime - fuse.grpcfuse grpcfuse rw,user_id=0,group_id=0,allow_other,max_read=1048576 519 609 0:185 /0 /dev/console rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 520 608 0:183 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw 521 608 0:183 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw 522 608 0:183 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw 523 608 0:183 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw 524 608 0:183 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw 525 608 0:212 / /proc/acpi ro,relatime - tmpfs tmpfs ro 526 608 0:184 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 527 608 0:184 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 528 608 0:184 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 529 611 0:213 / /sys/firmware ro,relatime - tmpfs tmpfs ro """, ) def test_container_id_detect_from_mountinfo_file(self, mock_get_container_id_v1, mock_cgroup_file): actual = ContainerResourceDetector().detect() self.assertDictEqual(actual.attributes.copy(), MockContainerResourceAttributes) @patch( "opentelemetry.resource.detector.containerid._get_container_id_v1", return_value=None, ) @patch( "builtins.open", side_effect=FileNotFoundError, ) def test_cannot_read_mountinfo_file(self, mock_get_container_id_v1, mock_mountinfo_file): with self.assertLogs("opentelemetry.resource.detector.containerid", level="DEBUG") as cm: actual = ContainerResourceDetector().detect() self.assertFalse(actual.attributes.copy()) self.assertIn( "DEBUG:opentelemetry.resource.detector.containerid:Failed to get container id. Exception: ", cm.output, ) @patch( "opentelemetry.resource.detector.containerid._get_container_id_v2", return_value=None, ) @patch( "builtins.open", side_effect=FileNotFoundError, ) def test_cannot_read_cgroup_file(self, mock_get_container_id_v2, mock_cgroup_file): with self.assertLogs("opentelemetry.resource.detector.containerid", level="DEBUG") as cm: actual = ContainerResourceDetector().detect() self.assertFalse(actual.attributes.copy()) self.assertIn( "DEBUG:opentelemetry.resource.detector.containerid:Failed to get container id. Exception: ", cm.output, ) @patch( "opentelemetry.resource.detector.containerid._get_container_id", return_value=MockContainerResourceAttributes[ResourceAttributes.CONTAINER_ID], ) def test_container_id_as_span_attribute(self, mock_cgroup_file): tracer_provider, exporter = self.create_tracer_provider( resource=get_aggregated_resources([ContainerResourceDetector()]) ) tracer = tracer_provider.get_tracer(__name__) with tracer.start_as_current_span("test", kind=trace_api.SpanKind.SERVER) as _: pass span_list = exporter.get_finished_spans() self.assertEqual( span_list[0].resource.attributes["container.id"], MockContainerResourceAttributes[ResourceAttributes.CONTAINER_ID], ) @patch( "opentelemetry.resource.detector.containerid._get_container_id", return_value=MockContainerResourceAttributes[ResourceAttributes.CONTAINER_ID], ) def test_container_id_detect_from_cgroup(self, mock_get_container_id): actual = ContainerResourceDetector().detect() self.assertDictEqual(actual.attributes.copy(), MockContainerResourceAttributes) @patch( "opentelemetry.resource.detector.containerid._get_container_id_v1", return_value=None, ) @patch( "opentelemetry.resource.detector.containerid._get_container_id_v2", return_value=MockContainerResourceAttributes[ResourceAttributes.CONTAINER_ID], ) def test_container_id_detect_from_mount_info(self, mock_get_container_id_v1, mock_get_container_id_v2): actual = ContainerResourceDetector().detect() self.assertDictEqual(actual.attributes.copy(), MockContainerResourceAttributes) def test_container_id_entrypoint(self): (entrypoint,) = entry_points(group="opentelemetry_resource_detector", name="containerid") detector = entrypoint.load()() self.assertIsInstance(detector, ContainerResourceDetector) opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/000077500000000000000000000000001527070072300304165ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/.changelog/000077500000000000000000000000001527070072300324235ustar00rootroot00000000000000.gitignore000066400000000000000000000000141527070072300343270ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/.changelog!.gitignore 4898.deprecated000066400000000000000000000001771527070072300350070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/.changelogDeprecate the `get_monitored_resource` function in `opentelemetry-resourcedetector-gcp` as it does not belong in this package. opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/CHANGELOG.md000066400000000000000000000022471527070072300322340ustar00rootroot00000000000000## Version 1.14.0 (2026-07-24) ### Changed - Update package namespace to `opentelemetry.resourcedetector.gcp_resource_detector`. ([#4861](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4861)) ### Fixed - Add back `gcp_resource_detector` entry point name for backward compatibility. ([#4865](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4865)) ## Version 1.13.0 (2026-07-22) ### Added - Moved the Google Cloud resource detector package from the opentelemetry-operations-python repository to the opentelemetry-python-contrib repo. ([#4749](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4749)) # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/LICENSE000066400000000000000000000261351527070072300314320ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/MANIFEST.rst000066400000000000000000000002511527070072300323540ustar00rootroot00000000000000graft src graft tests global-exclude *.pyc global-exclude *.pyo global-exclude __pycache__/* include CHANGELOG.md include MANIFEST.in include README.rst include LICENSE opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/README.rst000066400000000000000000000016431527070072300321110ustar00rootroot00000000000000OpenTelemetry Google Cloud Resource Detector ============================================ |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-resourcedetector-gcp.svg :target: https://pypi.org/project/opentelemetry-resourcedetector-gcp/ This library provides support for detecting GCP resources like GCE, GKE, etc. Installation ------------ :: pip install opentelemetry-resourcedetector-gcp Usage ----- The ``GoogleCloudResourceDetector`` is automatically used by the OpenTelemetry SDK when it is installed in the python environment. If you are setting resource detectors through the environment variable ``OTEL_EXPERIMENTAL_RESOURCE_DETECTORS``, you should use the value ``gcp``, for the ``GoogleCloudResourceDetector`` to be installed. References ---------- * `GCP Open Telemetry Docs `_ * `OpenTelemetry Project `_ opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/pyproject.toml000066400000000000000000000045461527070072300333430ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-resourcedetector-gcp" dynamic = ["version"] description = "Google Cloud Resource Detector for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.30", "opentelemetry-sdk ~= 1.30", "requests ~= 2.24", "typing-extensions >= 4.5.0", ] [project.entry-points.opentelemetry_resource_detector] gcp = "opentelemetry.resourcedetector.gcp_resource_detector:GoogleCloudResourceDetector" gcp_resource_detector = "opentelemetry.resourcedetector.gcp_resource_detector:GoogleCloudResourceDetector" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/resource/opentelemetry-resourcedetector-gcp" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/resourcedetector/gcp_resource_detector/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] [tool.towncrier] directory = ".changelog" filename = "CHANGELOG.md" start_string = "\n" template = "../../scripts/changelog_template.j2" issue_format = "[#{issue}](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/{issue})" wrap = true issue_pattern = "^(\\d+)" [[tool.towncrier.type]] directory = "added" name = "Added" showcontent = true [[tool.towncrier.type]] directory = "changed" name = "Changed" showcontent = true [[tool.towncrier.type]] directory = "deprecated" name = "Deprecated" showcontent = true [[tool.towncrier.type]] directory = "removed" name = "Removed" showcontent = true [[tool.towncrier.type]] directory = "fixed" name = "Fixed" showcontent = true requirements.latest.txt000066400000000000000000000005101527070072300351120ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcpasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 requests==2.33.1 syrupy==4.6.1 # test with the latest version of opentelemetry-api, sdk, and semantic conventions -e resource/opentelemetry-resourcedetector-gcp requirements.oldest.txt000066400000000000000000000006051527070072300351150ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcpasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 requests==2.33.1 syrupy==4.6.1 opentelemetry-api==1.30 # when updating, also update in pyproject.toml opentelemetry-sdk==1.30 # when updating, also update in pyproject.toml -e resource/opentelemetry-resourcedetector-gcp opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/src/000077500000000000000000000000001527070072300312055ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/000077500000000000000000000000001527070072300341015ustar00rootroot00000000000000resourcedetector/000077500000000000000000000000001527070072300374035ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/src/opentelemetrygcp_resource_detector/000077500000000000000000000000001527070072300437545ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector__init__.py000066400000000000000000000103371527070072300460710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Mapping from opentelemetry.resourcedetector.gcp_resource_detector import ( _faas, _gae, _gce, _gke, _metadata, ) from opentelemetry.resourcedetector.gcp_resource_detector._constants import ( ResourceAttributes, ) from opentelemetry.sdk.resources import Resource, ResourceDetector from opentelemetry.util.types import AttributeValue class GoogleCloudResourceDetector(ResourceDetector): def detect(self) -> Resource: # pylint: disable=too-many-return-statements if not _metadata.is_available(): return Resource.get_empty() if _gke.on_gke(): return _gke_resource() if _faas.on_cloud_functions(): return _cloud_functions_resource() if _faas.on_cloud_run(): return _cloud_run_resource() if _gae.on_app_engine(): return _gae_resource() if _gce.on_gce(): return _gce_resource() return Resource.get_empty() def _gke_resource() -> Resource: zone_or_region = _gke.availability_zone_or_region() zone_or_region_key = ( ResourceAttributes.CLOUD_AVAILABILITY_ZONE if zone_or_region.type == "zone" else ResourceAttributes.CLOUD_REGION ) return _make_resource( { ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_KUBERNETES_ENGINE, zone_or_region_key: zone_or_region.value, ResourceAttributes.K8S_CLUSTER_NAME: _gke.cluster_name(), ResourceAttributes.HOST_ID: _gke.host_id(), } ) def _gce_resource() -> Resource: zone_and_region = _gce.availability_zone_and_region() return _make_resource( { ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_COMPUTE_ENGINE, ResourceAttributes.CLOUD_AVAILABILITY_ZONE: zone_and_region.zone, ResourceAttributes.CLOUD_REGION: zone_and_region.region, ResourceAttributes.HOST_TYPE: _gce.host_type(), ResourceAttributes.HOST_ID: _gce.host_id(), ResourceAttributes.HOST_NAME: _gce.host_name(), } ) def _cloud_run_resource() -> Resource: return _make_resource( { ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_CLOUD_RUN, ResourceAttributes.FAAS_NAME: _faas.faas_name(), ResourceAttributes.FAAS_VERSION: _faas.faas_version(), ResourceAttributes.FAAS_INSTANCE: _faas.faas_instance(), ResourceAttributes.CLOUD_REGION: _faas.faas_cloud_region(), } ) def _cloud_functions_resource() -> Resource: return _make_resource( { ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_CLOUD_FUNCTIONS, ResourceAttributes.FAAS_NAME: _faas.faas_name(), ResourceAttributes.FAAS_VERSION: _faas.faas_version(), ResourceAttributes.FAAS_INSTANCE: _faas.faas_instance(), ResourceAttributes.CLOUD_REGION: _faas.faas_cloud_region(), } ) def _gae_resource() -> Resource: if _gae.on_app_engine_standard(): zone = _gae.standard_availability_zone() region = _gae.standard_cloud_region() else: zone_and_region = _gae.flex_availability_zone_and_region() zone = zone_and_region.zone region = zone_and_region.region faas_name = _gae.service_name() faas_version = _gae.service_version() faas_instance = _gae.service_instance() return _make_resource( { ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_APP_ENGINE, ResourceAttributes.FAAS_NAME: faas_name, ResourceAttributes.FAAS_VERSION: faas_version, ResourceAttributes.FAAS_INSTANCE: faas_instance, ResourceAttributes.CLOUD_AVAILABILITY_ZONE: zone, ResourceAttributes.CLOUD_REGION: region, } ) def _make_resource(attrs: Mapping[str, AttributeValue]) -> Resource: return Resource( { ResourceAttributes.CLOUD_PROVIDER: "gcp", ResourceAttributes.CLOUD_ACCOUNT_ID: _metadata.get_metadata()["project"]["projectId"], **attrs, } ) __all__ = ["GoogleCloudResourceDetector"] _constants.py000066400000000000000000000035321527070072300465040ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # TODO: use opentelemetry-semantic-conventions package for these constants once it has # stabilized. Right now, pinning an unstable version would cause dependency conflicts for # users so these are copied in. class ResourceAttributes: AWS_EC2 = "aws_ec2" CLOUD_ACCOUNT_ID = "cloud.account.id" CLOUD_AVAILABILITY_ZONE = "cloud.availability_zone" CLOUD_PLATFORM_KEY = "cloud.platform" CLOUD_PROVIDER = "cloud.provider" CLOUD_REGION = "cloud.region" FAAS_INSTANCE = "faas.instance" FAAS_NAME = "faas.name" FAAS_VERSION = "faas.version" GCP_APP_ENGINE = "gcp_app_engine" GCP_CLOUD_FUNCTIONS = "gcp_cloud_functions" GCP_CLOUD_RUN = "gcp_cloud_run" GCP_COMPUTE_ENGINE = "gcp_compute_engine" GCP_KUBERNETES_ENGINE = "gcp_kubernetes_engine" HOST_ID = "host.id" HOST_NAME = "host.name" HOST_TYPE = "host.type" K8S_CLUSTER_NAME = "k8s.cluster.name" K8S_CONTAINER_NAME = "k8s.container.name" K8S_NAMESPACE_NAME = "k8s.namespace.name" K8S_NODE_NAME = "k8s.node.name" K8S_POD_NAME = "k8s.pod.name" SERVICE_INSTANCE_ID = "service.instance.id" SERVICE_NAME = "service.name" SERVICE_NAMESPACE = "service.namespace" AWS_ACCOUNT = "aws_account" AWS_EC2_INSTANCE = "aws_ec2_instance" CLUSTER_NAME = "cluster_name" CONTAINER_NAME = "container_name" GCE_INSTANCE = "gce_instance" GENERIC_NODE = "generic_node" GENERIC_TASK = "generic_task" INSTANCE_ID = "instance_id" JOB = "job" K8S_CLUSTER = "k8s_cluster" K8S_CONTAINER = "k8s_container" K8S_NODE = "k8s_node" K8S_POD = "k8s_pod" LOCATION = "location" NAMESPACE = "namespace" NAMESPACE_NAME = "namespace_name" NODE_ID = "node_id" NODE_NAME = "node_name" POD_NAME = "pod_name" REGION = "region" TASK_ID = "task_id" ZONE = "zone" UNKNOWN_SERVICE_PREFIX = "unknown_service" _faas.py000066400000000000000000000025161527070072300454030ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Implementation in this file copied from # https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/v1.8.0/detectors/gcp/faas.go import os from opentelemetry.resourcedetector.gcp_resource_detector import _metadata _CLOUD_RUN_CONFIG_ENV = "K_CONFIGURATION" _CLOUD_FUNCTION_TARGET_ENV = "FUNCTION_TARGET" _FAAS_SERVICE_ENV = "K_SERVICE" _FAAS_REVISION_ENV = "K_REVISION" def on_cloud_run() -> bool: return _CLOUD_RUN_CONFIG_ENV in os.environ def on_cloud_functions() -> bool: return _CLOUD_FUNCTION_TARGET_ENV in os.environ def faas_name() -> str: """The name of the Cloud Run or Cloud Function. Check that on_cloud_run() or on_cloud_functions() is true before calling this, or it may throw exceptions. """ return os.environ[_FAAS_SERVICE_ENV] def faas_version() -> str: """The version/revision of the Cloud Run or Cloud Function. Check that on_cloud_run() or on_cloud_functions() is true before calling this, or it may throw exceptions. """ return os.environ[_FAAS_REVISION_ENV] def faas_instance() -> str: return str(_metadata.get_metadata()["instance"]["id"]) def faas_cloud_region() -> str: region = _metadata.get_metadata()["instance"]["region"] return region[region.rfind("/") + 1 :] _gae.py000066400000000000000000000042511527070072300452230ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Implementation in this file copied from # https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/v1.8.0/detectors/gcp/app_engine.go import os from opentelemetry.resourcedetector.gcp_resource_detector import ( _faas, _gce, _metadata, ) _GAE_SERVICE_ENV = "GAE_SERVICE" _GAE_VERSION_ENV = "GAE_VERSION" _GAE_INSTANCE_ENV = "GAE_INSTANCE" _GAE_ENV = "GAE_ENV" _GAE_STANDARD = "standard" def on_app_engine_standard() -> bool: return os.environ.get(_GAE_ENV) == _GAE_STANDARD def on_app_engine() -> bool: return _GAE_SERVICE_ENV in os.environ def service_name() -> str: """The service name of the app engine service. Check that ``on_app_engine()`` is true before calling this, or it may throw exceptions. """ return os.environ[_GAE_SERVICE_ENV] def service_version() -> str: """The service version of the app engine service. Check that ``on_app_engine()`` is true before calling this, or it may throw exceptions. """ return os.environ[_GAE_VERSION_ENV] def service_instance() -> str: """The service instance of the app engine service. Check that ``on_app_engine()`` is true before calling this, or it may throw exceptions. """ return os.environ[_GAE_INSTANCE_ENV] def flex_availability_zone_and_region() -> _gce.ZoneAndRegion: """The zone and region in which this program is running. Check that ``on_app_engine()`` is true before calling this, or it may throw exceptions. """ return _gce.availability_zone_and_region() def standard_availability_zone() -> str: """The zone the app engine service is running in. Check that ``on_app_engine_standard()`` is true before calling this, or it may throw exceptions. """ zone = _metadata.get_metadata()["instance"]["zone"] # zone is of the form "projects/233510669999/zones/us15" return zone[zone.rfind("/") + 1 :] def standard_cloud_region() -> str: """The region the app engine service is running in. Check that ``on_app_engine_standard()`` is true before calling this, or it may throw exceptions. """ return _faas.faas_cloud_region() _gce.py000066400000000000000000000027721527070072300452330ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import re from dataclasses import dataclass from opentelemetry.resourcedetector.gcp_resource_detector import _metadata # Format described in # https://cloud.google.com/compute/docs/metadata/default-metadata-values#vm_instance_metadata _ZONE_REGION_RE = re.compile(r"projects\/\d+\/zones\/(?P(?P\w+-\w+)-\w+)") _logger = logging.getLogger(__name__) def on_gce() -> bool: try: _metadata.get_metadata()["instance"]["machineType"] except (_metadata.MetadataAccessException, KeyError): _logger.debug( "Could not fetch metadata attribute instance/machineType, assuming not on GCE.", exc_info=True, ) return False return True def host_type() -> str: return _metadata.get_metadata()["instance"]["machineType"] def host_id() -> str: return str(_metadata.get_metadata()["instance"]["id"]) def host_name() -> str: return _metadata.get_metadata()["instance"]["name"] @dataclass class ZoneAndRegion: zone: str region: str def availability_zone_and_region() -> ZoneAndRegion: full_zone = _metadata.get_metadata()["instance"]["zone"] match = _ZONE_REGION_RE.search(full_zone) if not match: raise ValueError( f"zone was not in the expected format: projects/PROJECT_NUM/zones/COUNTRY-REGION-ZONE. Got {full_zone}" ) return ZoneAndRegion(zone=match.group("zone"), region=match.group("region")) _gke.py000066400000000000000000000021101527070072300452250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os from dataclasses import dataclass from typing import Literal from opentelemetry.resourcedetector.gcp_resource_detector import ( _gce, _metadata, ) KUBERNETES_SERVICE_HOST_ENV = "KUBERNETES_SERVICE_HOST" def on_gke() -> bool: return os.environ.get(KUBERNETES_SERVICE_HOST_ENV) is not None def host_id() -> str: return _gce.host_id() def cluster_name() -> str: return _metadata.get_metadata()["instance"]["attributes"]["cluster-name"] @dataclass class ZoneOrRegion: type: Literal["zone", "region"] value: str def availability_zone_or_region() -> ZoneOrRegion: cluster_location = _metadata.get_metadata()["instance"]["attributes"]["cluster-location"] hyphen_count = cluster_location.count("-") if hyphen_count == 1: return ZoneOrRegion(type="region", value=cluster_location) if hyphen_count == 2: return ZoneOrRegion(type="zone", value=cluster_location) raise ValueError(f"unrecognized format for cluster location: {cluster_location}") _mapping.py000066400000000000000000000166341527070072300461320ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json from collections.abc import Mapping from dataclasses import dataclass from typing_extensions import deprecated from opentelemetry.resourcedetector.gcp_resource_detector import _constants from opentelemetry.resourcedetector.gcp_resource_detector._constants import ( ResourceAttributes, ) from opentelemetry.sdk.resources import Attributes, Resource class MapConfig: otel_keys: tuple[str, ...] """ OTel resource keys to try and populate the resource label from. For entries with multiple OTel resource keys, the keys' values will be coalesced in order until there is a non-empty value. """ fallback: str """If none of the otelKeys are present in the Resource, fallback to this literal value""" def __init__(self, *otel_keys: str, fallback: str = ""): self.otel_keys = otel_keys self.fallback = fallback # Mappings of GCM resource label keys onto mapping config from OTel resource for a given # monitored resource type. Copied from Go impl: # https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/v1.8.0/internal/resourcemapping/resourcemapping.go#L51 MAPPINGS = { _constants.GCE_INSTANCE: { _constants.ZONE: MapConfig(ResourceAttributes.CLOUD_AVAILABILITY_ZONE), _constants.INSTANCE_ID: MapConfig(ResourceAttributes.HOST_ID), }, _constants.K8S_CONTAINER: { _constants.LOCATION: MapConfig( ResourceAttributes.CLOUD_AVAILABILITY_ZONE, ResourceAttributes.CLOUD_REGION, ), _constants.CLUSTER_NAME: MapConfig(ResourceAttributes.K8S_CLUSTER_NAME), _constants.NAMESPACE_NAME: MapConfig(ResourceAttributes.K8S_NAMESPACE_NAME), _constants.POD_NAME: MapConfig(ResourceAttributes.K8S_POD_NAME), _constants.CONTAINER_NAME: MapConfig(ResourceAttributes.K8S_CONTAINER_NAME), }, _constants.K8S_POD: { _constants.LOCATION: MapConfig( ResourceAttributes.CLOUD_AVAILABILITY_ZONE, ResourceAttributes.CLOUD_REGION, ), _constants.CLUSTER_NAME: MapConfig(ResourceAttributes.K8S_CLUSTER_NAME), _constants.NAMESPACE_NAME: MapConfig(ResourceAttributes.K8S_NAMESPACE_NAME), _constants.POD_NAME: MapConfig(ResourceAttributes.K8S_POD_NAME), }, _constants.K8S_NODE: { _constants.LOCATION: MapConfig( ResourceAttributes.CLOUD_AVAILABILITY_ZONE, ResourceAttributes.CLOUD_REGION, ), _constants.CLUSTER_NAME: MapConfig(ResourceAttributes.K8S_CLUSTER_NAME), _constants.NODE_NAME: MapConfig(ResourceAttributes.K8S_NODE_NAME), }, _constants.K8S_CLUSTER: { _constants.LOCATION: MapConfig( ResourceAttributes.CLOUD_AVAILABILITY_ZONE, ResourceAttributes.CLOUD_REGION, ), _constants.CLUSTER_NAME: MapConfig(ResourceAttributes.K8S_CLUSTER_NAME), }, _constants.AWS_EC2_INSTANCE: { _constants.INSTANCE_ID: MapConfig(ResourceAttributes.HOST_ID), _constants.REGION: MapConfig( ResourceAttributes.CLOUD_AVAILABILITY_ZONE, ResourceAttributes.CLOUD_REGION, ), _constants.AWS_ACCOUNT: MapConfig(ResourceAttributes.CLOUD_ACCOUNT_ID), }, _constants.GENERIC_TASK: { _constants.LOCATION: MapConfig( ResourceAttributes.CLOUD_AVAILABILITY_ZONE, ResourceAttributes.CLOUD_REGION, fallback="global", ), _constants.NAMESPACE: MapConfig(ResourceAttributes.SERVICE_NAMESPACE), _constants.JOB: MapConfig( ResourceAttributes.SERVICE_NAME, ResourceAttributes.FAAS_NAME, ), _constants.TASK_ID: MapConfig( ResourceAttributes.SERVICE_INSTANCE_ID, ResourceAttributes.FAAS_INSTANCE, ), }, _constants.GENERIC_NODE: { _constants.LOCATION: MapConfig( ResourceAttributes.CLOUD_AVAILABILITY_ZONE, ResourceAttributes.CLOUD_REGION, fallback="global", ), _constants.NAMESPACE: MapConfig(ResourceAttributes.SERVICE_NAMESPACE), _constants.NODE_ID: MapConfig(ResourceAttributes.HOST_ID, ResourceAttributes.HOST_NAME), }, } @dataclass class MonitoredResourceData: """Dataclass representing a protobuf monitored resource. Make sure to convert to a protobuf if needed.""" type: str labels: Mapping[str, str] @deprecated( "get_monitored_resource is deprecated and will be removed in a future release. Deprecated since version 1.15.0." ) def get_monitored_resource( resource: Resource, ) -> MonitoredResourceData | None: """Add Google resource specific information (e.g. instance id, region). See https://cloud.google.com/monitoring/custom-metrics/creating-metrics#custom-metric-resources for supported types Args: resource: OTel resource """ attrs = resource.attributes platform = attrs.get(ResourceAttributes.CLOUD_PLATFORM_KEY) if platform == ResourceAttributes.GCP_COMPUTE_ENGINE: mr = _create_monitored_resource(_constants.GCE_INSTANCE, attrs) elif platform == ResourceAttributes.GCP_KUBERNETES_ENGINE: if ResourceAttributes.K8S_CONTAINER_NAME in attrs: mr = _create_monitored_resource(_constants.K8S_CONTAINER, attrs) elif ResourceAttributes.K8S_POD_NAME in attrs: mr = _create_monitored_resource(_constants.K8S_POD, attrs) elif ResourceAttributes.K8S_NODE_NAME in attrs: mr = _create_monitored_resource(_constants.K8S_NODE, attrs) else: mr = _create_monitored_resource(_constants.K8S_CLUSTER, attrs) elif platform == ResourceAttributes.AWS_EC2: mr = _create_monitored_resource(_constants.AWS_EC2_INSTANCE, attrs) else: # fallback to generic_task if (ResourceAttributes.SERVICE_NAME in attrs or ResourceAttributes.FAAS_NAME in attrs) and ( ResourceAttributes.SERVICE_INSTANCE_ID in attrs or ResourceAttributes.FAAS_INSTANCE in attrs ): mr = _create_monitored_resource(_constants.GENERIC_TASK, attrs) else: mr = _create_monitored_resource(_constants.GENERIC_NODE, attrs) return mr def _create_monitored_resource(monitored_resource_type: str, resource_attrs: Attributes) -> MonitoredResourceData: mapping = MAPPINGS[monitored_resource_type] labels: dict[str, str] = {} for mr_key, map_config in mapping.items(): mr_value = None for otel_key in map_config.otel_keys: if otel_key in resource_attrs and not str(resource_attrs[otel_key]).startswith( _constants.UNKNOWN_SERVICE_PREFIX ): mr_value = resource_attrs[otel_key] break if mr_value is None and ResourceAttributes.SERVICE_NAME in map_config.otel_keys: # The service name started with unknown_service, and was ignored above. mr_value = resource_attrs.get(ResourceAttributes.SERVICE_NAME) if mr_value is None: mr_value = map_config.fallback # OTel attribute values can be any of str, bool, int, float, or Sequence of any of # them. Encode any non-strings as json string if not isinstance(mr_value, str): mr_value = json.dumps(mr_value, sort_keys=True, indent=None, separators=(",", ":")) labels[mr_key] = mr_value return MonitoredResourceData(type=monitored_resource_type, labels=labels) _metadata.py000066400000000000000000000036641527070072300462560ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging from functools import cache from typing import TypedDict import requests _GCP_METADATA_URL = "http://metadata.google.internal/computeMetadata/v1/" _INSTANCE = "instance" _RECURSIVE_PARAMS = {"recursive": "true"} _GCP_METADATA_URL_HEADER = {"Metadata-Flavor": "Google"} # Use a shorter timeout for connection so we won't block much if it's unreachable _TIMEOUT = (2, 5) _logger = logging.getLogger(__name__) class Project(TypedDict): projectId: str Attributes = TypedDict("Attributes", {"cluster-location": str, "cluster-name": str}, total=False) class Instance(TypedDict): attributes: Attributes # id can be an integer on GCE VMs or a string on other environments id: int | str machineType: str name: str region: str zone: str class Metadata(TypedDict): instance: Instance project: Project class MetadataAccessException(Exception): pass @cache def get_metadata() -> Metadata: """Get all instance and project metadata from the metadata server Cached for the lifetime of the process. """ try: res = requests.get( f"{_GCP_METADATA_URL}", params=_RECURSIVE_PARAMS, headers=_GCP_METADATA_URL_HEADER, timeout=_TIMEOUT, ) res.raise_for_status() all_metadata = res.json() except requests.RequestException as err: raise MetadataAccessException() from err return all_metadata @cache def is_available() -> bool: try: requests.get( f"{_GCP_METADATA_URL}{_INSTANCE}/", headers=_GCP_METADATA_URL_HEADER, timeout=_TIMEOUT, ).raise_for_status() except requests.RequestException: _logger.debug( "Failed to make request to metadata server, assuming it's not available", exc_info=True, ) return False return True version.py000066400000000000000000000001501527070072300460070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "1.15.0.dev" opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/tests/000077500000000000000000000000001527070072300315605ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/tests/__snapshots__/000077500000000000000000000000001527070072300343765ustar00rootroot00000000000000test_gcp_resource_detector.ambr000066400000000000000000000044051527070072300425750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/tests/__snapshots__# serializer version: 1 # name: test_detects_cloud_functions dict({ 'cloud.account.id': 'fakeProject', 'cloud.platform': 'gcp_cloud_functions', 'cloud.provider': 'gcp', 'cloud.region': 'us-east4', 'faas.instance': '0087244a', 'faas.name': 'fake-service', 'faas.version': 'fake-revision', }) # --- # name: test_detects_cloud_run dict({ 'cloud.account.id': 'fakeProject', 'cloud.platform': 'gcp_cloud_run', 'cloud.provider': 'gcp', 'cloud.region': 'us-east4', 'faas.instance': '0087244a', 'faas.name': 'fake-service', 'faas.version': 'fake-revision', }) # --- # name: test_detects_empty_as_fallback dict({ }) # --- # name: test_detects_empty_when_not_available dict({ }) # --- # name: test_detects_gae_flex dict({ 'cloud.account.id': 'fakeProject', 'cloud.availability_zone': 'us-east4-b', 'cloud.platform': 'gcp_app_engine', 'cloud.provider': 'gcp', 'cloud.region': 'us-east4', 'faas.instance': 'fake-instance', 'faas.name': 'fake-service', 'faas.version': 'fake-version', }) # --- # name: test_detects_gae_standard dict({ 'cloud.account.id': 'fakeProject', 'cloud.availability_zone': 'us-east4-b', 'cloud.platform': 'gcp_app_engine', 'cloud.provider': 'gcp', 'cloud.region': 'us-east4', 'faas.instance': 'fake-instance', 'faas.name': 'fake-service', 'faas.version': 'fake-version', }) # --- # name: test_detects_gce dict({ 'cloud.account.id': 'fakeProject', 'cloud.availability_zone': 'us-east4-b', 'cloud.platform': 'gcp_compute_engine', 'cloud.provider': 'gcp', 'cloud.region': 'us-east4', 'host.id': '0087244a', 'host.name': 'fakeName', 'host.type': 'fakeMachineType', }) # --- # name: test_detects_gke[regional] dict({ 'cloud.account.id': 'fakeProject', 'cloud.platform': 'gcp_kubernetes_engine', 'cloud.provider': 'gcp', 'cloud.region': 'us-east4', 'host.id': '12345', 'k8s.cluster.name': 'fakeClusterName', }) # --- # name: test_detects_gke[zonal] dict({ 'cloud.account.id': 'fakeProject', 'cloud.availability_zone': 'us-east4-b', 'cloud.platform': 'gcp_kubernetes_engine', 'cloud.provider': 'gcp', 'host.id': '12345', 'k8s.cluster.name': 'fakeClusterName', }) # --- test_mapping.ambr000066400000000000000000000122041527070072300376530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/tests/__snapshots__# serializer version: 1 # name: test_get_monitored_resource[aws ec2 region fallback] dict({ 'labels': dict({ 'aws_account': 'myawsaccount', 'instance_id': 'myhostid', 'region': 'myregion', }), 'type': 'aws_ec2_instance', }) # --- # name: test_get_monitored_resource[aws ec2] dict({ 'labels': dict({ 'aws_account': 'myawsaccount', 'instance_id': 'myhostid', 'region': 'myavailzone', }), 'type': 'aws_ec2_instance', }) # --- # name: test_get_monitored_resource[empty] dict({ 'labels': dict({ 'location': 'global', 'namespace': '', 'node_id': '', }), 'type': 'generic_node', }) # --- # name: test_get_monitored_resource[fallback generic node] dict({ 'labels': dict({ 'location': 'global', 'namespace': '', 'node_id': '', }), 'type': 'generic_node', }) # --- # name: test_get_monitored_resource[gce instance] dict({ 'labels': dict({ 'instance_id': 'myhost', 'zone': 'foo', }), 'type': 'gce_instance', }) # --- # name: test_get_monitored_resource[generic node fallback global] dict({ 'labels': dict({ 'location': 'global', 'namespace': 'servicens', 'node_id': 'hostid', }), 'type': 'generic_node', }) # --- # name: test_get_monitored_resource[generic node fallback host name] dict({ 'labels': dict({ 'location': 'global', 'namespace': 'servicens', 'node_id': 'hostname', }), 'type': 'generic_node', }) # --- # name: test_get_monitored_resource[generic node fallback region] dict({ 'labels': dict({ 'location': 'myregion', 'namespace': 'servicens', 'node_id': 'hostid', }), 'type': 'generic_node', }) # --- # name: test_get_monitored_resource[generic node] dict({ 'labels': dict({ 'location': 'myavailzone', 'namespace': 'servicens', 'node_id': 'hostid', }), 'type': 'generic_node', }) # --- # name: test_get_monitored_resource[generic task faas fallback] dict({ 'labels': dict({ 'job': 'unknown_service', 'location': 'myregion', 'namespace': 'servicens', 'task_id': 'faasinstance', }), 'type': 'generic_task', }) # --- # name: test_get_monitored_resource[generic task faas] dict({ 'labels': dict({ 'job': 'faasname', 'location': 'myregion', 'namespace': 'servicens', 'task_id': 'faasinstance', }), 'type': 'generic_task', }) # --- # name: test_get_monitored_resource[generic task fallback global] dict({ 'labels': dict({ 'job': 'servicename', 'location': 'global', 'namespace': 'servicens', 'task_id': 'serviceinstanceid', }), 'type': 'generic_task', }) # --- # name: test_get_monitored_resource[generic task fallback region] dict({ 'labels': dict({ 'job': 'servicename', 'location': 'myregion', 'namespace': 'servicens', 'task_id': 'serviceinstanceid', }), 'type': 'generic_task', }) # --- # name: test_get_monitored_resource[generic task] dict({ 'labels': dict({ 'job': 'servicename', 'location': 'myavailzone', 'namespace': 'servicens', 'task_id': 'serviceinstanceid', }), 'type': 'generic_task', }) # --- # name: test_get_monitored_resource[k8s cluster region fallback] dict({ 'labels': dict({ 'cluster_name': 'mycluster', 'location': 'myregion', }), 'type': 'k8s_cluster', }) # --- # name: test_get_monitored_resource[k8s cluster] dict({ 'labels': dict({ 'cluster_name': 'mycluster', 'location': 'myavailzone', }), 'type': 'k8s_cluster', }) # --- # name: test_get_monitored_resource[k8s container region fallback] dict({ 'labels': dict({ 'cluster_name': 'mycluster', 'container_name': 'mycontainer', 'location': 'myregion', 'namespace_name': 'myns', 'pod_name': 'mypod', }), 'type': 'k8s_container', }) # --- # name: test_get_monitored_resource[k8s container] dict({ 'labels': dict({ 'cluster_name': 'mycluster', 'container_name': 'mycontainer', 'location': 'myavailzone', 'namespace_name': 'myns', 'pod_name': 'mypod', }), 'type': 'k8s_container', }) # --- # name: test_get_monitored_resource[k8s node region fallback] dict({ 'labels': dict({ 'cluster_name': 'mycluster', 'location': 'myregion', 'node_name': 'mynode', }), 'type': 'k8s_node', }) # --- # name: test_get_monitored_resource[k8s node] dict({ 'labels': dict({ 'cluster_name': 'mycluster', 'location': 'myavailzone', 'node_name': 'mynode', }), 'type': 'k8s_node', }) # --- # name: test_get_monitored_resource[k8s pod region fallback] dict({ 'labels': dict({ 'cluster_name': 'mycluster', 'location': 'myregion', 'namespace_name': 'myns', 'pod_name': 'mypod', }), 'type': 'k8s_pod', }) # --- # name: test_get_monitored_resource[k8s pod] dict({ 'labels': dict({ 'cluster_name': 'mycluster', 'location': 'myavailzone', 'namespace_name': 'myns', 'pod_name': 'mypod', }), 'type': 'k8s_pod', }) # --- opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/tests/conftest.py000066400000000000000000000006451527070072300337640ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest.mock import MagicMock import pytest from opentelemetry.resourcedetector.gcp_resource_detector import _metadata @pytest.fixture(name="fake_get_metadata") def fixture_fake_get_metadata(monkeypatch: pytest.MonkeyPatch) -> MagicMock: mock = MagicMock() monkeypatch.setattr(_metadata, "get_metadata", mock) return mock opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/tests/test_faas.py000066400000000000000000000031421527070072300341030ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest.mock import MagicMock import pytest from opentelemetry.resourcedetector.gcp_resource_detector import _faas # Reset stuff before every test # pylint: disable=unused-argument @pytest.fixture(autouse=True) def autouse(fake_get_metadata): pass def test_detects_on_cloud_run(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("K_CONFIGURATION", "fake-configuration") assert _faas.on_cloud_run() def test_detects_not_on_cloud_run() -> None: assert not _faas.on_cloud_run() def test_detects_on_cloud_functions(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("FUNCTION_TARGET", "fake-function-target") assert _faas.on_cloud_functions() def test_detects_not_on_cloud_functions() -> None: assert not _faas.on_cloud_functions() def test_detects_faas_name(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("K_SERVICE", "fake-service") assert _faas.faas_name() == "fake-service" def test_detects_faas_version(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("K_REVISION", "fake-revision") assert _faas.faas_version() == "fake-revision" def test_detects_faas_instance(fake_get_metadata: MagicMock) -> None: fake_get_metadata.return_value = {"instance": {"id": "0087244a"}} assert _faas.faas_instance() == "0087244a" def test_detects_faas_region(fake_get_metadata: MagicMock) -> None: fake_get_metadata.return_value = {"instance": {"region": "projects/233510669999/regions/us-east4"}} assert _faas.faas_cloud_region() == "us-east4" opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/tests/test_gae.py000066400000000000000000000044151527070072300337310ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest.mock import MagicMock import pytest from opentelemetry.resourcedetector.gcp_resource_detector import _gae # Reset stuff before every test # pylint: disable=unused-argument @pytest.fixture(autouse=True) def autouse(fake_get_metadata): pass def test_detects_on_gae(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("GAE_SERVICE", "fake-service") assert _gae.on_app_engine() def test_detects_not_on_gae() -> None: assert not _gae.on_app_engine() def test_detects_on_gae_standard(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("GAE_ENV", "standard") assert _gae.on_app_engine_standard() def test_detects_not_on_gae_standard(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("GAE_SERVICE", "fake-service") assert _gae.on_app_engine() assert not _gae.on_app_engine_standard() def test_detects_gae_service_name(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("GAE_SERVICE", "fake-service") assert _gae.service_name() == "fake-service" def test_detects_gae_service_version(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("GAE_VERSION", "fake-version") assert _gae.service_version() == "fake-version" def test_detects_gae_service_instance(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("GAE_INSTANCE", "fake-instance") assert _gae.service_instance() == "fake-instance" def test_detects_gae_flex_zone_and_region( fake_get_metadata: MagicMock, ) -> None: fake_get_metadata.return_value = {"instance": {"zone": "projects/233510669999/zones/us-east4-b"}} zone_and_region = _gae.flex_availability_zone_and_region() assert zone_and_region.zone == "us-east4-b" assert zone_and_region.region == "us-east4" def test_gae_standard_zone( fake_get_metadata: MagicMock, ) -> None: fake_get_metadata.return_value = {"instance": {"zone": "projects/233510669999/zones/us15"}} assert _gae.standard_availability_zone() == "us15" def test_gae_standard_region( fake_get_metadata: MagicMock, ) -> None: fake_get_metadata.return_value = {"instance": {"region": "projects/233510669999/regions/us-east4"}} assert _gae.standard_cloud_region() == "us-east4" opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/tests/test_gce.py000066400000000000000000000035521527070072300337340ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest.mock import MagicMock import pytest from opentelemetry.resourcedetector.gcp_resource_detector import ( _gce, _metadata, ) # Reset stuff before every test # pylint: disable=unused-argument @pytest.fixture(autouse=True) def autouse(fake_get_metadata): pass def test_detects_on_gce() -> None: assert _gce.on_gce() def test_detects_not_on_gce(fake_get_metadata: MagicMock) -> None: # when the metadata server is not accessible fake_get_metadata.side_effect = _metadata.MetadataAccessException() assert not _gce.on_gce() # when the metadata server doesn't have the expected structure fake_get_metadata.return_value = {} assert not _gce.on_gce() def test_detects_host_type(fake_get_metadata: MagicMock) -> None: fake_get_metadata.return_value = {"instance": {"machineType": "fake"}} assert _gce.host_type() == "fake" def test_detects_host_id(fake_get_metadata: MagicMock) -> None: fake_get_metadata.return_value = {"instance": {"id": 12345}} assert _gce.host_id() == "12345" def test_detects_host_name(fake_get_metadata: MagicMock) -> None: fake_get_metadata.return_value = {"instance": {"name": "fake"}} assert _gce.host_name() == "fake" def test_detects_zone_and_region(fake_get_metadata: MagicMock) -> None: fake_get_metadata.return_value = {"instance": {"zone": "projects/233510669999/zones/us-east4-b"}} zone_and_region = _gce.availability_zone_and_region() assert zone_and_region.zone == "us-east4-b" assert zone_and_region.region == "us-east4" def test_throws_for_invalid_zone(fake_get_metadata: MagicMock) -> None: fake_get_metadata.return_value = {"instance": {"zone": ""}} with pytest.raises(ValueError, match="zone was not in the expected format"): _gce.availability_zone_and_region() test_gcp_resource_detector.py000066400000000000000000000135111527070072300374640ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from importlib.metadata import entry_points from unittest.mock import Mock import pytest import requests from opentelemetry.resourcedetector.gcp_resource_detector import ( GoogleCloudResourceDetector, _metadata, ) @pytest.fixture(name="reset_cache") def fixture_reset_cache(): yield _metadata.get_metadata.cache_clear() _metadata.is_available.cache_clear() @pytest.fixture(name="fake_get") def fixture_fake_get(monkeypatch: pytest.MonkeyPatch): mock = Mock() monkeypatch.setattr(requests, "get", mock) return mock @pytest.fixture(name="fake_metadata") def fixture_fake_metadata(fake_get: Mock): json = {"instance": {}, "project": {}} fake_get().json.return_value = json return json # Reset stuff before every test # pylint: disable=unused-argument @pytest.fixture(autouse=True) def autouse(reset_cache, fake_get, fake_metadata): pass def test_detects_empty_when_not_available(snapshot, fake_get: Mock): fake_get.side_effect = requests.HTTPError() assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot def test_detects_empty_as_fallback(snapshot): assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot def test_detects_gce(snapshot, fake_metadata: _metadata.Metadata): fake_metadata.update( { "project": {"projectId": "fakeProject"}, "instance": { "name": "fakeName", "id": "0087244a", "machineType": "fakeMachineType", "zone": "projects/233510669999/zones/us-east4-b", "attributes": {}, }, } ) assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot @pytest.mark.parametrize( "cluster_location", ( pytest.param("us-east4", id="regional"), pytest.param("us-east4-b", id="zonal"), ), ) def test_detects_gke( cluster_location: str, snapshot, fake_metadata: _metadata.Metadata, monkeypatch: pytest.MonkeyPatch, ): monkeypatch.setenv("KUBERNETES_SERVICE_HOST", "fakehost") fake_metadata.update( { "project": {"projectId": "fakeProject"}, "instance": { "name": "fakeName", "id": 12345, "machineType": "fakeMachineType", "zone": "projects/233510669999/zones/us-east4-b", # Plus some attributes "attributes": { "cluster-name": "fakeClusterName", "cluster-location": cluster_location, }, }, } ) assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot def test_detects_cloud_run( snapshot, fake_metadata: _metadata.Metadata, monkeypatch: pytest.MonkeyPatch, ): monkeypatch.setenv("K_CONFIGURATION", "fake-configuration") monkeypatch.setenv("K_SERVICE", "fake-service") monkeypatch.setenv("K_REVISION", "fake-revision") fake_metadata.update( { "project": {"projectId": "fakeProject"}, "instance": { # this will not be numeric on FaaS "id": "0087244a", "region": "projects/233510669999/regions/us-east4", }, } ) assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot def test_detects_cloud_functions( snapshot, fake_metadata: _metadata.Metadata, monkeypatch: pytest.MonkeyPatch, ): monkeypatch.setenv("FUNCTION_TARGET", "fake-function-target") # Note all K_* environment variables are set since Cloud Functions executes within Cloud # Run. This tests that the detector can differentiate between them monkeypatch.setenv("K_CONFIGURATION", "fake-configuration") monkeypatch.setenv("K_SERVICE", "fake-service") monkeypatch.setenv("K_REVISION", "fake-revision") fake_metadata.update( { "project": {"projectId": "fakeProject"}, "instance": { # this will not be numeric on FaaS "id": "0087244a", "region": "projects/233510669999/regions/us-east4", }, } ) assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot def test_detects_gae_standard( snapshot, fake_metadata: _metadata.Metadata, monkeypatch: pytest.MonkeyPatch, ): monkeypatch.setenv("GAE_ENV", "standard") monkeypatch.setenv("GAE_SERVICE", "fake-service") monkeypatch.setenv("GAE_VERSION", "fake-version") monkeypatch.setenv("GAE_INSTANCE", "fake-instance") fake_metadata.update( { "project": {"projectId": "fakeProject"}, "instance": { "region": "projects/233510669999/regions/us-east4", "zone": "us-east4-b", }, } ) assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot def test_detects_gae_flex( snapshot, fake_metadata: _metadata.Metadata, monkeypatch: pytest.MonkeyPatch, ): monkeypatch.setenv("GAE_SERVICE", "fake-service") monkeypatch.setenv("GAE_VERSION", "fake-version") monkeypatch.setenv("GAE_INSTANCE", "fake-instance") fake_metadata.update( { "project": {"projectId": "fakeProject"}, "instance": { "zone": "projects/233510669999/zones/us-east4-b", }, } ) assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot def test_entry_points(): eps = entry_points(group="opentelemetry_resource_detector") ep_map = {ep.name: ep for ep in eps} assert "gcp" in ep_map assert "gcp_resource_detector" in ep_map assert ep_map["gcp"].load() is GoogleCloudResourceDetector assert ep_map["gcp_resource_detector"].load() is GoogleCloudResourceDetector opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/tests/test_gke.py000066400000000000000000000033111527070072300337350ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest.mock import MagicMock import pytest from opentelemetry.resourcedetector.gcp_resource_detector import _gke def test_detects_on_gke(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("KUBERNETES_SERVICE_HOST", "fakehost") assert _gke.on_gke() def test_detects_not_on_gke() -> None: assert not _gke.on_gke() def test_detects_host_id(fake_get_metadata: MagicMock) -> None: fake_get_metadata.return_value = {"instance": {"id": 12345}} assert _gke.host_id() == "12345" def test_detects_cluster_name(fake_get_metadata: MagicMock) -> None: fake_get_metadata.return_value = {"instance": {"attributes": {"cluster-name": "fake"}}} assert _gke.cluster_name() == "fake" def test_detects_zone(fake_get_metadata: MagicMock) -> None: fake_get_metadata.return_value = {"instance": {"attributes": {"cluster-location": "us-east4-b"}}} zone_or_region = _gke.availability_zone_or_region() assert zone_or_region.type == "zone" assert zone_or_region.value == "us-east4-b" def test_detects_region(fake_get_metadata: MagicMock) -> None: fake_get_metadata.return_value = {"instance": {"attributes": {"cluster-location": "us-east4"}}} zone_or_region = _gke.availability_zone_or_region() assert zone_or_region.type == "region" assert zone_or_region.value == "us-east4" def test_throws_for_invalid_cluster_location( fake_get_metadata: MagicMock, ) -> None: fake_get_metadata.return_value = {"instance": {"attributes": {"cluster-location": "invalid"}}} with pytest.raises(ValueError, match="unrecognized format for cluster location"): _gke.availability_zone_or_region() test_mapping.py000066400000000000000000000177761527070072300345670ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/resource/opentelemetry-resourcedetector-gcp/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import dataclasses import pytest from syrupy.assertion import SnapshotAssertion from opentelemetry.resourcedetector.gcp_resource_detector._mapping import ( get_monitored_resource, ) from opentelemetry.sdk.resources import Attributes, LabelValue, Resource @pytest.mark.filterwarnings("ignore::DeprecationWarning") @pytest.mark.parametrize( "otel_attributes", [ # GCE pytest.param( { "cloud.platform": "gcp_compute_engine", "cloud.availability_zone": "foo", "host.id": "myhost", }, id="gce instance", ), # k8s container pytest.param( { "cloud.platform": "gcp_kubernetes_engine", "cloud.availability_zone": "myavailzone", "k8s.cluster.name": "mycluster", "k8s.namespace.name": "myns", "k8s.pod.name": "mypod", "k8s.container.name": "mycontainer", }, id="k8s container", ), pytest.param( { "cloud.platform": "gcp_kubernetes_engine", "cloud.region": "myregion", "k8s.cluster.name": "mycluster", "k8s.namespace.name": "myns", "k8s.pod.name": "mypod", "k8s.container.name": "mycontainer", }, id="k8s container region fallback", ), # k8s pod pytest.param( { "cloud.platform": "gcp_kubernetes_engine", "cloud.availability_zone": "myavailzone", "k8s.cluster.name": "mycluster", "k8s.namespace.name": "myns", "k8s.pod.name": "mypod", }, id="k8s pod", ), pytest.param( { "cloud.platform": "gcp_kubernetes_engine", "cloud.region": "myregion", "k8s.cluster.name": "mycluster", "k8s.namespace.name": "myns", "k8s.pod.name": "mypod", }, id="k8s pod region fallback", ), # k8s node pytest.param( { "cloud.platform": "gcp_kubernetes_engine", "cloud.availability_zone": "myavailzone", "k8s.cluster.name": "mycluster", "k8s.namespace.name": "myns", "k8s.node.name": "mynode", }, id="k8s node", ), pytest.param( { "cloud.platform": "gcp_kubernetes_engine", "cloud.region": "myregion", "k8s.cluster.name": "mycluster", "k8s.namespace.name": "myns", "k8s.node.name": "mynode", }, id="k8s node region fallback", ), # k8s cluster pytest.param( { "cloud.platform": "gcp_kubernetes_engine", "cloud.availability_zone": "myavailzone", "k8s.cluster.name": "mycluster", "k8s.namespace.name": "myns", }, id="k8s cluster", ), pytest.param( { "cloud.platform": "gcp_kubernetes_engine", "cloud.region": "myregion", "k8s.cluster.name": "mycluster", "k8s.namespace.name": "myns", }, id="k8s cluster region fallback", ), # aws ec2 pytest.param( { "cloud.platform": "aws_ec2", "cloud.availability_zone": "myavailzone", "host.id": "myhostid", "cloud.account.id": "myawsaccount", }, id="aws ec2", ), pytest.param( { "cloud.platform": "aws_ec2", "cloud.region": "myregion", "host.id": "myhostid", "cloud.account.id": "myawsaccount", }, id="aws ec2 region fallback", ), # generic task pytest.param( { "cloud.availability_zone": "myavailzone", "service.namespace": "servicens", "service.name": "servicename", "service.instance.id": "serviceinstanceid", }, id="generic task", ), pytest.param( { "cloud.region": "myregion", "service.namespace": "servicens", "service.name": "servicename", "service.instance.id": "serviceinstanceid", }, id="generic task fallback region", ), pytest.param( { "service.namespace": "servicens", "service.name": "servicename", "service.instance.id": "serviceinstanceid", }, id="generic task fallback global", ), pytest.param( { "service.name": "unknown_service", "cloud.region": "myregion", "service.namespace": "servicens", "faas.name": "faasname", "faas.instance": "faasinstance", }, id="generic task faas", ), pytest.param( { "service.name": "unknown_service", "cloud.region": "myregion", "service.namespace": "servicens", "faas.instance": "faasinstance", }, id="generic task faas fallback", ), # generic node pytest.param( { "cloud.availability_zone": "myavailzone", "service.namespace": "servicens", "service.name": "servicename", "host.id": "hostid", }, id="generic node", ), pytest.param( { "cloud.region": "myregion", "service.namespace": "servicens", "service.name": "servicename", "host.id": "hostid", }, id="generic node fallback region", ), pytest.param( { "service.namespace": "servicens", "service.name": "servicename", "host.id": "hostid", }, id="generic node fallback global", ), pytest.param( { "service.namespace": "servicens", "service.name": "servicename", "host.name": "hostname", }, id="generic node fallback host name", ), # fallback empty pytest.param( {"foo": "bar", "no.useful": "resourceattribs"}, id="fallback generic node", ), pytest.param( {}, id="empty", ), ], ) def test_get_monitored_resource(otel_attributes: Attributes, snapshot: SnapshotAssertion) -> None: resource = Resource(otel_attributes) monitored_resource_data = get_monitored_resource(resource) as_dict = dataclasses.asdict(monitored_resource_data) assert as_dict == snapshot @pytest.mark.filterwarnings("ignore::DeprecationWarning") @pytest.mark.parametrize( ("value", "expect"), [ (None, ""), (123, "123"), (123.4, "123.4"), ([1, 2, 3, 4], "[1,2,3,4]"), ([1.1, 2.2, 3.3, 4.4], "[1.1,2.2,3.3,4.4]"), (["a", "b", "c", "d"], '["a","b","c","d"]'), ], ) def test_non_string_values(value: LabelValue, expect: str): # host.id will end up in generic_node's node_id label monitored_resource_data = get_monitored_resource(Resource({"host.id": value})) assert monitored_resource_data is not None value_as_gcm_label = monitored_resource_data.labels["node_id"] assert value_as_gcm_label == expect def test_get_monitored_resource_deprecated(): resource = Resource({}) with pytest.warns(DeprecationWarning, match="deprecated"): get_monitored_resource(resource) opentelemetry-python-contrib-0.66b2/rtd-requirements.txt000066400000000000000000000012041527070072300236150ustar00rootroot00000000000000# Need to install the api/sdk in the venv for autodoc. Used by .readthedocs.yml opentelemetry-api @ git+https://github.com/open-telemetry/opentelemetry-python.git@main#egg=opentelemetry-api&subdirectory=opentelemetry-api opentelemetry-semantic-conventions @ git+https://github.com/open-telemetry/opentelemetry-python.git@main#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions opentelemetry-sdk @ git+https://github.com/open-telemetry/opentelemetry-python.git@main#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk ./opentelemetry-instrumentation ./util/opentelemetry-util-http -r ./docs-requirements.txt opentelemetry-python-contrib-0.66b2/scripts/000077500000000000000000000000001527070072300212345ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/scripts/build.sh000077500000000000000000000035671527070072300227050ustar00rootroot00000000000000#!/bin/sh # This script builds wheels for the API, SDK, and extension packages in the # dist/ dir, to be uploaded to PyPI. set -ev # Get the latest versions of packaging tools python3 -m pip install --upgrade pip build setuptools wheel BASEDIR=$(dirname "$(readlink -f "$(dirname $0)")") DISTDIR=dist ( cd $BASEDIR mkdir -p $DISTDIR rm -rf ${DISTDIR:?}/* # this is called by release workflow so don't list here independently released packages for d in exporter/*/ opentelemetry-instrumentation/ opentelemetry-contrib-instrumentations/ opentelemetry-distro/ instrumentation/*/ processor/*/ propagator/*/ resource/*/ sdk-extension/*/ util/*/ ; do ( echo "building $d" cd "$d" # Some ext directories (such as docker tests) are not intended to be # packaged. Verify the intent by looking for a pyproject.toml. if [ -f pyproject.toml ]; then python3 -m build --outdir "$BASEDIR/dist/" fi ) done ( cd $DISTDIR for x in * ; do # FIXME: Remove this once opentelemetry-resource-detector-azure package goes 1.X if echo "$x" | grep -Eq "^opentelemetry_(resource_detector_azure|util_genai).*(\.tar\.gz|\.whl)$"; then echo "Skipping $x because of manual upload by Azure maintainers." rm $x # opentelemetry-instrumentation-valkey-py is released independently via package-release.yml elif echo "$x" | grep -Eq "^opentelemetry_instrumentation_valkey_py-.*(\.tar\.gz|\.whl)$"; then echo "Skipping $x because it is released independently via package-release.yml." rm $x # NOTE: We filter beta vs 1.0 package at this point because we can read the # version directly from the .tar.gz/whl file elif echo "$x" | grep -Eq "^opentelemetry_.*-0\..*(\.tar\.gz|\.whl)$"; then : else echo "Skipping $x because it is not in pre-1.0 state and should be released using a tag." rm $x fi done ) ) opentelemetry-python-contrib-0.66b2/scripts/build_a_package.sh000077500000000000000000000044171527070072300246530ustar00rootroot00000000000000#!/bin/sh # Copyright The OpenTelemetry Authors # # 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. # This script builds wheels for a single package when triggered from per-package release # GitHub workflow (see .github/package-release.yml). # The wheel is then published to PyPI by the workflow. set -ev if [ $PACKAGE_NAME ]; then pkg_name=${PACKAGE_NAME} pkg_version=${VERSION} elif [ $GITHUB_REF ]; then pkg_name_and_version=${GITHUB_REF#refs/tags/*} pkg_name=${pkg_name_and_version%==*} pkg_version=${pkg_name_and_version#opentelemetry-*==} else echo 'Failed to run script, missing workflow env variable GITHUB_REF or PACKAGE_NAME and VERSION.' exit 1 fi # Get the latest versions of packaging tools python3 -m pip install --upgrade pip build setuptools wheel packaging # Validate version against PEP 440 conventions: https://packaging.pypa.io/en/latest/version.html python3 -c "from packaging.version import Version; Version('${pkg_version}')" basedir=$(git rev-parse --show-toplevel) cd $basedir distdir=${basedir}/dist mkdir -p $distdir rm -rf ${distdir:?}/* pyproject_toml_file_path=$(ls **/$pkg_name/pyproject.toml) if [ -z $pyproject_toml_file_path ]; then echo "Error! pyproject.toml not found for $pkg_name, can't build." exit 1 fi directory_with_package=$(dirname $pyproject_toml_file_path) cd $directory_with_package python3 -m build --outdir ${distdir} cd $distdir pkg_tar_gz_file=`echo $pkg_name | sed 's/-/_/g'`-${pkg_version}.tar.gz echo "Checking if $pkg_tar_gz_file exists in dist/ directory." # print the list of files in current directory echo "$(ls)" if ! [ -f $pkg_tar_gz_file ]; then echo 'Error! Tag version does not match version built using latest package files.' exit 1 fi # Build a wheel for the source distribution pip wheel --no-deps $pkg_tar_gz_file opentelemetry-python-contrib-0.66b2/scripts/changelog_template.j2000066400000000000000000000012321527070072300253110ustar00rootroot00000000000000## Version {{ versiondata.version }} ({{ versiondata.date }}) {% for section, _ in sections.items() %} {%- if section %}{{ section }}{% endif -%} {% if sections[section] %} {% for category, val in definitions.items() if category in sections[section] %} ### {{ definitions[category]['name'] }} {% for text, values in sections[section][category].items() %} {% if "\n - " in text or '\n * ' in text %} {%- set main_text, sub_items = text.split('\n', 1) %} - {{ main_text }} ({{ values|join(', ') }}) {% if sub_items %} {{- sub_items }} {% endif %} {% else %} - {{ text }} ({{ values|join(', ') }}) {% endif %} {% endfor %} {% endfor %} {% endif %} {% endfor %} opentelemetry-python-contrib-0.66b2/scripts/check_for_valid_readme.py000066400000000000000000000025101527070072300262230ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Test script to check given paths for valid README.rst files.""" import argparse import sys from pathlib import Path import readme_renderer.rst def is_valid_rst(path): """Checks if RST can be rendered on PyPI.""" with open(path, encoding="utf-8") as readme_file: markup = readme_file.read() return readme_renderer.rst.render(markup, stream=sys.stderr) is not None def parse_args(): parser = argparse.ArgumentParser(description="Checks README.rst file in path for syntax errors.") parser.add_argument("paths", nargs="+", help="paths containing a README.rst to test") parser.add_argument("-v", "--verbose", action="store_true") return parser.parse_args() def main(): args = parse_args() error = False for path in map(Path, args.paths): readme = path / "README.rst" try: if not is_valid_rst(readme): error = True print("FAILED: RST syntax errors in", readme) continue except FileNotFoundError: print("FAILED: README.rst not found in", path) continue if args.verbose: print("PASSED:", readme) if error: sys.exit(1) print("All clear.") if __name__ == "__main__": main() opentelemetry-python-contrib-0.66b2/scripts/check_license_header.py000066400000000000000000000047041527070072300257020ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import sys from pathlib import Path SPDX_LINE = "# SPDX-License-Identifier: Apache-2.0\n" SKIP_SUFFIXES = ( "_pb2.py", "_pb2_grpc.py", ) SKIP_DIRS = ( "target", ".tox", ".venv", "__pycache__", "node_modules", ) GENERATED_MARKERS = ( "# generated by datamodel-codegen", "# Generated by the protocol buffer compiler", "# Generated by the gRPC Python protocol compiler", ) def should_skip(relpath): parts = relpath.split(os.sep) if any(d in parts for d in SKIP_DIRS): return True basename = os.path.basename(relpath) return any(basename.endswith(s) for s in SKIP_SUFFIXES) def check_file(path): with open(path) as f: lines = f.readlines() # Skip empty files (e.g. namespace __init__.py) if not lines or all(line.strip() == "" for line in lines): return True # Skip generated files if any(lines[0].startswith(m) for m in GENERATED_MARKERS): return True # If file starts with a shebang, check lines after it start = 0 if lines[0].startswith("#!"): start = 1 # Skip optional blank line after shebang if start < len(lines) and lines[start].strip() == "": start += 1 if len(lines) < start + 2: return False # Line 1 must be a copyright comment, line 2 must be SPDX return lines[start].startswith("# Copyright") and lines[start + 1] == SPDX_LINE def main(): root = str(Path(__file__).resolve().parents[1]) failures = [] for dirpath, dirnames, filenames in os.walk(root): dirnames[:] = [d for d in dirnames if not d.startswith(".")] for filename in filenames: if not filename.endswith(".py"): continue path = os.path.join(dirpath, filename) relpath = os.path.relpath(path, root) if should_skip(relpath): continue if not check_file(path): failures.append(relpath) if failures: print("Files missing the SPDX license header:") print() print(" Expected first two lines:") print(" # Copyright ") print(" # SPDX-License-Identifier: Apache-2.0") print() for f in sorted(failures): print(f" {f}") sys.exit(1) print("All files have the correct license header.") if __name__ == "__main__": main() opentelemetry-python-contrib-0.66b2/scripts/coverage.sh000077500000000000000000000007761527070072300234000ustar00rootroot00000000000000#!/bin/bash set -e function cov { pytest \ --cov ${1} \ --cov-append \ --cov-branch \ --cov-report='' \ ${1} } coverage erase cov instrumentation/opentelemetry-instrumentation-flask cov instrumentation/opentelemetry-instrumentation-requests cov instrumentation/opentelemetry-instrumentation-wsgi cov instrumentation/opentelemetry-instrumentation-aiohttp-client cov instrumentation/opentelemetry-instrumentation-asgi coverage report --show-missing coverage xml opentelemetry-python-contrib-0.66b2/scripts/eachdist.py000077500000000000000000000617671527070072300234160ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import argparse import os import re import shlex import shutil import subprocess import sys from configparser import ConfigParser from datetime import datetime from inspect import cleandoc from itertools import chain from os.path import basename from pathlib import Path, PurePath DEFAULT_ALLSEP = " " DEFAULT_ALLFMT = "{rel}" NON_SRC_DIRS = ["build", "dist", "__pycache__", "lib", "venv", ".tox"] def unique(elems): seen = set() for elem in elems: if elem not in seen: yield elem seen.add(elem) subprocess_run = subprocess.run def extraargs_help(calledcmd): return cleandoc( f""" Additional arguments to pass on to {calledcmd}. This is collected from any trailing arguments passed to `%(prog)s`. Use an initial `--` to separate them from regular arguments. """ ) def parse_args(args=None): parser = argparse.ArgumentParser(description="Development helper script.") parser.set_defaults(parser=parser) parser.add_argument( "--dry-run", action="store_true", help="Only display what would be done, don't actually do anything.", ) subparsers = parser.add_subparsers(metavar="COMMAND") subparsers.required = True excparser = subparsers.add_parser( "exec", help="Run a command for each or all targets.", formatter_class=argparse.RawTextHelpFormatter, description=cleandoc( """Run a command according to the `format` argument for each or all targets. This is an advanced command that is used internally by other commands. For example, to install all distributions in this repository editable, you could use: scripts/eachdist.py exec "python -m pip install -e {}" This will run pip for all distributions which is quite slow. It gets a bit faster if we only invoke pip once but with all the paths gathered together, which can be achieved by using `--all`: scripts/eachdist.py exec "python -m pip install {}" --all "-e {}" The sortfirst option in the DEFAULT section of eachdist.ini makes sure that dependencies are installed before their dependents. Search for usages of `parse_subargs` in the source code of this script to see more examples. This command first collects target paths and then executes commands according to `format` and `--all`. Target paths are initially all Python distribution root paths (as determined by the existence of pyproject.toml, etc. files). They are then augmented according to the section of the `PROJECT_ROOT/eachdist.ini` config file specified by the `--mode` option. The following config options are available (and processed in that order): - `extraroots`: List of project root-relative glob expressions. The resulting paths will be added. - `sortfirst`: List of glob expressions. Any matching paths will be put to the front of the path list, in the same order they appear in this option. If more than one glob matches, ordering is according to the first. - `subglob`: List of glob expressions. Each path added so far is removed and replaced with the result of all glob expressions relative to it (in order of the glob expressions). After all this, any duplicate paths are removed (the first occurrence remains). """ ), ) excparser.set_defaults(func=execute_args) excparser.add_argument( "format", help=cleandoc( """Format string for the command to execute. The available replacements depend on whether `--all` is specified. If `--all` was specified, there is only a single replacement, `{}`, that is replaced with the string that is generated from joining all targets formatted with `--all` to a single string with the value of `--allsep` as separator. If `--all` was not specified, the following replacements are available: - `{}`: the absolute path to the current target in POSIX format (with forward slashes) - `{rel}`: like `{}` but relative to the project root. - `{raw}`: the absolute path to the current target in native format (thus exactly the same as `{}` on Unix but with backslashes on Windows). - `{rawrel}`: like `{raw}` but relative to the project root. The resulting string is then split according to POSIX shell rules (so you can use quotation marks or backslashes to handle arguments containing spaces). The first token is the name of the executable to run, the remaining tokens are the arguments. Note that a shell is *not* involved by default. You can add bash/sh/cmd/powershell yourself to the format if you want. If `--all` was specified, the resulting command is simply executed once. Otherwise, the command is executed for each found target. In both cases, the project root is the working directory. """ ), ) excparser.add_argument( "--all", nargs="?", const=DEFAULT_ALLFMT, metavar="ALLFORMAT", help=cleandoc( """Instead of running the command for each target, join all target paths together to run a single command. This option optionally takes a format string to apply to each path. The available replacements are the ones that would be available for `format` if `--all` was not specified. Default ALLFORMAT if this flag is specified: `%(const)s`. """ ), ) excparser.add_argument( "--allsep", help=cleandoc( """Separator string for the strings resulting from `--all`. Only valid if `--all` is specified. """ ), ) excparser.add_argument( "--allowexitcode", type=int, action="append", default=[0], help=cleandoc( """The given command exit code is treated as success and does not abort execution. Can be specified multiple times. """ ), ) excparser.add_argument( "--mode", "-m", default="DEFAULT", help=cleandoc( """Section of config file to use for target selection configuration. See description of exec for available options.""" ), ) instparser = subparsers.add_parser("install", help="Install all distributions.") def setup_instparser(instparser): instparser.set_defaults(func=install_args) instparser.add_argument("pipargs", nargs=argparse.REMAINDER, help=extraargs_help("pip")) setup_instparser(instparser) instparser.add_argument("--editable", "-e", action="store_true") instparser.add_argument("--with-dev-deps", action="store_true") instparser.add_argument("--eager-upgrades", action="store_true") devparser = subparsers.add_parser( "develop", help="Install all distributions editable + dev dependencies.", ) setup_instparser(devparser) devparser.set_defaults( editable=True, with_dev_deps=True, eager_upgrades=True, ) lintparser = subparsers.add_parser("lint", help="Lint everything, autofixing if possible.") lintparser.add_argument("--check-only", action="store_true") lintparser.set_defaults(func=lint_args) testparser = subparsers.add_parser( "test", help="Test everything (run pytest yourself for more complex operations).", ) testparser.set_defaults(func=test_args) testparser.add_argument("pytestargs", nargs=argparse.REMAINDER, help=extraargs_help("pytest")) releaseparser = subparsers.add_parser( "update_versions", help="Updates version numbers, used by maintainers and CI", ) releaseparser.set_defaults(func=release_args) releaseparser.add_argument("--versions", required=True) releaseparser.add_argument("releaseargs", nargs=argparse.REMAINDER, help=extraargs_help("pytest")) patchreleaseparser = subparsers.add_parser( "update_patch_versions", help="Updates version numbers during patch release, used by maintainers and CI", ) patchreleaseparser.set_defaults(func=patch_release_args) patchreleaseparser.add_argument("--stable_version", required=True) patchreleaseparser.add_argument("--unstable_version", required=True) patchreleaseparser.add_argument("--stable_version_prev", required=True) patchreleaseparser.add_argument("--unstable_version_prev", required=True) fmtparser = subparsers.add_parser( "format", help="Formats all source code with black and isort.", ) fmtparser.set_defaults(func=format_args) fmtparser.add_argument( "--path", required=False, help="Format only this path instead of entire repository", ) versionparser = subparsers.add_parser( "version", help="Get the version for a release", ) versionparser.set_defaults(func=version_args) versionparser.add_argument( "--mode", "-m", default="DEFAULT", help=cleandoc( """Section of config file to use for target selection configuration. See description of exec for available options.""" ), ) versionparser.add_argument( "--package", "-p", required=False, help="Name of a specific package to get the version for", ) findparser = subparsers.add_parser( "find-package", help="Find package path.", ) findparser.set_defaults(func=find_package_args) findparser.add_argument( "--package", "-p", required=True, help="Name of the package to find", ) return parser.parse_args(args) def find_projectroot(search_start=Path(".")): root = search_start.resolve() for root in chain((root,), root.parents): if any((root / marker).exists() for marker in (".git", "tox.ini")): return root return None def find_targets_unordered(rootpath): for subdir in rootpath.iterdir(): if not subdir.is_dir(): continue if subdir.name.startswith(".") or subdir.name.startswith("venv"): continue if any((subdir / marker).exists() for marker in ("pyproject.toml",)): yield subdir else: yield from find_targets_unordered(subdir) def getlistcfg(strval): return [val.strip() for line in strval.split("\n") for val in line.split(",") if val.strip()] def find_targets(mode, rootpath): if not rootpath: sys.exit("Could not find a root directory.") cfg = ConfigParser() cfg.read(str(rootpath / "eachdist.ini")) mcfg = cfg[mode] targets = list(find_targets_unordered(rootpath)) if "extraroots" in mcfg: targets += [path for extraglob in getlistcfg(mcfg["extraroots"]) for path in rootpath.glob(extraglob)] if "sortfirst" in mcfg: sortfirst = getlistcfg(mcfg["sortfirst"]) def keyfunc(path): path = path.relative_to(rootpath) for idx, pattern in enumerate(sortfirst): if path.match(pattern): return idx return float("inf") targets.sort(key=keyfunc) if "ignore" in mcfg: ignore = getlistcfg(mcfg["ignore"]) def filter_func(path): path = path.relative_to(rootpath) for pattern in ignore: if path.match(pattern): return False return True filtered = filter(filter_func, targets) targets = list(filtered) subglobs = getlistcfg(mcfg.get("subglob", "")) if subglobs: targets = [ newentry for newentry in ( target / subdir for target in targets for subglob in subglobs # We need to special-case the dot, because glob fails to parse that with an IndexError. for subdir in ((target,) if subglob == "." else target.glob(subglob)) ) if ".egg-info" not in str(newentry) and newentry.exists() ] return list(unique(targets)) def runsubprocess(dry_run, params, *args, **kwargs): cmdstr = join_args(params) if dry_run: print(cmdstr) return None # Py < 3.6 compat. cwd = kwargs.get("cwd") if cwd and isinstance(cwd, PurePath): kwargs["cwd"] = str(cwd) check = kwargs.pop("check") # Enforce specifying check print(">>>", cmdstr, file=sys.stderr, flush=True) # This is a workaround for subprocess.run(['python']) leaving the virtualenv on Win32. # The cause for this is that when running the python.exe in a virtualenv, # the wrapper executable launches the global python as a subprocess and the search sequence # for CreateProcessW which subprocess.run and Popen use is a follows # (https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw): # > 1. The directory from which the application loaded. # This will be the directory of the global python.exe, not the venv directory, due to the suprocess mechanism. # > 6. The directories that are listed in the PATH environment variable. # Only this would find the "correct" python.exe. params = list(params) executable = shutil.which(params[0]) if executable: params[0] = executable try: return subprocess_run(params, *args, check=check, **kwargs) except OSError as exc: raise ValueError("Failed executing " + repr(params) + ": " + str(exc)) from exc def execute_args(args): if args.allsep and not args.all: args.parser.error("--allsep specified but not --all.") if args.all and not args.allsep: args.allsep = DEFAULT_ALLSEP rootpath = find_projectroot() targets = find_targets(args.mode, rootpath) if not targets: sys.exit(f"Error: No targets selected (root: {rootpath})") def fmt_for_path(fmt, path): return fmt.format( path.as_posix(), rel=path.relative_to(rootpath).as_posix(), raw=path, rawrel=path.relative_to(rootpath), ) def _runcmd(cmd): result = runsubprocess(args.dry_run, shlex.split(cmd), cwd=rootpath, check=False) if result is not None and result.returncode not in args.allowexitcode: print( f"'{cmd}' failed with code {result.returncode}", file=sys.stderr, ) sys.exit(result.returncode) if args.all: allstr = args.allsep.join(fmt_for_path(args.all, path) for path in targets) cmd = args.format.format(allstr) _runcmd(cmd) else: for target in targets: cmd = fmt_for_path(args.format, target) _runcmd(cmd) def clean_remainder_args(remainder_args): if remainder_args and remainder_args[0] == "--": del remainder_args[0] def join_args(arglist): return " ".join(map(shlex.quote, arglist)) def install_args(args): clean_remainder_args(args.pipargs) if args.eager_upgrades: args.pipargs += ["--upgrade-strategy=eager"] if args.with_dev_deps: runsubprocess( args.dry_run, [ "python", "-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel", ] + args.pipargs, check=True, ) allfmt = "-e 'file://{}'" if args.editable else "'file://{}'" execute_args( parse_subargs( args, ( "exec", "python -m pip install {} " + join_args(args.pipargs), "--all", allfmt, ), ) ) if args.with_dev_deps: rootpath = find_projectroot() runsubprocess( args.dry_run, [ "python", "-m", "pip", "install", "--upgrade", "-r", str(rootpath / "dev-requirements.txt"), ] + args.pipargs, check=True, ) def parse_subargs(parentargs, args): subargs = parse_args(args) subargs.dry_run = parentargs.dry_run or subargs.dry_run return subargs def lint_args(args): runsubprocess( args.dry_run, ("ruff", "check") + (() if args.check_only else ("--fix",)), check=True, ) runsubprocess( args.dry_run, ("ruff", "format") + (("--check",) if args.check_only else ()), check=True, ) execute_args(parse_subargs(args, ("exec", "pylint {}", "--all", "--mode", "lintroots"))) execute_args( parse_subargs( args, ( "exec", "python scripts/check_for_valid_readme.py {}", "--all", ), ) ) def update_changelog(path, version, new_entry): unreleased_changes = False try: with open(path, encoding="utf-8") as changelog: text = changelog.read() if f"## [{version}]" in text: raise AttributeError(f"{path} already contains version {version}") with open(path, encoding="utf-8") as changelog: for line in changelog: if line.startswith("## [Unreleased]"): unreleased_changes = False elif line.startswith("## "): break elif len(line.strip()) > 0: unreleased_changes = True except FileNotFoundError: print(f"file missing: {path}") return if unreleased_changes: print(f"updating: {path}") text = re.sub(r"## \[Unreleased\].*", new_entry, text) with open(path, "w", encoding="utf-8") as changelog: changelog.write(text) def update_changelogs(version): today = datetime.now().strftime("%Y-%m-%d") new_entry = f"""## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v{version}...HEAD) ## [{version}](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v{version}) - {today} """ errors = False try: update_changelog("./CHANGELOG.md", version, new_entry) except Exception as err: # pylint: disable=broad-except print(str(err)) errors = True if errors: sys.exit(1) def find(name, path): non_src_dirs = [os.path.join(path, nsd) for nsd in NON_SRC_DIRS] def _is_non_src_dir(root) -> bool: for nsd in non_src_dirs: if root.startswith(nsd): return True return False for root, _, files in os.walk(path): if _is_non_src_dir(root): continue if name in files: return os.path.join(root, name) return None def filter_packages(targets, packages): if not packages: return targets filtered_packages = [] for target in targets: for pkg in packages: if str(pkg) == "all": continue if str(pkg) in str(target): filtered_packages.append(target) break return filtered_packages def update_version_files(targets, version, packages): print("updating version.py files") targets = filter_packages(targets, packages) update_files( targets, "version.py", "__version__ .*", f'__version__ = "{version}"', ) def update_dependencies(targets, version, packages): print("updating dependencies") if "all" in packages: packages.extend(targets) for pkg in packages: if str(pkg) == "all": continue print(pkg) package_name = basename(pkg) print(package_name) update_files( targets, "pyproject.toml", rf"({package_name}.*)==(.*)", r"\1== " + version + '",', ) def update_patch_dependencies(targets, version, prev_version, packages): print("updating patch dependencies") if "all" in packages: packages.extend(targets) # PEP 508 allowed specifier operators operators = ["==", "!=", "<=", ">=", "<", ">", "===", "~=", "="] operators_pattern = "|".join(re.escape(op) for op in operators) for pkg in packages: search = rf"({basename(pkg)}[^,]*?)(\s?({operators_pattern})\s?)(.*{prev_version})" replace = r"\g<1>\g<2>" + version print(f"{search=}\t{replace=}\t{pkg=}") update_files( targets, "pyproject.toml", search, replace, ) def update_files(targets, filename, search, replace): errors = False for target in targets: curr_file = find(filename, target) if curr_file is None: print(f"file missing: {target}/{filename}") continue with open(curr_file, encoding="utf-8") as _file: text = _file.read() if replace in text: print(f"{curr_file} already contains {replace}") continue with open(curr_file, "w", encoding="utf-8") as _file: _file.write(re.sub(search, replace, text)) if errors: sys.exit(1) def release_args(args): print("preparing release") rootpath = find_projectroot() targets = list(find_targets_unordered(rootpath)) cfg = ConfigParser() cfg.read(str(find_projectroot() / "eachdist.ini")) versions = args.versions updated_versions = [] # remove excluded packages excluded = cfg["exclude_release"]["packages"].split() targets = [target for target in targets if basename(target) not in excluded] for group in versions.split(","): mcfg = cfg[group] version = mcfg["version"] updated_versions.append(version) packages = None if "packages" in mcfg: packages = [pkg for pkg in mcfg["packages"].split() if pkg not in excluded] print(f"update {group} packages to {version}") update_dependencies(targets, version, packages) update_version_files(targets, version, packages) update_changelogs("-".join(updated_versions)) def patch_release_args(args): print("preparing patch release") rootpath = find_projectroot() targets = list(find_targets_unordered(rootpath)) cfg = ConfigParser() cfg.read(str(find_projectroot() / "eachdist.ini")) # remove excluded packages excluded = cfg["exclude_release"]["packages"].split() targets = [target for target in targets if basename(target) not in excluded] # stable mcfg = cfg["stable"] packages = mcfg["packages"].split() print(f"update stable packages to {args.stable_version}") update_patch_dependencies(targets, args.stable_version, args.stable_version_prev, packages) update_version_files(targets, args.stable_version, packages) # prerelease mcfg = cfg["prerelease"] packages = mcfg["packages"].split() print(f"update prerelease packages to {args.unstable_version}") update_patch_dependencies(targets, args.unstable_version, args.unstable_version_prev, packages) update_version_files(targets, args.unstable_version, packages) def test_args(args): clean_remainder_args(args.pytestargs) execute_args( parse_subargs( args, ( "exec", "pytest {} " + join_args(args.pytestargs), "--mode", "testroots", ), ) ) def format_args(args): format_dir = str(find_projectroot()) if args.path: format_dir = os.path.join(format_dir, args.path) runsubprocess( args.dry_run, ("ruff", "check", "--fix"), cwd=format_dir, check=True, ) runsubprocess( args.dry_run, ("ruff", "format"), cwd=format_dir, check=True, ) def version_args(args): cfg = ConfigParser() cfg.read(str(find_projectroot() / "eachdist.ini")) if not args.package: print(cfg[args.mode]["version"]) return root = find_projectroot() for package in find_targets_unordered(root): if args.package == package.name: version_file = find("version.py", package) if version_file is None: print(f"file missing: {package}/version.py") return with open(version_file, encoding="utf-8") as file: for line in file: if "__version__" in line: print(line.split('"')[1]) return print("package not found") sys.exit(1) def find_package_args(args): root = find_projectroot() for package in find_targets_unordered(root): if args.package == package.name: relative_path = package.relative_to(root) print(relative_path) return print("package not found") sys.exit(1) def main(): args = parse_args() args.func(args) if __name__ == "__main__": main() opentelemetry-python-contrib-0.66b2/scripts/generate_instrumentation_bootstrap.py000077500000000000000000000076621527070072300310360ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import ast import logging import os import subprocess import sys import astor from otel_packaging import ( get_instrumentation_packages, root_path, scripts_path, ) logging.basicConfig(level=logging.INFO) logger = logging.getLogger("instrumentation_list_generator") _template = """ {header} # DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. # RUN `python scripts/generate_instrumentation_bootstrap.py` TO REGENERATE. {source} """ _source_tmpl = """ libraries = {} default_instrumentations = [] """ gen_path = os.path.join( root_path, "opentelemetry-instrumentation", "src", "opentelemetry", "instrumentation", "bootstrap_gen.py", ) packages_to_exclude = [ # AWS Lambda instrumentation is excluded from the default list because it often # requires specific configurations and dependencies that may not be set up # in all environments. Instead, users who need AWS Lambda support can opt-in # by manually adding it to their environment. # See https://github.com/open-telemetry/opentelemetry-python-contrib/issues/2787 "opentelemetry-instrumentation-aws-lambda", # OpenAI Agents instrumentation is currently excluded because it is still in early # development. This filter will get removed once it is further along in its # development lifecycle and ready to be included by default. "opentelemetry-instrumentation-openai-agents-v2", # Valkey instrumentation is currently a PyPI boilerplate package. # Include it in the bootstrap once its implementation is available. "opentelemetry-instrumentation-valkey-py", ] # Static version specifiers for instrumentations that are released independently independent_packages = { "opentelemetry-instrumentation-openai-v2": "", "opentelemetry-instrumentation-vertexai": ">=2.0b0", } def main(): # pylint: disable=no-member default_instrumentations = ast.List(elts=[]) libraries = ast.List(elts=[]) for pkg in get_instrumentation_packages(independent_packages=independent_packages): pkg_name = pkg.get("name") if pkg_name in packages_to_exclude: continue if not pkg["instruments"] and not pkg["instruments-any"]: default_instrumentations.elts.append(ast.Str(pkg["requirement"])) for target_pkg in pkg["instruments"]: libraries.elts.append( ast.Dict( keys=[ast.Str("library"), ast.Str("instrumentation")], values=[ast.Str(target_pkg), ast.Str(pkg["requirement"])], ) ) # instruments-any is an optional field that can be used instead of or in addition to _instruments. While _instruments is a list of dependencies, all of which are expected by the instrumentation, instruments-any is a list any of which but not all are expected. for target_pkg in pkg["instruments-any"]: libraries.elts.append( ast.Dict( keys=[ast.Str("library"), ast.Str("instrumentation")], values=[ast.Str(target_pkg), ast.Str(pkg["requirement"])], ) ) tree = ast.parse(_source_tmpl) tree.body[0].value = libraries tree.body[1].value = default_instrumentations source = astor.to_source(tree) with open(os.path.join(scripts_path, "license_header.txt"), encoding="utf-8") as header_file: header = header_file.read() source = _template.format(header=header, source=source) with open(gen_path, "w", encoding="utf-8") as gen_file: gen_file.write(source) subprocess.run( [ sys.executable, "scripts/eachdist.py", "format", "--path", "opentelemetry-instrumentation/src", ], check=True, ) logger.info("generated %s", gen_path) if __name__ == "__main__": main() opentelemetry-python-contrib-0.66b2/scripts/generate_instrumentation_metapackage.py000077500000000000000000000041671527070072300312600ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import os import tomli import tomli_w logging.basicConfig(level=logging.INFO) logger = logging.getLogger("instrumentation_metapackage_generator") _prefix = "opentelemetry-instrumentation-" root_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) base_instrumentation_path = os.path.join(root_path, "instrumentation") packages_to_exclude = [ # Valkey instrumentation is currently a PyPI boilerplate package. # Include it in the metapackage once its implementation is available. "opentelemetry-instrumentation-valkey-py", ] def get_instrumentation_packages(): for instrumentation in sorted(os.listdir(base_instrumentation_path)): if instrumentation in packages_to_exclude: continue instrumentation_path = os.path.join(base_instrumentation_path, instrumentation) if not os.path.isdir(instrumentation_path) or not instrumentation.startswith(_prefix): continue src_dir = os.path.join(instrumentation_path, "src", "opentelemetry", "instrumentation") src_pkgs = [f for f in os.listdir(src_dir) if os.path.isdir(os.path.join(src_dir, f))] assert len(src_pkgs) == 1 name = src_pkgs[0] pkg_info = {} version_filename = os.path.join( src_dir, name, "version.py", ) with open(version_filename, encoding="utf-8") as fh: exec(fh.read(), pkg_info) version = pkg_info["__version__"] yield (instrumentation, version) def main(): dependencies = get_instrumentation_packages() pyproject_toml_path = os.path.join(root_path, "opentelemetry-contrib-instrumentations", "pyproject.toml") deps = [f"{pkg.strip()}=={version.strip()}" for pkg, version in dependencies] with open(pyproject_toml_path, "rb") as file: pyproject_toml = tomli.load(file) pyproject_toml["project"]["dependencies"] = deps with open(pyproject_toml_path, "wb") as fh: tomli_w.dump(pyproject_toml, fh) if __name__ == "__main__": main() opentelemetry-python-contrib-0.66b2/scripts/generate_instrumentation_readme.py000077500000000000000000000051401527070072300302430ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import os logging.basicConfig(level=logging.INFO) logger = logging.getLogger("instrumentation_readme_generator") _prefix = "opentelemetry-instrumentation-" header = """ | Instrumentation | Supported Packages | Metrics support | Semconv status | | --------------- | ------------------ | --------------- | -------------- |""" def main(base_instrumentation_path): table = [header] for instrumentation in sorted(os.listdir(base_instrumentation_path)): instrumentation_path = os.path.join(base_instrumentation_path, instrumentation) if not os.path.isdir(instrumentation_path) or not instrumentation.startswith(_prefix): continue src_dir = os.path.join(instrumentation_path, "src", "opentelemetry", "instrumentation") src_pkgs = [f for f in os.listdir(src_dir) if os.path.isdir(os.path.join(src_dir, f))] assert len(src_pkgs) == 1 name = src_pkgs[0] pkg_info = {} version_filename = os.path.join( src_dir, name, "package.py", ) with open(version_filename, encoding="utf-8") as fh: exec(fh.read(), pkg_info) instruments_and = pkg_info.get("_instruments", ()) # _instruments_any is an optional field that can be used instead of or in addition to _instruments. While _instruments is a list of dependencies, all of which are expected by the instrumentation, _instruments_any is a list any of which but not all are expected. instruments_any = pkg_info.get("_instruments_any", ()) supports_metrics = pkg_info.get("_supports_metrics") semconv_status = pkg_info.get("_semconv_status") instruments_all = () if not instruments_and and not instruments_any: instruments_all = (name,) else: instruments_all = tuple(instruments_and + instruments_any) if not semconv_status: semconv_status = "development" metric_column = "Yes" if supports_metrics else "No" table.append( f"| [{instrumentation}](./{instrumentation}) | {','.join(instruments_all)} | {metric_column} | {semconv_status}" ) with open( os.path.join(base_instrumentation_path, "README.md"), "w", encoding="utf-8", ) as fh: fh.write("\n".join(table)) if __name__ == "__main__": root_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) instrumentation_path = os.path.join(root_path, "instrumentation") main(instrumentation_path) opentelemetry-python-contrib-0.66b2/scripts/generate_release_notes.sh000077500000000000000000000021411527070072300262730ustar00rootroot00000000000000#!/bin/bash # This script copies release notes for the current version from CHANGELOG.md file # and stores them in /tmp/release-notes.txt. # It also stores them in a /tmp/CHANGELOG_SECTION.md which is used later to copy them over to the # CHANGELOG in main branch which is done by merge_changelog_to_main script. # This script is called from the release workflows (package-release.yml and release.yml). set -ev # conditional block not indented because of the heredoc if [[ ! -z $PRIOR_VERSION_WHEN_PATCH ]]; then cat > /tmp/release-notes.txt << EOF This is a patch release on the previous $PRIOR_VERSION_WHEN_PATCH release, fixing the issue(s) below. EOF fi # CHANGELOG_SECTION.md is also used at the end of the release workflow # for copying the change log updates to main sed -n "0,/^## Version ${VERSION}/d;/^## Version /q;p" $CHANGELOG > /tmp/CHANGELOG_SECTION.md # the complex perl regex is needed because markdown docs render newlines as soft wraps # while release notes render them as line breaks perl -0pe 's/(?> /tmp/release-notes.txt opentelemetry-python-contrib-0.66b2/scripts/license_header.txt000066400000000000000000000001131527070072300247220ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0opentelemetry-python-contrib-0.66b2/scripts/merge_changelog_to_main.sh000077500000000000000000000036651527070072300264210ustar00rootroot00000000000000#!/bin/bash # This script copies release notes for the current version from CHANGELOG.md file # and stores them in a temporary file for later use in the release workflow # This script is called from the release workflows (package-release.yml and release.yml). set -ev if [[ -z $PRIOR_VERSION_WHEN_PATCH ]]; then # this was not a patch release, so the version exists already in the CHANGELOG.md # update the release date date=$(gh release view $RELEASE_TAG --json publishedAt --jq .publishedAt | sed 's/T.*//') sed -Ei "s/## Version ${VERSION}.*/## Version ${VERSION} ($date)/" ${CHANGELOG} # the entries are copied over from the release branch to support workflows # where change log entries may be updated after preparing the release branch # copy the portion above the release, up to and including the heading sed -n "0,/^## Version ${VERSION} ($date)/p" ${CHANGELOG} > /tmp/CHANGELOG.md # copy the release notes cat /tmp/CHANGELOG_SECTION.md >> /tmp/CHANGELOG.md # copy the portion below the release sed -n "0,/^## Version ${VERSION} /d;0,/^## Version /{/^## Version/!d};p" ${CHANGELOG} \ >> /tmp/CHANGELOG.md # update the real CHANGELOG.md cp /tmp/CHANGELOG.md ${CHANGELOG} else # this was a patch release, so the version does not exist already in the CHANGELOG.md # copy the portion above the top-most release, not including the heading sed -n "0,/^## Version /{ /^## Version /!p }" ${CHANGELOG} > /tmp/CHANGELOG.md # add the heading date=$(gh release view $RELEASE_TAG --json publishedAt --jq .publishedAt | sed 's/T.*//') echo "## Version ${VERSION} ($date)" >> /tmp/CHANGELOG.md # copy the release notes cat /tmp/CHANGELOG_SECTION.md >> /tmp/CHANGELOG.md # copy the portion starting from the top-most release sed -n "/^## Version /,\$p" ${CHANGELOG} >> /tmp/CHANGELOG.md # update the real CHANGELOG.md cp /tmp/CHANGELOG.md ${CHANGELOG} fiopentelemetry-python-contrib-0.66b2/scripts/opamp_proto_codegen.sh000077500000000000000000000047141527070072300256240ustar00rootroot00000000000000#!/bin/bash # Copyright The OpenTelemetry Authors # # 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. # Regenerate python code from opamp protos in # https://github.com/open-telemetry/opamp-spec # # To use, update OPAMP_SPEC_REPO_BRANCH_OR_COMMIT variable below to a commit hash or # tag in opentelemtry-proto repo that you want to build off of. Then, just run # this script to update the proto files. Commit the changes as well as any # fixes needed in the OTLP exporter. # # Optional envars: # OPAMP_SPEC_REPO_DIR - the path to an existing checkout of the opamp-spec repo # Pinned commit/branch/tag for the current version used in the opamp python package. OPAMP_SPEC_REPO_BRANCH_OR_COMMIT="v0.12.0" set -e OPAMP_SPEC_REPO_DIR=${OPAMP_SPEC_REPO_DIR:-"/tmp/opamp-spec"} # root of opentelemetry-python repo repo_root="$(git rev-parse --show-toplevel)" proto_output_dir="$repo_root/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/proto" protoc() { uvx -c $repo_root/opamp-gen-requirements.txt \ --python 3.12 \ --from grpcio-tools \ --with mypy-protobuf \ python -m grpc_tools.protoc "$@" } protoc --version # Clone the proto repo if it doesn't exist if [ ! -d "$OPAMP_SPEC_REPO_DIR" ]; then git clone https://github.com/open-telemetry/opamp-spec.git $OPAMP_SPEC_REPO_DIR fi # Pull in changes and switch to requested branch ( cd $OPAMP_SPEC_REPO_DIR git fetch --all git checkout $OPAMP_SPEC_REPO_BRANCH_OR_COMMIT # pull if OPAMP_SPEC_BRANCH_OR_COMMIT is not a detached head git symbolic-ref -q HEAD && git pull --ff-only || true ) cd $proto_output_dir # clean up old generated code find . -regex ".*_pb2.*\.pyi?" -exec rm {} + # generate proto code for all protos all_protos=$(find $OPAMP_SPEC_REPO_DIR/ -name "*.proto") protoc \ -I $OPAMP_SPEC_REPO_DIR/proto \ --python_out=. \ --mypy_out=. \ $all_protos sed -i -e 's/import anyvalue_pb2 as anyvalue__pb2/from . import anyvalue_pb2 as anyvalue__pb2/' opamp_pb2.py opentelemetry-python-contrib-0.66b2/scripts/otel_packaging.py000066400000000000000000000054641527070072300245660ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import os import subprocess from subprocess import CalledProcessError import tomli scripts_path = os.path.dirname(os.path.abspath(__file__)) root_path = os.path.dirname(scripts_path) instrumentations_path = os.path.join(root_path, "instrumentation") genai_instrumentations_path = os.path.join(root_path, "instrumentation-genai") def get_instrumentation_packages( independent_packages: dict[str, str] | None = None, ): independent_packages = independent_packages or {} pkg_paths = [] for pkg in os.listdir(instrumentations_path): pkg_path = os.path.join(instrumentations_path, pkg) if not os.path.isdir(pkg_path): continue pkg_paths.append(pkg_path) for pkg in os.listdir(genai_instrumentations_path): pkg_path = os.path.join(genai_instrumentations_path, pkg) if not os.path.isdir(pkg_path): continue pkg_paths.append(pkg_path) for pkg_path in sorted(pkg_paths): try: version = subprocess.check_output( "hatch version", shell=True, cwd=pkg_path, universal_newlines=True, ) except CalledProcessError as exc: print(f"Could not get hatch version from path {pkg_path}") print(exc.output) raise exc pyproject_toml_path = os.path.join(pkg_path, "pyproject.toml") with open(pyproject_toml_path, "rb") as file: pyproject_toml = tomli.load(file) optional_dependencies = pyproject_toml["project"]["optional-dependencies"] instruments = optional_dependencies.get("instruments", []) # instruments-any is an optional field that can be used instead of or in addition to instruments. While instruments is a list of dependencies, all of which are expected by the instrumentation, instruments-any is a list any of which but not all are expected. instruments_any = optional_dependencies.get("instruments-any", []) instrumentation = { "name": pyproject_toml["project"]["name"], "version": version.strip(), "instruments": instruments, "instruments-any": instruments_any, } if instrumentation["name"] in independent_packages: specifier = independent_packages[instrumentation["name"]] instrumentation["requirement"] = f"{instrumentation['name']}{specifier}" else: instrumentation["requirement"] = "==".join( ( instrumentation["name"], instrumentation["version"], ) ) yield instrumentation if __name__ == "__main__": print(list(get_instrumentation_packages())) opentelemetry-python-contrib-0.66b2/scripts/prepare_release.sh000077500000000000000000000021711527070072300247320ustar00rootroot00000000000000#!/bin/bash set -e # This script: # 1. parses the version number from the branch name # 2. updates version.py files to match that version # 3. iterates through CHANGELOG.md files and updates any files containing # unreleased changes # 4. sets the output variable 'version_updated' to determine whether # the github action to create a pull request should run. this allows # maintainers to merge changes back into the release branch without # triggering unnecessary pull requests # VERSION=$(./scripts/eachdist.py version --mode stable)-$(./scripts/eachdist.py version --mode prerelease) echo "Using version ${VERSION}" # create the release branch git checkout -b release/${VERSION} git push origin release/${VERSION} ./scripts/eachdist.py update_versions --versions stable,prerelease ./scripts/generate_instrumentation_bootstrap.py rc=$? if [ $rc != 0 ]; then echo "version_updated=0" >> $GITHUB_OUTPUT exit 0 fi git add . git commit -m "updating changelogs and version to ${VERSION}" echo "Time to create a release, here's a sample title:" echo "[pre-release] Update changelogs, version [${VERSION}]" opentelemetry-python-contrib-0.66b2/sdk-extension/000077500000000000000000000000001527070072300223405ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/000077500000000000000000000000001527070072300306155ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/.changelog/000077500000000000000000000000001527070072300326225ustar00rootroot00000000000000.gitignore000066400000000000000000000000141527070072300345260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/.changelog!.gitignore 3761.added000066400000000000000000000001351527070072300341250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/.changelogAdd caching, matching, and targets logic to complete AWS X-Ray Remote Sampler implementation 4030.added000066400000000000000000000001541527070072300341140ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/.changelog`opentelemetry-sdk-extension-aws`: Update xray trace id generator for 'random-trace-id' flags compatibility 4414.fixed000066400000000000000000000001451527070072300341600ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/.changelog`opentelemetry-sdk-extension-aws`: fix EKS detector failing on clusters using the Access Entries API opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/CHANGELOG.md000066400000000000000000000021431527070072300324260ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Version 2.1.0 (2024-12-24) - Make ec2 resource detector silent when loaded outside AWS ([#3120](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3120)) - Make ecs and beanstalk resource detector silent when loaded outside AWS ([#3076](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3076)) - Make EKS resource detector don't warn when not running in EKS ([#3074](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3074)) ## Version 2.0.2 (2024-08-05) See [common CHANGELOG](../../CHANGELOG.md) for the changes in this and prior versions. opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/LICENSE000066400000000000000000000261351527070072300316310ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/MANIFEST.rst000066400000000000000000000002511527070072300325530ustar00rootroot00000000000000graft src graft tests global-exclude *.pyc global-exclude *.pyo global-exclude __pycache__/* include CHANGELOG.md include MANIFEST.in include README.rst include LICENSE opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/README.rst000066400000000000000000000064031527070072300323070ustar00rootroot00000000000000OpenTelemetry SDK Extension for AWS X-Ray Compatibility ======================================================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-sdk-extension-aws.svg :target: https://pypi.org/project/opentelemetry-sdk-extension-aws/ This library provides components necessary to configure the OpenTelemetry SDK for tracing with AWS X-Ray. Installation ------------ :: pip install opentelemetry-sdk-extension-aws Usage (AWS X-Ray IDs Generator) ------------------------------- Configure the OTel SDK TracerProvider with the provided custom IDs Generator to make spans compatible with the AWS X-Ray backend tracing service. Install the OpenTelemetry SDK package. :: pip install opentelemetry-sdk Next, use the provided `AwsXRayIdGenerator` to initialize the `TracerProvider`. .. code-block:: python import opentelemetry.trace as trace from opentelemetry.sdk.extension.aws.trace import AwsXRayIdGenerator from opentelemetry.sdk.trace import TracerProvider trace.set_tracer_provider( TracerProvider(id_generator=AwsXRayIdGenerator()) ) Usage (AWS Resource Detectors) ------------------------------ Use the provided `Resource Detectors` to automatically populate attributes under the `resource` namespace of each generated span. For example, if tracing with OpenTelemetry on an AWS EC2 instance, you can automatically populate `resource` attributes by creating a `TraceProvider` using the `AwsEc2ResourceDetector`: .. code-block:: python import opentelemetry.trace as trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.extension.aws.resource.ec2 import ( AwsEc2ResourceDetector, ) from opentelemetry.sdk.resources import get_aggregated_resources trace.set_tracer_provider( TracerProvider( resource=get_aggregated_resources( [ AwsEc2ResourceDetector(), ] ), ) ) Refer to each detectors' docstring to determine any possible requirements for that detector. Usage (AWS X-Ray Remote Sampler) -------------------------------- Use the provided AWS X-Ray Remote Sampler by setting this sampler in your instrumented application: .. code-block:: python from opentelemetry.sdk.extension.aws.trace.sampler import AwsXRayRemoteSampler from opentelemetry import trace from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.semconv.resource import ResourceAttributes from opentelemetry.util.types import Attributes resource = Resource.create(attributes={ ResourceAttributes.SERVICE_NAME: "myService", ResourceAttributes.CLOUD_PLATFORM: "aws_ec2", }) xraySampler = AwsXRayRemoteSampler(resource=resource, polling_interval=300) trace.set_tracer_provider(TracerProvider(sampler=xraySampler)) References ---------- * `OpenTelemetry Project `_ * `AWS X-Ray Trace IDs Format `_ * `OpenTelemetry Specification for Resource Attributes `_ benchmark-requirements.txt000066400000000000000000000000301527070072300357430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-awspytest-benchmark==4.0.0 opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/benchmarks/000077500000000000000000000000001527070072300327325ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/benchmarks/trace/000077500000000000000000000000001527070072300340305ustar00rootroot00000000000000test_benchmark_aws_xray_ids_generator.py000066400000000000000000000006331527070072300441400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/benchmarks/trace# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.sdk.extension.aws.trace import ( # pylint: disable=no-name-in-module AwsXRayIdGenerator, ) id_generator = AwsXRayIdGenerator() def test_generate_xray_trace_id(benchmark): benchmark(id_generator.generate_trace_id) def test_generate_xray_span_id(benchmark): benchmark(id_generator.generate_span_id) opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/pyproject.toml000066400000000000000000000056271527070072300335430ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-sdk-extension-aws" dynamic = ["version"] description = "AWS SDK extension for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-api ~= 1.26", "opentelemetry-sdk ~= 1.26", "opentelemetry-instrumentation ~= 0.47b0", "opentelemetry-semantic-conventions ~= 0.47b0", "requests ~= 2.28", ] [project.entry-points.opentelemetry_id_generator] xray = "opentelemetry.sdk.extension.aws.trace.aws_xray_id_generator:AwsXRayIdGenerator" [project.entry-points.opentelemetry_resource_detector] aws_ec2 = "opentelemetry.sdk.extension.aws.resource.ec2:AwsEc2ResourceDetector" aws_ecs = "opentelemetry.sdk.extension.aws.resource.ecs:AwsEcsResourceDetector" aws_eks = "opentelemetry.sdk.extension.aws.resource.eks:AwsEksResourceDetector" aws_elastic_beanstalk = "opentelemetry.sdk.extension.aws.resource.beanstalk:AwsBeanstalkResourceDetector" aws_lambda = "opentelemetry.sdk.extension.aws.resource._lambda:AwsLambdaResourceDetector" [project.entry-points.opentelemetry_sampler] aws_xray_remote_sampler = "opentelemetry.sdk.extension.aws.trace.sampler:AwsXRayRemoteSampler" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/sdk-extension/opentelemetry-sdk-extension-aws" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/sdk/extension/aws/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] [tool.towncrier] directory = ".changelog" filename = "CHANGELOG.md" start_string = "\n" template = "../../scripts/changelog_template.j2" issue_format = "[#{issue}](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/{issue})" wrap = true issue_pattern = "^(\\d+)" [[tool.towncrier.type]] directory = "added" name = "Added" showcontent = true [[tool.towncrier.type]] directory = "changed" name = "Changed" showcontent = true [[tool.towncrier.type]] directory = "deprecated" name = "Deprecated" showcontent = true [[tool.towncrier.type]] directory = "removed" name = "Removed" showcontent = true [[tool.towncrier.type]] directory = "fixed" name = "Fixed" showcontent = true opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/000077500000000000000000000000001527070072300314045ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/000077500000000000000000000000001527070072300343005ustar00rootroot00000000000000sdk/000077500000000000000000000000001527070072300350025ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetryextension/000077500000000000000000000000001527070072300370165ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdkaws/000077500000000000000000000000001527070072300376105ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extensionresource/000077500000000000000000000000001527070072300414375ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws__init__.py000066400000000000000000000013231527070072300435470ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint:disable=no-name-in-module from opentelemetry.sdk.extension.aws.resource._lambda import ( AwsLambdaResourceDetector, ) from opentelemetry.sdk.extension.aws.resource.beanstalk import ( AwsBeanstalkResourceDetector, ) from opentelemetry.sdk.extension.aws.resource.ec2 import AwsEc2ResourceDetector from opentelemetry.sdk.extension.aws.resource.ecs import AwsEcsResourceDetector from opentelemetry.sdk.extension.aws.resource.eks import AwsEksResourceDetector __all__ = [ "AwsBeanstalkResourceDetector", "AwsEc2ResourceDetector", "AwsEcsResourceDetector", "AwsEksResourceDetector", "AwsLambdaResourceDetector", ] _lambda.py000066400000000000000000000032711527070072300433730ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging from os import environ from opentelemetry.sdk.resources import Resource, ResourceDetector from opentelemetry.semconv.resource import ( CloudPlatformValues, CloudProviderValues, ResourceAttributes, ) logger = logging.getLogger(__name__) class AwsLambdaResourceDetector(ResourceDetector): """Detects attribute values only available when the app is running on AWS Lambda and returns them in a Resource. Uses Lambda defined runtime environment variables. See more: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime """ def detect(self) -> "Resource": try: return Resource( { ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AWS.value, ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AWS_LAMBDA.value, ResourceAttributes.CLOUD_REGION: environ["AWS_REGION"], ResourceAttributes.FAAS_NAME: environ["AWS_LAMBDA_FUNCTION_NAME"], ResourceAttributes.FAAS_VERSION: environ["AWS_LAMBDA_FUNCTION_VERSION"], ResourceAttributes.FAAS_INSTANCE: environ["AWS_LAMBDA_LOG_STREAM_NAME"], ResourceAttributes.FAAS_MAX_MEMORY: int(environ["AWS_LAMBDA_FUNCTION_MEMORY_SIZE"]), } ) # pylint: disable=broad-except except Exception as exception: if self.raise_on_error: raise exception logger.warning("%s failed: %s", self.__class__.__name__, exception) return Resource.get_empty() beanstalk.py000066400000000000000000000040071527070072300437560ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import logging import os from opentelemetry.sdk.resources import Resource, ResourceDetector from opentelemetry.semconv.resource import ( CloudPlatformValues, CloudProviderValues, ResourceAttributes, ) logger = logging.getLogger(__name__) class AwsBeanstalkResourceDetector(ResourceDetector): """Detects attribute values only available when the app is running on AWS Elastic Beanstalk and returns them in a Resource. NOTE: Requires enabling X-Ray on Beanstalk Environment. See more here: https://docs.aws.amazon.com/xray/latest/devguide/xray-services-beanstalk.html """ def detect(self) -> "Resource": if os.name == "nt": conf_file_path = "C:\\Program Files\\Amazon\\XRay\\environment.conf" else: conf_file_path = "/var/elasticbeanstalk/xray/environment.conf" if not os.path.exists(conf_file_path): return Resource.get_empty() try: with open(conf_file_path, encoding="utf-8") as conf_file: parsed_data = json.load(conf_file) return Resource( { ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AWS.value, ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AWS_ELASTIC_BEANSTALK.value, ResourceAttributes.SERVICE_NAME: CloudPlatformValues.AWS_ELASTIC_BEANSTALK.value, ResourceAttributes.SERVICE_INSTANCE_ID: parsed_data["deployment_id"], ResourceAttributes.SERVICE_NAMESPACE: parsed_data["environment_name"], ResourceAttributes.SERVICE_VERSION: parsed_data["version_label"], } ) # pylint: disable=broad-except except Exception as exception: if self.raise_on_error: raise exception logger.warning("%s failed: %s", self.__class__.__name__, exception) return Resource.get_empty() ec2.py000066400000000000000000000064401527070072300424660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import logging from urllib.error import URLError from urllib.request import Request, urlopen from opentelemetry.sdk.resources import Resource, ResourceDetector from opentelemetry.semconv.resource import ( CloudPlatformValues, CloudProviderValues, ResourceAttributes, ) logger = logging.getLogger(__name__) _AWS_METADATA_TOKEN_HEADER = "X-aws-ec2-metadata-token" _GET_METHOD = "GET" _AWS_METADATA_HOST = "169.254.169.254" def _aws_http_request(method, path, headers, timeout=None): if timeout is None: timeout = 5 with urlopen( Request( "http://" + _AWS_METADATA_HOST + path, headers=headers, method=method, ), timeout=timeout, ) as response: return response.read().decode("utf-8") def _get_token(timeout=None): return _aws_http_request( "PUT", "/latest/api/token", {"X-aws-ec2-metadata-token-ttl-seconds": "60"}, timeout, ) def _get_identity(token, timeout=None): return _aws_http_request( _GET_METHOD, "/latest/dynamic/instance-identity/document", {_AWS_METADATA_TOKEN_HEADER: token}, timeout, ) def _get_host(token, timeout=None): return _aws_http_request( _GET_METHOD, "/latest/meta-data/hostname", {_AWS_METADATA_TOKEN_HEADER: token}, timeout, ) class AwsEc2ResourceDetector(ResourceDetector): """Detects attribute values only available when the app is running on AWS Elastic Compute Cloud (EC2) and returns them in a Resource. Uses a special URI to get instance meta-data. See more: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html """ def detect(self) -> "Resource": try: # If can't get a token quick assume we are not on ec2 try: token = _get_token(timeout=1) except URLError as exception: logger.debug( "%s failed to get token: %s", self.__class__.__name__, exception, ) return Resource.get_empty() identity_dict = json.loads(_get_identity(token)) hostname = _get_host(token) return Resource( { ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AWS.value, ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AWS_EC2.value, ResourceAttributes.CLOUD_ACCOUNT_ID: identity_dict["accountId"], ResourceAttributes.CLOUD_REGION: identity_dict["region"], ResourceAttributes.CLOUD_AVAILABILITY_ZONE: identity_dict["availabilityZone"], ResourceAttributes.HOST_ID: identity_dict["instanceId"], ResourceAttributes.HOST_TYPE: identity_dict["instanceType"], ResourceAttributes.HOST_NAME: hostname, } ) # pylint: disable=broad-except except Exception as exception: if self.raise_on_error: raise exception logger.warning("%s failed: %s", self.__class__.__name__, exception) return Resource.get_empty() ecs.py000066400000000000000000000134331527070072300425670ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import logging import os import re import socket from urllib.request import Request, urlopen from opentelemetry.sdk.resources import Resource, ResourceDetector from opentelemetry.semconv.resource import ( CloudPlatformValues, CloudProviderValues, ResourceAttributes, ) logger = logging.getLogger(__name__) _CONTAINER_ID_LENGTH = 64 class AwsEcsResourceDetector(ResourceDetector): """Detects attribute values only available when the app is running on AWS Elastic Container Service (ECS) and returns them in a Resource. """ def detect(self) -> "Resource": try: if not os.environ.get("ECS_CONTAINER_METADATA_URI") and not os.environ.get("ECS_CONTAINER_METADATA_URI_V4"): return Resource.get_empty() container_id = "" try: with open("/proc/self/cgroup", encoding="utf8") as container_info_file: for raw_line in container_info_file: line = raw_line.strip() # Subsequent IDs should be the same, exit if found one if len(line) > _CONTAINER_ID_LENGTH: container_id = line[-_CONTAINER_ID_LENGTH:] break except FileNotFoundError as exception: logger.warning("Failed to get container ID on ECS: %s.", exception) base_resource = Resource( { ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AWS.value, ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AWS_ECS.value, ResourceAttributes.CONTAINER_NAME: socket.gethostname(), ResourceAttributes.CONTAINER_ID: container_id, } ) metadata_v4_endpoint = os.environ.get("ECS_CONTAINER_METADATA_URI_V4") if not metadata_v4_endpoint: return base_resource # Returns https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4.html#task-metadata-endpoint-v4-response metadata_container = json.loads(_http_get(metadata_v4_endpoint)) metadata_task = json.loads(_http_get(f"{metadata_v4_endpoint}/task")) task_arn = metadata_task["TaskARN"] base_arn = task_arn[0 : task_arn.rindex(":")] cluster: str = metadata_task["Cluster"] cluster_arn = cluster if cluster.startswith("arn:") else f"{base_arn}:cluster/{cluster}" logs_resource = _get_logs_resource(metadata_container) return base_resource.merge(logs_resource).merge( Resource( { ResourceAttributes.AWS_ECS_CONTAINER_ARN: metadata_container["ContainerARN"], ResourceAttributes.AWS_ECS_CLUSTER_ARN: cluster_arn, ResourceAttributes.AWS_ECS_LAUNCHTYPE: metadata_task["LaunchType"].lower(), ResourceAttributes.AWS_ECS_TASK_ARN: task_arn, ResourceAttributes.AWS_ECS_TASK_FAMILY: metadata_task["Family"], ResourceAttributes.AWS_ECS_TASK_REVISION: metadata_task["Revision"], } ) ) # pylint: disable=broad-except except Exception as exception: if self.raise_on_error: raise exception logger.warning("%s failed: %s", self.__class__.__name__, exception) return Resource.get_empty() def _get_logs_resource(metadata_container): if metadata_container.get("LogDriver") == "awslogs": log_options = metadata_container.get("LogOptions") if log_options: logs_region = log_options.get("awslogs-region") logs_group_name = log_options.get("awslogs-group") logs_stream_name = log_options.get("awslogs-stream") container_arn = metadata_container["ContainerARN"] if not logs_region: aws_region_match = re.match(r"arn:aws:ecs:([^:]+):.*", container_arn) if aws_region_match: logs_region = aws_region_match.group(1) else: logger.warning("Cannot parse AWS region out of ECS ARN") # We need to retrieve the account ID from some other ARN to create the # log-group and log-stream ARNs aws_account = None aws_account_match = re.match(r"arn:aws:ecs:[^:]+:([^:]+):.*", container_arn) if aws_account_match: aws_account = aws_account_match.group(1) logs_group_arn = None logs_stream_arn = None if logs_region and aws_account: if logs_group_name: logs_group_arn = f"arn:aws:logs:{logs_region}:{aws_account}:log-group:{logs_group_name}" if logs_stream_name: logs_stream_arn = f"arn:aws:logs:{logs_region}:{aws_account}:log-group:{logs_group_name}:log-stream:{logs_stream_name}" return Resource( { ResourceAttributes.AWS_LOG_GROUP_NAMES: [logs_group_name], ResourceAttributes.AWS_LOG_GROUP_ARNS: [logs_group_arn], ResourceAttributes.AWS_LOG_STREAM_NAMES: [logs_stream_name], ResourceAttributes.AWS_LOG_STREAM_ARNS: [logs_stream_arn], } ) logger.warning( "The metadata endpoint v4 has returned 'awslogs' as 'LogDriver', but there is no 'LogOptions' data" ) return Resource.get_empty() def _http_get(url): with urlopen( Request(url, method="GET"), timeout=5, ) as response: return response.read().decode("utf-8") eks.py000066400000000000000000000110501527070072300425700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import base64 import json import logging import os import re import ssl from urllib.request import Request, urlopen from opentelemetry.sdk.resources import Resource, ResourceDetector from opentelemetry.semconv.resource import ( CloudPlatformValues, CloudProviderValues, ResourceAttributes, ) logger = logging.getLogger(__name__) _CONTAINER_ID_LENGTH = 64 _GET_METHOD = "GET" _TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token" _CERT_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" _EKS_OIDC_RE = re.compile( r"^https://oidc\.eks\.[^.]+\.amazonaws\.com(?:\.cn)?/id/[A-F0-9]{32}$", re.ASCII, ) def _aws_http_request(method, path, cred_value): with urlopen( Request( "https://kubernetes.default.svc" + path, headers={"Authorization": cred_value}, method=method, ), timeout=5, context=ssl.create_default_context( cafile=_CERT_PATH, ), ) as response: return response.read().decode("utf-8") def _get_k8s_cred_value(): try: with open(_TOKEN_PATH, encoding="utf8") as token_file: return "Bearer " + token_file.read() # pylint: disable=broad-except except Exception as exception: logger.error("Failed to get k8s token: %s", exception) raise exception def _is_eks(cred_value): parts = cred_value.removeprefix("Bearer ").split(".") if len(parts) != 3: return False try: seg = parts[1] payload = json.loads(base64.urlsafe_b64decode(seg + "=" * (-len(seg) % 4))) except ValueError as exception: logger.error("Failed to parse JWT for EKS detection: %s", exception) return False return bool(_EKS_OIDC_RE.match(payload.get("iss", ""))) def _get_cluster_info(cred_value): return _aws_http_request( _GET_METHOD, "/api/v1/namespaces/amazon-cloudwatch/configmaps/cluster-info", cred_value, ) def _get_cluster_name(cred_value) -> str: cluster_info = json.loads(_get_cluster_info(cred_value)) cluster_name = "" try: cluster_name = cluster_info["data"]["cluster.name"] except KeyError as exception: logger.warning("Cannot get cluster name on EKS: %s", exception) return cluster_name def _get_container_id(): container_id = "" with open("/proc/self/cgroup", encoding="utf8") as container_info_file: for raw_line in container_info_file: line = raw_line.strip() # Subsequent IDs should be the same, exit if found one if len(line) > _CONTAINER_ID_LENGTH: container_id = line[-_CONTAINER_ID_LENGTH:] break return container_id def _is_k8s() -> bool: return os.path.exists(_TOKEN_PATH) and os.path.exists(_CERT_PATH) class AwsEksResourceDetector(ResourceDetector): """Detects attribute values only available when the app is running on AWS Elastic Kubernetes Service (EKS) and returns them in a Resource. NOTE: Uses a `cluster-info` configmap in the `amazon-cloudwatch` namespace. See more here: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Container-Insights-setup-EKS-quickstart.html#Container-Insights-setup-EKS-quickstart-Fluentd """ def detect(self) -> "Resource": try: # if we are not running on eks exit early without warnings if not _is_k8s(): return Resource.get_empty() cred_value = _get_k8s_cred_value() if not _is_eks(cred_value): raise RuntimeError("Could not confirm process is running on EKS.") cluster_name = _get_cluster_name(cred_value) container_id = _get_container_id() if not container_id and not cluster_name: raise RuntimeError("Neither cluster name nor container ID found on EKS process.") return Resource( { ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AWS.value, ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AWS_EKS.value, ResourceAttributes.K8S_CLUSTER_NAME: cluster_name, ResourceAttributes.CONTAINER_ID: container_id, } ) # pylint: disable=broad-except except Exception as exception: if self.raise_on_error: raise exception logger.warning("%s failed: %s", self.__class__.__name__, exception) return Resource.get_empty() trace/000077500000000000000000000000001527070072300407065ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws__init__.py000066400000000000000000000003661527070072300430240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint:disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace.aws_xray_id_generator import ( AwsXRayIdGenerator, ) __all__ = ["AwsXRayIdGenerator"] aws_xray_id_generator.py000066400000000000000000000042301527070072300456360ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Installation ------------ :: pip install opentelemetry-sdk-extension-aws AWS X-Ray IDs Generator ----------------------- The **AWS X-Ray IDs Generator** provides a custom IDs Generator to make traces generated using the OpenTelemetry SDKs `TracerProvider` compatible with the AWS X-Ray backend service `trace ID format`_. Usage ----- Configure the OTel SDK TracerProvider with the provided custom IDs Generator to make spans compatible with the AWS X-Ray backend tracing service. Install the OpenTelemetry SDK package. :: pip install opentelemetry-sdk Next, use the provided `AwsXRayIdGenerator` to initialize the `TracerProvider`. .. code-block:: python import opentelemetry.trace as trace from opentelemetry.sdk.extension.aws.trace import AwsXRayIdGenerator from opentelemetry.sdk.trace import TracerProvider trace.set_tracer_provider(TracerProvider(id_generator=AwsXRayIdGenerator())) API --- .. _trace ID format: https://docs.aws.amazon.com/xray/latest/devguide/xray-api-sendingdata.html#xray-api-traceids """ import random import time from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator class AwsXRayIdGenerator(IdGenerator): """Generates tracing IDs compatible with the AWS X-Ray tracing service. In the X-Ray system, the first 32 bits of the `TraceId` are the Unix epoch time in seconds. Since spans (AWS calls them segments) with an embedded timestamp more than 30 days ago are rejected, a purely random `TraceId` risks being rejected by the service. See: https://docs.aws.amazon.com/xray/latest/devguide/xray-api-sendingdata.html#xray-api-traceids """ random_id_generator = RandomIdGenerator() def generate_span_id(self) -> int: return self.random_id_generator.generate_span_id() # pylint: disable=no-self-use def generate_trace_id(self) -> int: trace_time = int(time.time()) trace_identifier = random.getrandbits(96) return (trace_time << 96) + trace_identifier # pylint: disable=no-self-use def is_trace_id_random(self) -> bool: return True sampler/000077500000000000000000000000001527070072300423515ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace__init__.py000066400000000000000000000004051527070072300444610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace.sampler.aws_xray_remote_sampler import ( AwsXRayRemoteSampler, ) __all__ = ["AwsXRayRemoteSampler"] _aws_xray_sampling_client.py000066400000000000000000000110401527070072300501430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import json from logging import getLogger import requests # pylint: disable=no-name-in-module from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.sdk.extension.aws.trace.sampler._sampling_rule import ( _SamplingRule, ) from opentelemetry.sdk.extension.aws.trace.sampler._sampling_target import ( _SamplingTargetResponse, ) _logger = getLogger(__name__) DEFAULT_SAMPLING_PROXY_ENDPOINT = "http://127.0.0.1:2000" class _AwsXRaySamplingClient: # pyright: ignore[reportUnusedClass] def __init__( self, endpoint: str = DEFAULT_SAMPLING_PROXY_ENDPOINT, log_level: str | None = None, ): # Override default log level if log_level is not None: _logger.setLevel(log_level) self.__get_sampling_rules_endpoint = endpoint + "/GetSamplingRules" self.__get_sampling_targets_endpoint = endpoint + "/SamplingTargets" self.__session = requests.Session() def get_sampling_rules(self) -> list[_SamplingRule]: sampling_rules: list[_SamplingRule] = [] headers = {"content-type": "application/json"} with suppress_instrumentation(): try: xray_response = self.__session.post( url=self.__get_sampling_rules_endpoint, headers=headers, timeout=20, ) sampling_rules_response = xray_response.json() if sampling_rules_response is None or "SamplingRuleRecords" not in sampling_rules_response: _logger.error( "SamplingRuleRecords is missing in getSamplingRules response: %s", sampling_rules_response, ) return [] sampling_rules_records = sampling_rules_response["SamplingRuleRecords"] for record in sampling_rules_records: if "SamplingRule" not in record: _logger.error("SamplingRule is missing in SamplingRuleRecord") else: sampling_rules.append(_SamplingRule(**record["SamplingRule"])) except requests.exceptions.RequestException as req_err: _logger.error("Request error occurred: %s", req_err) except json.JSONDecodeError as json_err: _logger.error("Error in decoding JSON response: %s", json_err) # pylint: disable=broad-exception-caught except Exception as err: _logger.error("Error occurred when attempting to fetch rules: %s", err) return sampling_rules def get_sampling_targets(self, statistics: list[dict[str, str | float | int]]) -> _SamplingTargetResponse: sampling_targets_response = _SamplingTargetResponse( LastRuleModification=None, SamplingTargetDocuments=None, UnprocessedStatistics=None, ) headers = {"content-type": "application/json"} with suppress_instrumentation(): try: xray_response = self.__session.post( url=self.__get_sampling_targets_endpoint, headers=headers, timeout=20, json={"SamplingStatisticsDocuments": statistics}, ) xray_response_json = xray_response.json() if ( xray_response_json is None or "SamplingTargetDocuments" not in xray_response_json or "LastRuleModification" not in xray_response_json ): _logger.debug("getSamplingTargets response is invalid. Unable to update targets.") return sampling_targets_response sampling_targets_response = _SamplingTargetResponse(**xray_response_json) except requests.exceptions.RequestException as req_err: _logger.debug("Request error occurred: %s", req_err) except json.JSONDecodeError as json_err: _logger.debug("Error in decoding JSON response: %s", json_err) # pylint: disable=broad-exception-caught except Exception as err: _logger.debug("Error occurred when attempting to fetch targets: %s", err) return sampling_targets_response _clock.py000066400000000000000000000014131527070072300441540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import datetime class _Clock: # pyright: ignore[reportUnusedClass] def __init__(self): self.__datetime = datetime.datetime def now(self) -> datetime.datetime: return self.__datetime.now() # pylint: disable=no-self-use def from_timestamp(self, timestamp: float) -> datetime.datetime: return datetime.datetime.fromtimestamp(timestamp) def time_delta(self, seconds: float) -> datetime.timedelta: return datetime.timedelta(seconds=seconds) def max(self) -> datetime.datetime: return datetime.datetime.max _fallback_sampler.py000066400000000000000000000042651527070072300463530ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Sequence # pylint: disable=no-name-in-module from opentelemetry.context import Context from opentelemetry.sdk.extension.aws.trace.sampler._clock import _Clock from opentelemetry.sdk.extension.aws.trace.sampler._rate_limiting_sampler import ( _RateLimitingSampler, ) from opentelemetry.sdk.trace.sampling import ( Decision, Sampler, SamplingResult, TraceIdRatioBased, ) from opentelemetry.trace import Link, SpanKind from opentelemetry.trace.span import TraceState from opentelemetry.util.types import Attributes class _FallbackSampler(Sampler): # pyright: ignore[reportUnusedClass] def __init__(self, clock: _Clock): self.__rate_limiting_sampler = _RateLimitingSampler(1, clock) self.__fixed_rate_sampler = TraceIdRatioBased(0.05) def should_sample( self, parent_context: Context | None, trace_id: int, name: str, kind: SpanKind | None = None, attributes: Attributes | None = None, links: Sequence[Link] | None = None, trace_state: TraceState | None = None, ) -> SamplingResult: sampling_result = self.__rate_limiting_sampler.should_sample( parent_context, trace_id, name, kind=kind, attributes=attributes, links=links, trace_state=trace_state, ) if sampling_result.decision is not Decision.DROP: return sampling_result return self.__fixed_rate_sampler.should_sample( parent_context, trace_id, name, kind=kind, attributes=attributes, links=links, trace_state=trace_state, ) # pylint: disable=no-self-use def get_description(self) -> str: description = ( "FallbackSampler{fallback sampling with sampling config of 1 req/sec and 5% of additional requests}" ) return description _matcher.py000066400000000000000000000053711527070072300445130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import re from opentelemetry.semconv._incubating.attributes.cloud_attributes import ( CloudPlatformValues, ) from opentelemetry.util.types import Attributes, AttributeValue cloud_platform_mapping = { CloudPlatformValues.AWS_LAMBDA.value: "AWS::Lambda::Function", CloudPlatformValues.AWS_ELASTIC_BEANSTALK.value: "AWS::ElasticBeanstalk::Environment", CloudPlatformValues.AWS_EC2.value: "AWS::EC2::Instance", CloudPlatformValues.AWS_ECS.value: "AWS::ECS::Container", CloudPlatformValues.AWS_EKS.value: "AWS::EKS::Container", } class _Matcher: @staticmethod def wild_card_match(text: AttributeValue | None = None, pattern: str | None = None) -> bool: if pattern == "*": return True if not isinstance(text, str) or pattern is None: return False if len(pattern) == 0: return len(text) == 0 for char in pattern: if char in ("*", "?"): return re.fullmatch(_Matcher.to_regex_pattern(pattern), text) is not None return pattern == text @staticmethod def to_regex_pattern(rule_pattern: str) -> str: token_start = -1 regex_pattern = "" for index, char in enumerate(rule_pattern): char = rule_pattern[index] if char in ("*", "?"): if token_start != -1: regex_pattern += re.escape(rule_pattern[token_start:index]) token_start = -1 if char == "*": regex_pattern += ".*" else: regex_pattern += "." else: if token_start == -1: token_start = index if token_start != -1: regex_pattern += re.escape(rule_pattern[token_start:]) return regex_pattern @staticmethod def attribute_match( attributes: Attributes | None = None, rule_attributes: dict[str, str] | None = None, ) -> bool: if rule_attributes is None or len(rule_attributes) == 0: return True if attributes is None or len(attributes) == 0 or len(rule_attributes) > len(attributes): return False matched_count = 0 for key, val in attributes.items(): text_to_match = val pattern = rule_attributes.get(key, None) if pattern is None: continue if _Matcher.wild_card_match(text_to_match, pattern): matched_count += 1 return matched_count == len(rule_attributes) _rate_limiter.py000066400000000000000000000034151527070072300455450ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from decimal import Decimal from threading import Lock # pylint: disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace.sampler._clock import _Clock class _RateLimiter: # pyright: ignore[reportUnusedClass] def __init__(self, max_balance_in_seconds: int, quota: int, clock: _Clock): # max_balance_in_seconds is usually 1 # pylint: disable=invalid-name self.MAX_BALANCE_MILLIS = Decimal(max_balance_in_seconds * 1000.0) self._clock = clock self._quota = Decimal(quota) self.__wallet_floor_millis = Decimal(self._clock.now().timestamp() * 1000.0) # current "wallet_balance" would be ceiling - floor self.__lock = Lock() def try_spend(self, cost: float) -> bool: if self._quota == 0: return False quota_per_millis = self._quota / Decimal(1000.0) # assume divide by zero not possible cost_in_millis = Decimal(cost) / quota_per_millis with self.__lock: wallet_ceiling_millis = Decimal(self._clock.now().timestamp() * 1000.0) current_balance_millis = wallet_ceiling_millis - self.__wallet_floor_millis current_balance_millis = min(current_balance_millis, self.MAX_BALANCE_MILLIS) pending_remaining_balance_millis = current_balance_millis - cost_in_millis if pending_remaining_balance_millis >= 0: self.__wallet_floor_millis = wallet_ceiling_millis - pending_remaining_balance_millis return True # No changes to the wallet state return False _rate_limiting_sampler.py000066400000000000000000000035151527070072300474400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Sequence # pylint: disable=no-name-in-module from opentelemetry.context import Context from opentelemetry.sdk.extension.aws.trace.sampler._clock import _Clock from opentelemetry.sdk.extension.aws.trace.sampler._rate_limiter import ( _RateLimiter, ) from opentelemetry.sdk.trace.sampling import Decision, Sampler, SamplingResult from opentelemetry.trace import Link, SpanKind from opentelemetry.trace.span import TraceState from opentelemetry.util.types import Attributes class _RateLimitingSampler(Sampler): # pyright: ignore[reportUnusedClass] def __init__(self, quota: int, clock: _Clock): self.__quota = quota self.__reservoir = _RateLimiter(1, quota, clock) def should_sample( self, parent_context: Context | None, trace_id: int, name: str, kind: SpanKind | None = None, attributes: Attributes | None = None, links: Sequence[Link] | None = None, trace_state: TraceState | None = None, ) -> SamplingResult: if self.__reservoir.try_spend(1): return SamplingResult( decision=Decision.RECORD_AND_SAMPLE, attributes=attributes, trace_state=trace_state, ) return SamplingResult( decision=Decision.DROP, attributes=attributes, trace_state=trace_state, ) def get_description(self) -> str: description = f"RateLimitingSampler{{rate limiting sampling with sampling config of {self.__quota} req/sec and 0% of additional requests}}" return description _rule_cache.py000066400000000000000000000147231527070072300451630ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Sequence from logging import getLogger from threading import Lock # pylint: disable=no-name-in-module from opentelemetry.context import Context from opentelemetry.sdk.extension.aws.trace.sampler._clock import _Clock from opentelemetry.sdk.extension.aws.trace.sampler._fallback_sampler import ( _FallbackSampler, ) from opentelemetry.sdk.extension.aws.trace.sampler._sampling_rule import ( _SamplingRule, ) from opentelemetry.sdk.extension.aws.trace.sampler._sampling_rule_applier import ( _SamplingRuleApplier, ) from opentelemetry.sdk.extension.aws.trace.sampler._sampling_target import ( _SamplingTarget, _SamplingTargetResponse, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace.sampling import SamplingResult from opentelemetry.trace import Link, SpanKind from opentelemetry.trace.span import TraceState from opentelemetry.util.types import Attributes _logger = getLogger(__name__) CACHE_TTL_SECONDS = 3600 DEFAULT_TARGET_POLLING_INTERVAL_SECONDS = 10 class _RuleCache: # pyright: ignore[reportUnusedClass] def __init__( self, resource: Resource, fallback_sampler: _FallbackSampler, client_id: str, clock: _Clock, lock: Lock, ): self.__client_id = client_id self.__rule_appliers: list[_SamplingRuleApplier] = [] self.__cache_lock = lock self.__resource = resource self._fallback_sampler = fallback_sampler self._clock = clock self._last_modified = self._clock.now() def should_sample( self, parent_context: Context | None, trace_id: int, name: str, kind: SpanKind | None = None, attributes: Attributes | None = None, links: Sequence[Link] | None = None, trace_state: TraceState | None = None, ) -> SamplingResult: rule_applier: _SamplingRuleApplier for rule_applier in self.__rule_appliers: if rule_applier.matches(self.__resource, attributes): return rule_applier.should_sample( parent_context, trace_id, name, kind=kind, attributes=attributes, links=links, trace_state=trace_state, ) _logger.debug("No sampling rules were matched") # Should not ever reach fallback sampler as default rule is able to match return self._fallback_sampler.should_sample( parent_context, trace_id, name, kind=kind, attributes=attributes, links=links, trace_state=trace_state, ) def update_sampling_rules(self, new_sampling_rules: list[_SamplingRule]) -> None: new_sampling_rules.sort() temp_rule_appliers: list[_SamplingRuleApplier] = [] for sampling_rule in new_sampling_rules: if sampling_rule.RuleName == "": _logger.debug("sampling rule without rule name is not supported") continue if sampling_rule.Version != 1: _logger.debug( "sampling rule without Version 1 is not supported: RuleName: %s", sampling_rule.RuleName, ) continue temp_rule_appliers.append(_SamplingRuleApplier(sampling_rule, self.__client_id, self._clock)) with self.__cache_lock: # map list of rule appliers by each applier's sampling_rule name rule_applier_map: dict[str, _SamplingRuleApplier] = { applier.sampling_rule.RuleName: applier for applier in self.__rule_appliers } # If a sampling rule has not changed, keep its respective applier in the cache. new_applier: _SamplingRuleApplier for index, new_applier in enumerate(temp_rule_appliers): rule_name_to_check = new_applier.sampling_rule.RuleName if rule_name_to_check in rule_applier_map: old_applier = rule_applier_map[rule_name_to_check] if new_applier.sampling_rule == old_applier.sampling_rule: temp_rule_appliers[index] = old_applier self.__rule_appliers = temp_rule_appliers self._last_modified = self._clock.now() def update_sampling_targets(self, sampling_targets_response: _SamplingTargetResponse): targets: list[_SamplingTarget] = sampling_targets_response.SamplingTargetDocuments with self.__cache_lock: next_polling_interval = DEFAULT_TARGET_POLLING_INTERVAL_SECONDS min_polling_interval = None target_map: dict[str, _SamplingTarget] = {target.RuleName: target for target in targets} new_appliers: list[_SamplingRuleApplier] = [] applier: _SamplingRuleApplier for applier in self.__rule_appliers: if applier.sampling_rule.RuleName in target_map: target = target_map[applier.sampling_rule.RuleName] new_appliers.append(applier.with_target(target)) if target.Interval is not None: if min_polling_interval is None or min_polling_interval > target.Interval: min_polling_interval = target.Interval else: new_appliers.append(applier) self.__rule_appliers = new_appliers if min_polling_interval is not None: next_polling_interval = min_polling_interval last_rule_modification = self._clock.from_timestamp(sampling_targets_response.LastRuleModification) refresh_rules = last_rule_modification > self._last_modified return (refresh_rules, next_polling_interval) def get_all_statistics(self): all_statistics: list[dict[str, str | float | int]] = [] applier: _SamplingRuleApplier for applier in self.__rule_appliers: all_statistics.append(applier.get_then_reset_statistics()) return all_statistics def expired(self) -> bool: with self.__cache_lock: return self._clock.now() > self._last_modified + self._clock.time_delta(seconds=CACHE_TTL_SECONDS) _sampling_rule.py000066400000000000000000000056611527070072300457330ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations # Disable snake_case naming style so this class can match the sampling rules response from X-Ray # pylint: disable=invalid-name,too-many-positional-arguments class _SamplingRule: def __init__( self, Attributes: dict[str, str] | None = None, FixedRate: float | None = None, HTTPMethod: str | None = None, Host: str | None = None, Priority: int | None = None, ReservoirSize: int | None = None, ResourceARN: str | None = None, RuleARN: str | None = None, RuleName: str | None = None, ServiceName: str | None = None, ServiceType: str | None = None, URLPath: str | None = None, Version: int | None = None, ): self.Attributes = Attributes if Attributes is not None else {} self.FixedRate = FixedRate if FixedRate is not None else 0.0 self.HTTPMethod = HTTPMethod if HTTPMethod is not None else "" self.Host = Host if Host is not None else "" # Default to value with lower priority than default rule self.Priority = Priority if Priority is not None else 10001 self.ReservoirSize = ReservoirSize if ReservoirSize is not None else 0 self.ResourceARN = ResourceARN if ResourceARN is not None else "" self.RuleARN = RuleARN if RuleARN is not None else "" self.RuleName = RuleName if RuleName is not None else "" self.ServiceName = ServiceName if ServiceName is not None else "" self.ServiceType = ServiceType if ServiceType is not None else "" self.URLPath = URLPath if URLPath is not None else "" self.Version = Version if Version is not None else 0 def __lt__(self, other: _SamplingRule) -> bool: if self.Priority == other.Priority: # String order priority example: # "A","Abc","a","ab","abc","abcdef" return self.RuleName < other.RuleName return self.Priority < other.Priority def __eq__(self, other: object) -> bool: if not isinstance(other, _SamplingRule): return False return ( self.FixedRate == other.FixedRate and self.HTTPMethod == other.HTTPMethod and self.Host == other.Host and self.Priority == other.Priority and self.ReservoirSize == other.ReservoirSize and self.ResourceARN == other.ResourceARN and self.RuleARN == other.RuleARN and self.RuleName == other.RuleName and self.ServiceName == other.ServiceName and self.ServiceType == other.ServiceType and self.URLPath == other.URLPath and self.Version == other.Version and self.Attributes == other.Attributes ) _sampling_rule_applier.py000066400000000000000000000244071527070072300474460ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Sequence from threading import Lock from urllib.parse import urlparse # pylint: disable=no-name-in-module from opentelemetry.context import Context from opentelemetry.sdk.extension.aws.trace.sampler._clock import _Clock from opentelemetry.sdk.extension.aws.trace.sampler._matcher import ( _Matcher, cloud_platform_mapping, ) from opentelemetry.sdk.extension.aws.trace.sampler._rate_limiting_sampler import ( _RateLimitingSampler, ) from opentelemetry.sdk.extension.aws.trace.sampler._sampling_rule import ( _SamplingRule, ) from opentelemetry.sdk.extension.aws.trace.sampler._sampling_statistics_document import ( _SamplingStatisticsDocument, ) from opentelemetry.sdk.extension.aws.trace.sampler._sampling_target import ( _SamplingTarget, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace.sampling import ( Decision, Sampler, SamplingResult, TraceIdRatioBased, ) from opentelemetry.semconv._incubating.attributes.aws_attributes import ( AWS_ECS_CONTAINER_ARN, ) from opentelemetry.semconv._incubating.attributes.cloud_attributes import ( CLOUD_PLATFORM, CLOUD_RESOURCE_ID, CloudPlatformValues, ) from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_REQUEST_METHOD, ) from opentelemetry.semconv._incubating.attributes.server_attributes import ( SERVER_ADDRESS, ) from opentelemetry.semconv._incubating.attributes.service_attributes import ( SERVICE_NAME, ) from opentelemetry.semconv._incubating.attributes.url_attributes import ( URL_FULL, URL_PATH, ) from opentelemetry.trace import Link, SpanKind from opentelemetry.trace.span import TraceState from opentelemetry.util.types import Attributes, AttributeValue class _SamplingRuleApplier: def __init__( self, sampling_rule: _SamplingRule, client_id: str, clock: _Clock, statistics: _SamplingStatisticsDocument | None = None, target: _SamplingTarget | None = None, ): self.__client_id = client_id self._clock = clock self.sampling_rule = sampling_rule if statistics is None: self.__statistics = _SamplingStatisticsDocument(self.__client_id, self.sampling_rule.RuleName) else: self.__statistics = statistics self.__statistics_lock = Lock() self.__borrowing = False if target is None: self.__fixed_rate_sampler = TraceIdRatioBased(self.sampling_rule.FixedRate) # Until targets are fetched, initialize as borrowing=True if there will be a quota > 0 if self.sampling_rule.ReservoirSize > 0: self.__reservoir_sampler = self.__create_reservoir_sampler(quota=1) self.__borrowing = True else: self.__reservoir_sampler = self.__create_reservoir_sampler(quota=0) # No targets are present, borrow until the end of time if there is any quota self.__reservoir_expiry = self._clock.max() else: new_quota = target.ReservoirQuota if target.ReservoirQuota is not None else 0 new_fixed_rate = target.FixedRate self.__reservoir_sampler = self.__create_reservoir_sampler(quota=new_quota) self.__fixed_rate_sampler = TraceIdRatioBased(new_fixed_rate) if target.ReservoirQuotaTTL is not None: self.__reservoir_expiry = self._clock.from_timestamp(target.ReservoirQuotaTTL) else: # assume expired if no TTL self.__reservoir_expiry = self._clock.now() def should_sample( self, parent_context: Context | None, trace_id: int, name: str, kind: SpanKind | None = None, attributes: Attributes | None = None, links: Sequence[Link] | None = None, trace_state: TraceState | None = None, ) -> SamplingResult: has_borrowed = False has_sampled = False sampling_result = SamplingResult( decision=Decision.DROP, attributes=attributes, trace_state=trace_state, ) reservoir_expired: bool = self._clock.now() >= self.__reservoir_expiry if not reservoir_expired: sampling_result = self.__reservoir_sampler.should_sample( parent_context, trace_id, name, kind=kind, attributes=attributes, links=links, trace_state=trace_state, ) if sampling_result.decision is not Decision.DROP: has_borrowed = self.__borrowing has_sampled = True else: sampling_result = self.__fixed_rate_sampler.should_sample( parent_context, trace_id, name, kind=kind, attributes=attributes, links=links, trace_state=trace_state, ) if sampling_result.decision is not Decision.DROP: has_sampled = True with self.__statistics_lock: self.__statistics.RequestCount += 1 self.__statistics.BorrowCount += 1 if has_borrowed else 0 self.__statistics.SampleCount += 1 if has_sampled else 0 return sampling_result def get_then_reset_statistics(self): with self.__statistics_lock: old_stats = self.__statistics self.__statistics = _SamplingStatisticsDocument(self.__client_id, self.sampling_rule.RuleName) return old_stats.snapshot(self._clock) def with_target(self, target: _SamplingTarget) -> _SamplingRuleApplier: new_applier = _SamplingRuleApplier( self.sampling_rule, self.__client_id, self._clock, self.__statistics, target, ) return new_applier def matches(self, resource: Resource, attributes: Attributes) -> bool: url_path: AttributeValue | None = None url_full: AttributeValue | None = None http_request_method: AttributeValue | None = None server_address: AttributeValue | None = None service_name: AttributeValue | None = None if attributes is not None: # If `URL_PATH/URL_FULL/HTTP_REQUEST_METHOD/SERVER_ADDRESS` are not populated # also check `HTTP_TARGET/HTTP_URL/HTTP_METHOD/HTTP_HOST` respectively as backup url_path = attributes.get( URL_PATH, attributes.get("http.target", None), ) url_full = attributes.get( URL_FULL, attributes.get("http.url", None), ) http_request_method = attributes.get( HTTP_REQUEST_METHOD, attributes.get("http.method", None), ) server_address = attributes.get( SERVER_ADDRESS, attributes.get("http.host", None), ) # Resource shouldn't be none as it should default to empty resource service_name = resource.attributes.get(SERVICE_NAME, "") # target may be in url if url_path is None and isinstance(url_full, str): scheme_end_index = url_full.find("://") # For network calls, URL usually has `scheme://host[:port][path][?query][#fragment]` format # Per spec, url.full is always populated with scheme:// # If scheme is not present, assume it's bad instrumentation and ignore. if scheme_end_index > -1: # urlparse("scheme://netloc/path;parameters?query#fragment") url_path = urlparse(url_full).path if url_path == "": url_path = "/" elif url_path is None and url_full is None: # When missing, the URL Path is assumed to be / url_path = "/" return ( _Matcher.attribute_match(attributes, self.sampling_rule.Attributes) and _Matcher.wild_card_match(url_path, self.sampling_rule.URLPath) and _Matcher.wild_card_match(http_request_method, self.sampling_rule.HTTPMethod) and _Matcher.wild_card_match(server_address, self.sampling_rule.Host) and _Matcher.wild_card_match(service_name, self.sampling_rule.ServiceName) and _Matcher.wild_card_match( self.__get_service_type(resource), self.sampling_rule.ServiceType, ) and _Matcher.wild_card_match( self.__get_arn(resource, attributes), self.sampling_rule.ResourceARN, ) ) def __create_reservoir_sampler(self, quota: int) -> Sampler: return _RateLimitingSampler(quota, self._clock) # pylint: disable=no-self-use def __get_service_type(self, resource: Resource) -> str: cloud_platform = resource.attributes.get(CLOUD_PLATFORM, None) if not isinstance(cloud_platform, str): return "" return cloud_platform_mapping.get(cloud_platform, "") def __get_arn(self, resource: Resource, attributes: Attributes) -> AttributeValue: arn = resource.attributes.get(AWS_ECS_CONTAINER_ARN, None) if arn is not None: return arn if resource.attributes.get(CLOUD_PLATFORM) == CloudPlatformValues.AWS_LAMBDA.value: return self.__get_lambda_arn(resource, attributes) return "" def __get_lambda_arn(self, resource: Resource, attributes: Attributes) -> AttributeValue: arn = resource.attributes.get( CLOUD_RESOURCE_ID, resource.attributes.get("faas.id", None), ) if arn is not None: return arn if attributes is None: return "" # Note from `CLOUD_RESOURCE_ID`: # "On some cloud providers, it may not be possible to determine the full ID at startup, # so it may be necessary to set cloud.resource_id as a span attribute instead." arn = attributes.get(CLOUD_RESOURCE_ID, attributes.get("faas.id", None)) if arn is not None: return arn return "" _sampling_statistics_document.py000066400000000000000000000023371527070072300510510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 # pylint: disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace.sampler._clock import _Clock # Disable snake_case naming style so this class can match the statistics document response from X-Ray # pylint: disable=invalid-name class _SamplingStatisticsDocument: # pyright: ignore[reportUnusedClass] def __init__( self, clientID: str, ruleName: str, RequestCount: int = 0, BorrowCount: int = 0, SampleCount: int = 0, ): self.ClientID = clientID self.RuleName = ruleName self.Timestamp = None self.RequestCount = RequestCount self.BorrowCount = BorrowCount self.SampleCount = SampleCount def snapshot(self, clock: _Clock): return { "ClientID": self.ClientID, "RuleName": self.RuleName, "Timestamp": clock.now().timestamp(), "RequestCount": self.RequestCount, "BorrowCount": self.BorrowCount, "SampleCount": self.SampleCount, } _sampling_target.py000066400000000000000000000052451527070072300462500ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from logging import getLogger from typing import Any, cast _logger = getLogger(__name__) # Disable snake_case naming style so this class can match the sampling rules response from X-Ray # pylint: disable=invalid-name class _SamplingTarget: def __init__( self, FixedRate: float | None = None, Interval: int | None = None, ReservoirQuota: int | None = None, ReservoirQuotaTTL: float | None = None, RuleName: str | None = None, ): self.FixedRate = FixedRate if FixedRate is not None else 0.0 self.Interval = Interval # can be None self.ReservoirQuota = ReservoirQuota # can be None self.ReservoirQuotaTTL = ReservoirQuotaTTL # can be None self.RuleName = RuleName if RuleName is not None else "" class _UnprocessedStatistics: def __init__( self, ErrorCode: str | None = None, Message: str | None = None, RuleName: str | None = None, ): self.ErrorCode = ErrorCode if ErrorCode is not None else "" self.Message = Message if ErrorCode is not None else "" self.RuleName = RuleName if ErrorCode is not None else "" class _SamplingTargetResponse: # pyright: ignore[reportUnusedClass] def __init__( self, LastRuleModification: float | None, SamplingTargetDocuments: list[_SamplingTarget] | None = None, UnprocessedStatistics: list[_UnprocessedStatistics] | None = None, ): self.LastRuleModification: float = LastRuleModification if LastRuleModification is not None else 0.0 self.SamplingTargetDocuments: list[_SamplingTarget] = [] if SamplingTargetDocuments is not None: for document in SamplingTargetDocuments: try: self.SamplingTargetDocuments.append(_SamplingTarget(**cast(Any, document))) except Exception as e: # pylint: disable=broad-exception-caught _logger.debug("Error creating _SamplingTarget: %s", e) self.UnprocessedStatistics: list[_UnprocessedStatistics] = [] if UnprocessedStatistics is not None: for unprocessed in UnprocessedStatistics: try: self.UnprocessedStatistics.append(_UnprocessedStatistics(**cast(Any, unprocessed))) except Exception as e: # pylint: disable=broad-exception-caught _logger.debug("Error creating _UnprocessedStatistics: %s", e) aws_xray_remote_sampler.py000066400000000000000000000207561527070072300476700ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import random from collections.abc import Sequence from logging import getLogger from threading import Lock, Timer from typing_extensions import override # pylint: disable=no-name-in-module from opentelemetry.context import Context from opentelemetry.sdk.extension.aws.trace.sampler._aws_xray_sampling_client import ( DEFAULT_SAMPLING_PROXY_ENDPOINT, _AwsXRaySamplingClient, ) from opentelemetry.sdk.extension.aws.trace.sampler._clock import _Clock from opentelemetry.sdk.extension.aws.trace.sampler._fallback_sampler import ( _FallbackSampler, ) from opentelemetry.sdk.extension.aws.trace.sampler._rule_cache import ( DEFAULT_TARGET_POLLING_INTERVAL_SECONDS, _RuleCache, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace.sampling import ( ParentBased, Sampler, SamplingResult, ) from opentelemetry.trace import Link, SpanKind from opentelemetry.trace.span import TraceState from opentelemetry.util.types import Attributes _logger = getLogger(__name__) DEFAULT_RULES_POLLING_INTERVAL_SECONDS = 300 # Wrapper class to ensure that all XRay Sampler Functionality in _InternalAwsXRayRemoteSampler # uses ParentBased logic to respect the parent span's sampling decision class AwsXRayRemoteSampler(Sampler): def __init__( self, resource: Resource, endpoint: str | None = None, polling_interval: int | None = None, log_level: str | None = None, ): self._root = ParentBased( _InternalAwsXRayRemoteSampler( resource=resource, endpoint=endpoint, polling_interval=polling_interval, log_level=log_level, ) ) @override def should_sample( self, parent_context: Context | None, trace_id: int, name: str, kind: SpanKind | None = None, attributes: Attributes | None = None, links: Sequence[Link] | None = None, trace_state: TraceState | None = None, ) -> SamplingResult: return self._root.should_sample( parent_context, trace_id, name, kind=kind, attributes=attributes, links=links, trace_state=trace_state, ) @override def get_description(self) -> str: return f"AwsXRayRemoteSampler{{root:{self._root.get_description()}}}" # _InternalAwsXRayRemoteSampler contains all core XRay Sampler Functionality, # however it is NOT Parent-based (e.g. Sample logic runs for each span) # Not intended for external use, use Parent-based `AwsXRayRemoteSampler` instead. class _InternalAwsXRayRemoteSampler(Sampler): """ Remote Sampler for OpenTelemetry that gets sampling configurations from AWS X-Ray Args: resource: OpenTelemetry Resource (Required) endpoint: proxy endpoint for AWS X-Ray Sampling (Optional) polling_interval: Polling interval for getSamplingRules call (Optional) log_level: custom log level configuration for remote sampler (Optional) """ def __init__( self, resource: Resource, endpoint: str | None = None, polling_interval: int | None = None, log_level: str | None = None, ): # Override default log level if log_level is not None: _logger.setLevel(log_level) if endpoint is None: _logger.info( "`endpoint` is `None`. Defaulting to %s", DEFAULT_SAMPLING_PROXY_ENDPOINT, ) endpoint = DEFAULT_SAMPLING_PROXY_ENDPOINT if polling_interval is None or polling_interval < 10: _logger.info( "`polling_interval` is `None` or too small. Defaulting to %s", DEFAULT_RULES_POLLING_INTERVAL_SECONDS, ) polling_interval = DEFAULT_RULES_POLLING_INTERVAL_SECONDS self.__client_id = self.__generate_client_id() self._clock = _Clock() self.__xray_client = _AwsXRaySamplingClient(endpoint, log_level=log_level) self.__fallback_sampler = _FallbackSampler(self._clock) self.__polling_interval = polling_interval self.__target_polling_interval = DEFAULT_TARGET_POLLING_INTERVAL_SECONDS self.__rule_polling_jitter = random.uniform(0.0, 5.0) self.__target_polling_jitter = random.uniform(0.0, 0.1) self.__resource = resource self.__rule_cache_lock = Lock() self.__rule_cache = _RuleCache( self.__resource, self.__fallback_sampler, self.__client_id, self._clock, self.__rule_cache_lock, ) # Schedule the next rule poll now # Python Timers only run once, so they need to be recreated for every poll self._rules_timer = Timer(0, self.__start_sampling_rule_poller) self._rules_timer.daemon = True # Ensures that when the main thread exits, the Timer threads are killed self._rules_timer.start() # set up the target poller to go off once after the default interval. Subsequent polls may use new intervals. self._targets_timer = Timer( self.__target_polling_interval + self.__target_polling_jitter, self.__start_sampling_target_poller, ) self._targets_timer.daemon = True # Ensures that when the main thread exits, the Timer threads are killed self._targets_timer.start() @override def should_sample( self, parent_context: Context | None, trace_id: int, name: str, kind: SpanKind | None = None, attributes: Attributes | None = None, links: Sequence[Link] | None = None, trace_state: TraceState | None = None, ) -> SamplingResult: if self.__rule_cache.expired(): _logger.debug("Rule cache is expired so using fallback sampling strategy") return self.__fallback_sampler.should_sample( parent_context, trace_id, name, kind=kind, attributes=attributes, links=links, trace_state=trace_state, ) return self.__rule_cache.should_sample( parent_context, trace_id, name, kind=kind, attributes=attributes, links=links, trace_state=trace_state, ) # pylint: disable=no-self-use @override def get_description(self) -> str: description = "_InternalAwsXRayRemoteSampler{remote sampling with AWS X-Ray}" return description def __get_and_update_sampling_rules(self) -> None: sampling_rules = self.__xray_client.get_sampling_rules() self.__rule_cache.update_sampling_rules(sampling_rules) def __start_sampling_rule_poller(self) -> None: self.__get_and_update_sampling_rules() # Schedule the next sampling rule poll self._rules_timer = Timer( self.__polling_interval + self.__rule_polling_jitter, self.__start_sampling_rule_poller, ) self._rules_timer.daemon = True self._rules_timer.start() def __get_and_update_sampling_targets(self) -> None: all_statistics = self.__rule_cache.get_all_statistics() sampling_targets_response = self.__xray_client.get_sampling_targets(all_statistics) refresh_rules, min_polling_interval = self.__rule_cache.update_sampling_targets(sampling_targets_response) if refresh_rules: self.__get_and_update_sampling_rules() if min_polling_interval is not None: # type: ignore self.__target_polling_interval = min_polling_interval def __start_sampling_target_poller(self) -> None: self.__get_and_update_sampling_targets() # Schedule the next sampling targets poll self._targets_timer = Timer( self.__target_polling_interval + self.__target_polling_jitter, self.__start_sampling_target_poller, ) self._targets_timer.daemon = True self._targets_timer.start() def __generate_client_id(self) -> str: hex_chars = "0123456789abcdef" client_id_array: list[str] = [] for _ in range(24): client_id_array.append(random.choice(hex_chars)) return "".join(client_id_array) version.py000066400000000000000000000001471527070072300416510ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "2.2.0.dev" test-requirements-0.txt000066400000000000000000000006361527070072300351410ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-awsasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 pytest-benchmark==4.0.0 requests==2.33.1 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 opentelemetry-api==1.26 # when updating, also update in pyproject.toml opentelemetry-sdk==1.26 # when updating, also update in pyproject.toml -e sdk-extension/opentelemetry-sdk-extension-aws test-requirements-1.txt000066400000000000000000000005411527070072300351350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-awsasgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 pytest-benchmark==4.0.0 requests==2.33.1 tomli==2.0.1 typing_extensions==4.12.2 wrapt==1.16.0 zipp==3.19.2 # test with the latest version of opentelemetry-api, sdk, and semantic conventions -e sdk-extension/opentelemetry-sdk-extension-aws opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/000077500000000000000000000000001527070072300317575ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/__init__.py000066400000000000000000000000001527070072300340560ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/000077500000000000000000000000001527070072300336065ustar00rootroot00000000000000ecs/000077500000000000000000000000001527070072300343015ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/resourcemetadatav4-response-container-ec2.json000066400000000000000000000032711527070072300435140ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/ecs{ "DockerId": "ea32192c8553fbff06c9340478a2ff089b2bb5646fb718b4ee206641c9086d66", "Name": "curl", "DockerName": "ecs-curltest-24-curl-cca48e8dcadd97805600", "Image": "111122223333.dkr.ecr.us-west-2.amazonaws.com/curltest:latest", "ImageID": "sha256:d691691e9652791a60114e67b365688d20d19940dde7c4736ea30e660d8d3553", "Labels": { "com.amazonaws.ecs.cluster": "default", "com.amazonaws.ecs.container-name": "curl", "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-west-2:111122223333:task/default/8f03e41243824aea923aca126495f665", "com.amazonaws.ecs.task-definition-family": "curltest", "com.amazonaws.ecs.task-definition-version": "24" }, "DesiredStatus": "RUNNING", "KnownStatus": "RUNNING", "Limits": { "CPU": 10, "Memory": 128 }, "CreatedAt": "2020-10-02T00:15:07.620912337Z", "StartedAt": "2020-10-02T00:15:08.062559351Z", "Type": "NORMAL", "LogDriver": "awslogs", "LogOptions": { "awslogs-create-group": "true", "awslogs-group": "/ecs/metadata", "awslogs-region": "us-west-2", "awslogs-stream": "ecs/curl/8f03e41243824aea923aca126495f665" }, "ContainerARN": "arn:aws:ecs:us-west-2:111122223333:container/0206b271-b33f-47ab-86c6-a0ba208a70a9", "Networks": [ { "NetworkMode": "awsvpc", "IPv4Addresses": [ "10.0.2.100" ], "AttachmentIndex": 0, "MACAddress": "0e:9e:32:c7:48:85", "IPv4SubnetCIDRBlock": "10.0.2.0/24", "PrivateDNSName": "ip-10-0-2-100.us-west-2.compute.internal", "SubnetGatewayIpv4Address": "10.0.2.1/24" } ] } metadatav4-response-container-fargate.json000066400000000000000000000035341527070072300444560ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/ecs{ "DockerId": "cd189a933e5849daa93386466019ab50-2495160603", "Name": "curl", "DockerName": "curl", "Image": "111122223333.dkr.ecr.us-west-2.amazonaws.com/curltest:latest", "ImageID": "sha256:25f3695bedfb454a50f12d127839a68ad3caf91e451c1da073db34c542c4d2cb", "Labels": { "com.amazonaws.ecs.cluster": "arn:aws:ecs:us-west-2:111122223333:cluster/default", "com.amazonaws.ecs.container-name": "curl", "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-west-2:111122223333:task/default/cd189a933e5849daa93386466019ab50", "com.amazonaws.ecs.task-definition-family": "curltest", "com.amazonaws.ecs.task-definition-version": "2" }, "DesiredStatus": "RUNNING", "KnownStatus": "RUNNING", "Limits": { "CPU": 10, "Memory": 128 }, "CreatedAt": "2020-10-08T20:09:11.44527186Z", "StartedAt": "2020-10-08T20:09:11.44527186Z", "Type": "NORMAL", "Networks": [ { "NetworkMode": "awsvpc", "IPv4Addresses": [ "192.0.2.3" ], "AttachmentIndex": 0, "MACAddress": "0a:de:f6:10:51:e5", "IPv4SubnetCIDRBlock": "192.0.2.0/24", "DomainNameServers": [ "192.0.2.2" ], "DomainNameSearchList": [ "us-west-2.compute.internal" ], "PrivateDNSName": "ip-10-0-0-222.us-west-2.compute.internal", "SubnetGatewayIpv4Address": "192.0.2.0/24" } ], "ContainerARN": "arn:aws:ecs:us-west-2:111122223333:container/05966557-f16c-49cb-9352-24b3a0dcd0e1", "LogOptions": { "awslogs-create-group": "true", "awslogs-group": "/ecs/containerlogs", "awslogs-region": "us-west-2", "awslogs-stream": "ecs/curl/cd189a933e5849daa93386466019ab50" }, "LogDriver": "awslogs" }metadatav4-response-task-ec2.json000066400000000000000000000100451527070072300424710ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/ecs{ "Cluster": "default", "TaskARN": "arn:aws:ecs:us-west-2:111122223333:task/default/158d1c8083dd49d6b527399fd6414f5c", "Family": "curltest", "Revision": "26", "DesiredStatus": "RUNNING", "KnownStatus": "RUNNING", "PullStartedAt": "2020-10-02T00:43:06.202617438Z", "PullStoppedAt": "2020-10-02T00:43:06.31288465Z", "AvailabilityZone": "us-west-2d", "LaunchType": "EC2", "Containers": [ { "DockerId": "598cba581fe3f939459eaba1e071d5c93bb2c49b7d1ba7db6bb19deeb70d8e38", "Name": "~internal~ecs~pause", "DockerName": "ecs-curltest-26-internalecspause-e292d586b6f9dade4a00", "Image": "amazon/amazon-ecs-pause:0.1.0", "ImageID": "", "Labels": { "com.amazonaws.ecs.cluster": "default", "com.amazonaws.ecs.container-name": "~internal~ecs~pause", "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-west-2:111122223333:task/default/158d1c8083dd49d6b527399fd6414f5c", "com.amazonaws.ecs.task-definition-family": "curltest", "com.amazonaws.ecs.task-definition-version": "26" }, "DesiredStatus": "RESOURCES_PROVISIONED", "KnownStatus": "RESOURCES_PROVISIONED", "Limits": { "CPU": 0, "Memory": 0 }, "CreatedAt": "2020-10-02T00:43:05.602352471Z", "StartedAt": "2020-10-02T00:43:06.076707576Z", "Type": "CNI_PAUSE", "Networks": [ { "NetworkMode": "awsvpc", "IPv4Addresses": [ "10.0.2.61" ], "AttachmentIndex": 0, "MACAddress": "0e:10:e2:01:bd:91", "IPv4SubnetCIDRBlock": "10.0.2.0/24", "PrivateDNSName": "ip-10-0-2-61.us-west-2.compute.internal", "SubnetGatewayIpv4Address": "10.0.2.1/24" } ] }, { "DockerId": "ee08638adaaf009d78c248913f629e38299471d45fe7dc944d1039077e3424ca", "Name": "curl", "DockerName": "ecs-curltest-26-curl-a0e7dba5aca6d8cb2e00", "Image": "111122223333.dkr.ecr.us-west-2.amazonaws.com/curltest:latest", "ImageID": "sha256:d691691e9652791a60114e67b365688d20d19940dde7c4736ea30e660d8d3553", "Labels": { "com.amazonaws.ecs.cluster": "default", "com.amazonaws.ecs.container-name": "curl", "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-west-2:111122223333:task/default/158d1c8083dd49d6b527399fd6414f5c", "com.amazonaws.ecs.task-definition-family": "curltest", "com.amazonaws.ecs.task-definition-version": "26" }, "DesiredStatus": "RUNNING", "KnownStatus": "RUNNING", "Limits": { "CPU": 10, "Memory": 128 }, "CreatedAt": "2020-10-02T00:43:06.326590752Z", "StartedAt": "2020-10-02T00:43:06.767535449Z", "Type": "NORMAL", "LogDriver": "awslogs", "LogOptions": { "awslogs-create-group": "true", "awslogs-group": "/ecs/metadata", "awslogs-region": "us-west-2", "awslogs-stream": "ecs/curl/158d1c8083dd49d6b527399fd6414f5c" }, "ContainerARN": "arn:aws:ecs:us-west-2:111122223333:container/abb51bdd-11b4-467f-8f6c-adcfe1fe059d", "Networks": [ { "NetworkMode": "awsvpc", "IPv4Addresses": [ "10.0.2.61" ], "AttachmentIndex": 0, "MACAddress": "0e:10:e2:01:bd:91", "IPv4SubnetCIDRBlock": "10.0.2.0/24", "PrivateDNSName": "ip-10-0-2-61.us-west-2.compute.internal", "SubnetGatewayIpv4Address": "10.0.2.1/24" } ] } ] } metadatav4-response-task-fargate.json000066400000000000000000000062711527070072300434370ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/ecs{ "Cluster": "arn:aws:ecs:us-west-2:111122223333:cluster/default", "TaskARN": "arn:aws:ecs:us-west-2:111122223333:task/default/e9028f8d5d8e4f258373e7b93ce9a3c3", "Family": "curltest", "Revision": "3", "DesiredStatus": "RUNNING", "KnownStatus": "RUNNING", "Limits": { "CPU": 0.25, "Memory": 512 }, "PullStartedAt": "2020-10-08T20:47:16.053330955Z", "PullStoppedAt": "2020-10-08T20:47:19.592684631Z", "AvailabilityZone": "us-west-2a", "Containers": [ { "DockerId": "e9028f8d5d8e4f258373e7b93ce9a3c3-2495160603", "Name": "curl", "DockerName": "curl", "Image": "111122223333.dkr.ecr.us-west-2.amazonaws.com/curltest:latest", "ImageID": "sha256:25f3695bedfb454a50f12d127839a68ad3caf91e451c1da073db34c542c4d2cb", "Labels": { "com.amazonaws.ecs.cluster": "arn:aws:ecs:us-west-2:111122223333:cluster/default", "com.amazonaws.ecs.container-name": "curl", "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-west-2:111122223333:task/default/e9028f8d5d8e4f258373e7b93ce9a3c3", "com.amazonaws.ecs.task-definition-family": "curltest", "com.amazonaws.ecs.task-definition-version": "3" }, "DesiredStatus": "RUNNING", "KnownStatus": "RUNNING", "Limits": { "CPU": 10, "Memory": 128 }, "CreatedAt": "2020-10-08T20:47:20.567813946Z", "StartedAt": "2020-10-08T20:47:20.567813946Z", "Type": "NORMAL", "Networks": [ { "NetworkMode": "awsvpc", "IPv4Addresses": [ "192.0.2.3" ], "IPv6Addresses": [ "2001:dB8:10b:1a00:32bf:a372:d80f:e958" ], "AttachmentIndex": 0, "MACAddress": "02:b7:20:19:72:39", "IPv4SubnetCIDRBlock": "192.0.2.0/24", "IPv6SubnetCIDRBlock": "2600:1f13:10b:1a00::/64", "DomainNameServers": [ "192.0.2.2" ], "DomainNameSearchList": [ "us-west-2.compute.internal" ], "PrivateDNSName": "ip-172-31-30-173.us-west-2.compute.internal", "SubnetGatewayIpv4Address": "192.0.2.0/24" } ], "ClockDrift": { "ClockErrorBound": 0.5458234999999999, "ReferenceTimestamp": "2021-09-07T16:57:44Z", "ClockSynchronizationStatus": "SYNCHRONIZED" }, "ContainerARN": "arn:aws:ecs:us-west-2:111122223333:container/1bdcca8b-f905-4ee6-885c-4064cb70f6e6", "LogOptions": { "awslogs-create-group": "true", "awslogs-group": "/ecs/containerlogs", "awslogs-region": "us-west-2", "awslogs-stream": "ecs/curl/e9028f8d5d8e4f258373e7b93ce9a3c3" }, "LogDriver": "awslogs" } ], "LaunchType": "FARGATE" }test__lambda.py000066400000000000000000000032601527070072300365200ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from collections import OrderedDict from unittest.mock import patch from opentelemetry.sdk.extension.aws.resource._lambda import ( # pylint: disable=no-name-in-module AwsLambdaResourceDetector, ) from opentelemetry.semconv.resource import ( CloudPlatformValues, CloudProviderValues, ResourceAttributes, ) MockLambdaResourceAttributes = { ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AWS.value, ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AWS_LAMBDA.value, ResourceAttributes.CLOUD_REGION: "mock-west-2", ResourceAttributes.FAAS_NAME: "mock-lambda-name", ResourceAttributes.FAAS_VERSION: "mock-version-42", ResourceAttributes.FAAS_INSTANCE: "mock-log-stream", ResourceAttributes.FAAS_MAX_MEMORY: 128, } class AwsLambdaResourceDetectorTest(unittest.TestCase): @patch.dict( "os.environ", { "AWS_REGION": MockLambdaResourceAttributes[ResourceAttributes.CLOUD_REGION], "AWS_LAMBDA_FUNCTION_NAME": MockLambdaResourceAttributes[ResourceAttributes.FAAS_NAME], "AWS_LAMBDA_FUNCTION_VERSION": MockLambdaResourceAttributes[ResourceAttributes.FAAS_VERSION], "AWS_LAMBDA_LOG_STREAM_NAME": MockLambdaResourceAttributes[ResourceAttributes.FAAS_INSTANCE], "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": f"{MockLambdaResourceAttributes[ResourceAttributes.FAAS_MAX_MEMORY]}", }, clear=True, ) def test_simple_create(self): actual = AwsLambdaResourceDetector().detect() self.assertDictEqual(actual.attributes.copy(), OrderedDict(MockLambdaResourceAttributes)) test_beanstalk.py000066400000000000000000000036711527070072300371130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from collections import OrderedDict from unittest.mock import mock_open, patch from opentelemetry.sdk.extension.aws.resource.beanstalk import ( # pylint: disable=no-name-in-module AwsBeanstalkResourceDetector, ) from opentelemetry.semconv.resource import ( CloudPlatformValues, CloudProviderValues, ResourceAttributes, ) MockBeanstalkResourceAttributes = { ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AWS.value, ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AWS_ELASTIC_BEANSTALK.value, ResourceAttributes.SERVICE_NAME: CloudPlatformValues.AWS_ELASTIC_BEANSTALK.value, ResourceAttributes.SERVICE_INSTANCE_ID: "42", ResourceAttributes.SERVICE_NAMESPACE: "mock-env-name", ResourceAttributes.SERVICE_VERSION: "app-zip-label", } class AwsBeanstalkResourceDetectorTest(unittest.TestCase): @patch( "builtins.open", new_callable=mock_open, read_data=f'{{"deployment_id":"{MockBeanstalkResourceAttributes[ResourceAttributes.SERVICE_INSTANCE_ID]}","environment_name":"{MockBeanstalkResourceAttributes[ResourceAttributes.SERVICE_NAMESPACE]}","version_label":"{MockBeanstalkResourceAttributes[ResourceAttributes.SERVICE_VERSION]}"}}', ) @patch("os.path.exists", return_value=True) def test_simple_create(self, mock_path_exists, mock_open_function): actual = AwsBeanstalkResourceDetector().detect() self.assertDictEqual( actual.attributes.copy(), OrderedDict(MockBeanstalkResourceAttributes), ) @patch("os.name", "posix") @patch("os.path.exists", return_value=False) def test_not_on_beanstalk(self, mock_path_exists): actual = AwsBeanstalkResourceDetector().detect() self.assertDictEqual(actual.attributes.copy(), {}) mock_path_exists.assert_called_once_with("/var/elasticbeanstalk/xray/environment.conf") test_ec2.py000066400000000000000000000060461527070072300356170ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from collections import OrderedDict from unittest.mock import patch from urllib.error import URLError from opentelemetry.sdk.extension.aws.resource.ec2 import ( # pylint: disable=no-name-in-module AwsEc2ResourceDetector, ) from opentelemetry.semconv.resource import ( CloudPlatformValues, CloudProviderValues, ResourceAttributes, ) MockEc2ResourceAttributes = { ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AWS.value, ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AWS_EC2.value, ResourceAttributes.CLOUD_ACCOUNT_ID: "123456789012", ResourceAttributes.CLOUD_REGION: "mock-west-2", ResourceAttributes.CLOUD_AVAILABILITY_ZONE: "mock-west-2a", ResourceAttributes.HOST_ID: "i-1234ab56cd7e89f01", ResourceAttributes.HOST_TYPE: "t2.micro-mock", ResourceAttributes.HOST_NAME: "ip-172-12-34-567.mock-west-2.compute.internal", } class AwsEc2ResourceDetectorTest(unittest.TestCase): @patch( "opentelemetry.sdk.extension.aws.resource.ec2._get_host", return_value=MockEc2ResourceAttributes[ResourceAttributes.HOST_NAME], ) @patch( "opentelemetry.sdk.extension.aws.resource.ec2._get_identity", return_value=f"""{{ "accountId" : "{MockEc2ResourceAttributes[ResourceAttributes.CLOUD_ACCOUNT_ID]}", "architecture" : "x86_64", "availabilityZone" : "{MockEc2ResourceAttributes[ResourceAttributes.CLOUD_AVAILABILITY_ZONE]}", "billingProducts" : null, "devpayProductCodes" : null, "marketplaceProductCodes" : null, "imageId" : "ami-0957cee1854021123", "instanceId" : "{MockEc2ResourceAttributes[ResourceAttributes.HOST_ID]}", "instanceType" : "{MockEc2ResourceAttributes[ResourceAttributes.HOST_TYPE]}", "kernelId" : null, "pendingTime" : "2021-07-13T21:53:41Z", "privateIp" : "172.12.34.567", "ramdiskId" : null, "region" : "{MockEc2ResourceAttributes[ResourceAttributes.CLOUD_REGION]}", "version" : "2017-09-30" }}""", ) @patch( "opentelemetry.sdk.extension.aws.resource.ec2._get_token", return_value="mock-token", ) def test_simple_create(self, mock_get_token, mock_get_identity, mock_get_host): actual = AwsEc2ResourceDetector().detect() self.assertDictEqual(actual.attributes.copy(), OrderedDict(MockEc2ResourceAttributes)) @patch( "opentelemetry.sdk.extension.aws.resource.ec2._get_token", side_effect=URLError("Something went wrong"), ) def test_empty_resource_if_token_returns_an_url_error(self, mock_get_token): with self.assertLogs("opentelemetry.sdk.extension.aws.resource.ec2", level="DEBUG") as logger: actual = AwsEc2ResourceDetector().detect() self.assertEqual( logger.output, [ "DEBUG:opentelemetry.sdk.extension.aws.resource.ec2:AwsEc2ResourceDetector failed to get token: " ], ) self.assertDictEqual(actual.attributes.copy(), OrderedDict()) test_ecs.py000066400000000000000000000234561527070072300357240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from collections import OrderedDict from os.path import dirname, join from unittest.mock import mock_open, patch from opentelemetry.sdk.extension.aws.resource.ecs import ( # pylint: disable=no-name-in-module AwsEcsResourceDetector, ) from opentelemetry.semconv.resource import ( CloudPlatformValues, CloudProviderValues, ResourceAttributes, ) MockEcsResourceAttributes = { ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AWS.value, ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AWS_ECS.value, ResourceAttributes.CONTAINER_NAME: "mock-container-name", ResourceAttributes.CONTAINER_ID: "a4d00c9dd675d67f866c786181419e1b44832d4696780152e61afd44a3e02856", } def _read_file(filename: str) -> str: with open(join(dirname(__file__), "ecs", filename), encoding="utf-8") as file: return file.read() MetadataV4Uri = "mock-uri-4" MetadataV4ContainerResponseEc2 = _read_file("metadatav4-response-container-ec2.json") MetadataV4TaskResponseEc2 = _read_file("metadatav4-response-task-ec2.json") MetadataV4ContainerResponseFargate = _read_file("metadatav4-response-container-fargate.json") MetadataV4TaskResponseFargate = _read_file("metadatav4-response-task-fargate.json") def _http_get_function_ec2(url: str, *args, **kwargs) -> str: if url == MetadataV4Uri: return MetadataV4ContainerResponseEc2 if url == f"{MetadataV4Uri}/task": return MetadataV4TaskResponseEc2 return None def _http_get_function_fargate(url: str, *args, **kwargs) -> str: if url == MetadataV4Uri: return MetadataV4ContainerResponseFargate if url == f"{MetadataV4Uri}/task": return MetadataV4TaskResponseFargate return None class AwsEcsResourceDetectorTest(unittest.TestCase): @patch.dict("os.environ", {}, clear=True) def test_not_on_ecs(self): actual = AwsEcsResourceDetector().detect() self.assertDictEqual(actual.attributes.copy(), {}) @patch.dict( "os.environ", {"ECS_CONTAINER_METADATA_URI": "mock-uri"}, clear=True, ) @patch( "socket.gethostname", return_value=f"{MockEcsResourceAttributes[ResourceAttributes.CONTAINER_NAME]}", ) @patch( "builtins.open", new_callable=mock_open, read_data=f"""14:name=systemd:/docker/{MockEcsResourceAttributes[ResourceAttributes.CONTAINER_ID]} 13:rdma:/ 12:pids:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 11:hugetlb:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 10:net_prio:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 9:perf_event:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 8:net_cls:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 7:freezer:/docker/ 6:devices:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 5:memory:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 4:blkio:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 3:cpuacct:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 2:cpu:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 1:cpuset:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked """, ) def test_simple_create_metadata_v3( self, mock_open_function, mock_socket_gethostname, ): actual = AwsEcsResourceDetector().detect() self.assertDictEqual(actual.attributes.copy(), OrderedDict(MockEcsResourceAttributes)) @patch.dict( "os.environ", {"ECS_CONTAINER_METADATA_URI_V4": MetadataV4Uri}, clear=True, ) @patch( "socket.gethostname", return_value=f"{MockEcsResourceAttributes[ResourceAttributes.CONTAINER_NAME]}", ) @patch( "builtins.open", new_callable=mock_open, read_data=f"""14:name=systemd:/docker/{MockEcsResourceAttributes[ResourceAttributes.CONTAINER_ID]} 13:rdma:/ 12:pids:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 11:hugetlb:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 10:net_prio:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 9:perf_event:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 8:net_cls:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 7:freezer:/docker/ 6:devices:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 5:memory:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 4:blkio:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 3:cpuacct:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 2:cpu:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 1:cpuset:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked """, ) @patch( "opentelemetry.sdk.extension.aws.resource.ecs._http_get", ) def test_simple_create_metadata_v4_launchtype_ec2( self, mock_http_get_function, mock_open_function, mock_socket_gethostname, ): mock_http_get_function.side_effect = _http_get_function_ec2 actual = AwsEcsResourceDetector().detect() self.assertDictEqual( actual.attributes.copy(), OrderedDict( { **MockEcsResourceAttributes, ResourceAttributes.AWS_LOG_GROUP_NAMES: ("/ecs/metadata",), ResourceAttributes.AWS_LOG_GROUP_ARNS: ( "arn:aws:logs:us-west-2:111122223333:log-group:/ecs/metadata", ), ResourceAttributes.AWS_LOG_STREAM_NAMES: ("ecs/curl/8f03e41243824aea923aca126495f665",), ResourceAttributes.AWS_LOG_STREAM_ARNS: ( "arn:aws:logs:us-west-2:111122223333:log-group:/ecs/metadata:log-stream:ecs/curl/8f03e41243824aea923aca126495f665", ), ResourceAttributes.AWS_ECS_CONTAINER_ARN: "arn:aws:ecs:us-west-2:111122223333:container/0206b271-b33f-47ab-86c6-a0ba208a70a9", ResourceAttributes.AWS_ECS_CLUSTER_ARN: "arn:aws:ecs:us-west-2:111122223333:cluster/default", ResourceAttributes.AWS_ECS_LAUNCHTYPE: "ec2", ResourceAttributes.AWS_ECS_TASK_ARN: "arn:aws:ecs:us-west-2:111122223333:task/default/158d1c8083dd49d6b527399fd6414f5c", ResourceAttributes.AWS_ECS_TASK_FAMILY: "curltest", ResourceAttributes.AWS_ECS_TASK_REVISION: "26", } ), ) @patch.dict( "os.environ", {"ECS_CONTAINER_METADATA_URI_V4": MetadataV4Uri}, clear=True, ) @patch( "socket.gethostname", return_value=f"{MockEcsResourceAttributes[ResourceAttributes.CONTAINER_NAME]}", ) @patch( "builtins.open", new_callable=mock_open, read_data=f"""14:name=systemd:/docker/{MockEcsResourceAttributes[ResourceAttributes.CONTAINER_ID]} 13:rdma:/ 12:pids:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 11:hugetlb:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 10:net_prio:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 9:perf_event:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 8:net_cls:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 7:freezer:/docker/ 6:devices:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 5:memory:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 4:blkio:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 3:cpuacct:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 2:cpu:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 1:cpuset:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked """, ) @patch( "opentelemetry.sdk.extension.aws.resource.ecs._http_get", ) def test_simple_create_metadata_v4_launchtype_fargate( self, mock_http_get_function, mock_open_function, mock_socket_gethostname, ): mock_http_get_function.side_effect = _http_get_function_fargate actual = AwsEcsResourceDetector().detect() self.assertDictEqual( actual.attributes.copy(), OrderedDict( { **MockEcsResourceAttributes, ResourceAttributes.AWS_LOG_GROUP_NAMES: ("/ecs/containerlogs",), ResourceAttributes.AWS_LOG_GROUP_ARNS: ( "arn:aws:logs:us-west-2:111122223333:log-group:/ecs/containerlogs", ), ResourceAttributes.AWS_LOG_STREAM_NAMES: ("ecs/curl/cd189a933e5849daa93386466019ab50",), ResourceAttributes.AWS_LOG_STREAM_ARNS: ( "arn:aws:logs:us-west-2:111122223333:log-group:/ecs/containerlogs:log-stream:ecs/curl/cd189a933e5849daa93386466019ab50", ), ResourceAttributes.AWS_ECS_CONTAINER_ARN: "arn:aws:ecs:us-west-2:111122223333:container/05966557-f16c-49cb-9352-24b3a0dcd0e1", ResourceAttributes.AWS_ECS_CLUSTER_ARN: "arn:aws:ecs:us-west-2:111122223333:cluster/default", ResourceAttributes.AWS_ECS_LAUNCHTYPE: "fargate", ResourceAttributes.AWS_ECS_TASK_ARN: "arn:aws:ecs:us-west-2:111122223333:task/default/e9028f8d5d8e4f258373e7b93ce9a3c3", ResourceAttributes.AWS_ECS_TASK_FAMILY: "curltest", ResourceAttributes.AWS_ECS_TASK_REVISION: "3", } ), ) test_eks.py000066400000000000000000000176071527070072300357350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import base64 import json import unittest from collections import OrderedDict from unittest.mock import mock_open, patch from opentelemetry.sdk.extension.aws.resource.eks import ( # pylint: disable=no-name-in-module AwsEksResourceDetector, ) from opentelemetry.semconv.resource import ( CloudPlatformValues, CloudProviderValues, ResourceAttributes, ) def _bearer_jwt(payload: dict) -> str: header = base64.urlsafe_b64encode(b'{"alg":"RS256"}').rstrip(b"=").decode() body = base64.urlsafe_b64encode(json.dumps(payload).encode()).rstrip(b"=").decode() return f"Bearer {header}.{body}.fakesig" MockEksResourceAttributes = { ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AWS.value, ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AWS_EKS.value, ResourceAttributes.K8S_CLUSTER_NAME: "mock-cluster-name", ResourceAttributes.CONTAINER_ID: "a4d00c9dd675d67f866c786181419e1b44832d4696780152e61afd44a3e02856", } class AwsEksResourceDetectorTest(unittest.TestCase): @patch( "opentelemetry.sdk.extension.aws.resource.eks._get_k8s_cred_value", return_value="MOCK_TOKEN", ) @patch( "opentelemetry.sdk.extension.aws.resource.eks._is_eks", return_value=True, ) @patch( "opentelemetry.sdk.extension.aws.resource.eks._is_k8s", return_value=True, ) @patch( "opentelemetry.sdk.extension.aws.resource.eks._get_cluster_info", return_value=f"""{{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": {{ "name": "cluster-info", "namespace": "amazon-cloudwatch", "selfLink": "/api/v1/namespaces/amazon-cloudwatch/configmaps/cluster-info", "uid": "0734438c-48f4-45c3-b06d-b6f16f7f0e1e", "resourceVersion": "25911", "creationTimestamp": "2021-07-23T18:41:56Z", "annotations": {{ "kubectl.kubernetes.io/last-applied-configuration": "{{\\"apiVersion\\":\\"v1\\",\\"data\\":{{\\"cluster.name\\":\\"{MockEksResourceAttributes[ResourceAttributes.K8S_CLUSTER_NAME]}\\",\\"logs.region\\":\\"us-west-2\\"}},\\"kind\\":\\"ConfigMap\\",\\"metadata\\":{{\\"annotations\\":{{}},\\"name\\":\\"cluster-info\\",\\"namespace\\":\\"amazon-cloudwatch\\"}}}}\\n" }} }}, "data": {{ "cluster.name": "{MockEksResourceAttributes[ResourceAttributes.K8S_CLUSTER_NAME]}", "logs.region": "us-west-2" }} }} """, ) @patch( "builtins.open", new_callable=mock_open, read_data=f"""14:name=systemd:/docker/{MockEksResourceAttributes[ResourceAttributes.CONTAINER_ID]} 13:rdma:/ 12:pids:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 11:hugetlb:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 10:net_prio:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 9:perf_event:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 8:net_cls:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 7:freezer:/docker/ 6:devices:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 5:memory:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 4:blkio:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 3:cpuacct:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 2:cpu:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked 1:cpuset:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked """, ) def test_simple_create( self, mock_open_function, mock_get_cluster_info, mock_is_k8s, mock_is_eks, mock_get_k8_cred_value, ): actual = AwsEksResourceDetector().detect() self.assertDictEqual(actual.attributes.copy(), OrderedDict(MockEksResourceAttributes)) @patch( "opentelemetry.sdk.extension.aws.resource.eks._get_k8s_cred_value", return_value="MOCK_TOKEN", ) @patch( "opentelemetry.sdk.extension.aws.resource.eks._is_eks", return_value=False, ) @patch( "opentelemetry.sdk.extension.aws.resource.eks._is_k8s", return_value=True, ) def test_if_no_eks_env_var_and_should_raise(self, mock_is_k8s, mock_is_eks, mock_get_k8_cred_value): with self.assertRaises(RuntimeError): AwsEksResourceDetector(raise_on_error=True).detect() @patch( "opentelemetry.sdk.extension.aws.resource.eks._get_k8s_cred_value", return_value="MOCK_TOKEN", ) @patch( "opentelemetry.sdk.extension.aws.resource.eks._is_eks", return_value=False, ) @patch( "opentelemetry.sdk.extension.aws.resource.eks._is_k8s", return_value=False, ) def test_if_no_eks_paths_should_not_raise(self, mock_is_k8s, mock_is_eks, mock_get_k8_cred_value): try: AwsEksResourceDetector(raise_on_error=True).detect() except RuntimeError: self.fail("Should not raise") @patch( "opentelemetry.sdk.extension.aws.resource.eks._get_k8s_cred_value", return_value=_bearer_jwt( {"iss": "https://oidc.eks.eu-west-2.amazonaws.com/id/A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4"} ), ) @patch( "opentelemetry.sdk.extension.aws.resource.eks._is_k8s", return_value=True, ) @patch( "opentelemetry.sdk.extension.aws.resource.eks._get_cluster_info", return_value=f"""{{ "data": {{ "cluster.name": "{MockEksResourceAttributes[ResourceAttributes.K8S_CLUSTER_NAME]}" }} }} """, ) @patch( "builtins.open", new_callable=mock_open, read_data=f"14:name=systemd:/docker/{MockEksResourceAttributes[ResourceAttributes.CONTAINER_ID]}\n", ) def test_eks_oidc_jwt_detected( self, mock_open_function, mock_get_cluster_info, mock_is_k8s, mock_get_k8s_cred_value, ): actual = AwsEksResourceDetector().detect() self.assertEqual( actual.attributes.get(ResourceAttributes.CLOUD_PLATFORM), CloudPlatformValues.AWS_EKS.value, ) @patch( "opentelemetry.sdk.extension.aws.resource.eks._get_k8s_cred_value", return_value=_bearer_jwt({"iss": "https://accounts.google.com"}), ) @patch( "opentelemetry.sdk.extension.aws.resource.eks._is_k8s", return_value=True, ) def test_non_eks_jwt_returns_empty(self, mock_is_k8s, mock_get_k8s_cred_value): actual = AwsEksResourceDetector().detect() self.assertEqual(actual.attributes, {}) @patch( "opentelemetry.sdk.extension.aws.resource.eks._get_k8s_cred_value", return_value=_bearer_jwt({"iss": "https://wrong.jwt.com"}), ) @patch( "opentelemetry.sdk.extension.aws.resource.eks._is_k8s", return_value=True, ) def test_non_eks_jwt_should_raise(self, mock_is_k8s, mock_get_k8s_cred_value): with self.assertRaises(RuntimeError): AwsEksResourceDetector(raise_on_error=True).detect() @patch( "opentelemetry.sdk.extension.aws.resource.eks._get_k8s_cred_value", return_value="Bearer notajwt.otel", ) @patch( "opentelemetry.sdk.extension.aws.resource.eks._is_k8s", return_value=True, ) def test_is_eks_wrong_parts_count_should_raise(self, mock_is_k8s, mock_get_k8s_cred_value): with self.assertRaises(RuntimeError): AwsEksResourceDetector(raise_on_error=True).detect() @patch( "opentelemetry.sdk.extension.aws.resource.eks._get_k8s_cred_value", return_value="Bearer header.eyJpc3MiOg.fakesig", ) @patch( "opentelemetry.sdk.extension.aws.resource.eks._is_k8s", return_value=True, ) def test_is_eks_invalid_json_payload_should_raise(self, mock_is_k8s, mock_get_k8s_cred_value): with self.assertRaises(RuntimeError): AwsEksResourceDetector(raise_on_error=True).detect() opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/000077500000000000000000000000001527070072300330555ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070072300350750ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/tracesampler/000077500000000000000000000000001527070072300344415ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace__init__.py000066400000000000000000000000001527070072300365400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler_mock_clock.py000066400000000000000000000013271527070072300372610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import datetime # pylint: disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace.sampler._clock import _Clock class MockClock(_Clock): def __init__(self, dt: datetime.datetime = datetime.datetime.now()): self.time_now = dt super() def now(self) -> datetime.datetime: return self.time_now def add_time(self, seconds: float) -> None: self.time_now += self.time_delta(seconds) def set_time(self, dt: datetime.datetime) -> None: self.time_now = dt data/000077500000000000000000000000001527070072300353525ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/samplerget-sampling-rules-response-sample-2.json000066400000000000000000000027071527070072300452440ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler/data{ "NextToken": null, "SamplingRuleRecords": [ { "CreatedAt": 1.676038494E9, "ModifiedAt": 1.676038494E9, "SamplingRule": { "Attributes": { "foo": "bar", "abc": "1234" }, "FixedRate": 0.05, "HTTPMethod": "*", "Host": "*", "Priority": 10000, "ReservoirSize": 100, "ResourceARN": "*", "RuleARN": "arn:aws:xray:us-east-1:999999999999:sampling-rule/Default", "RuleName": "Default", "ServiceName": "*", "ServiceType": "*", "URLPath": "*", "Version": 1 } }, { "CreatedAt": 1.67799933E9, "ModifiedAt": 1.67799933E9, "SamplingRule": { "Attributes": { "abc": "1234" }, "FixedRate": 0.11, "HTTPMethod": "*", "Host": "*", "Priority": 20, "ReservoirSize": 1, "ResourceARN": "*", "RuleARN": "arn:aws:xray:us-east-1:999999999999:sampling-rule/test", "RuleName": "test", "ServiceName": "*", "ServiceType": "*", "URLPath": "*", "Version": 1 } } ] }get-sampling-rules-response-sample.json000066400000000000000000000040441527070072300451010ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler/data{ "NextToken": null, "SamplingRuleRecords": [ { "CreatedAt": 1.67799933E9, "ModifiedAt": 1.67799933E9, "SamplingRule": { "Attributes": { "foo": "bar", "doo": "baz" }, "FixedRate": 0.05, "HTTPMethod": "*", "Host": "*", "Priority": 1000, "ReservoirSize": 10, "ResourceARN": "*", "RuleARN": "arn:aws:xray:us-west-2:123456789000:sampling-rule/Rule1", "RuleName": "Rule1", "ServiceName": "*", "ServiceType": "AWS::Foo::Bar", "URLPath": "*", "Version": 1 } }, { "CreatedAt": 0.0, "ModifiedAt": 1.611564245E9, "SamplingRule": { "Attributes": {}, "FixedRate": 0.05, "HTTPMethod": "*", "Host": "*", "Priority": 10000, "ReservoirSize": 1, "ResourceARN": "*", "RuleARN": "arn:aws:xray:us-west-2:123456789000:sampling-rule/Default", "RuleName": "Default", "ServiceName": "*", "ServiceType": "*", "URLPath": "*", "Version": 1 } }, { "CreatedAt": 1.676038494E9, "ModifiedAt": 1.676038494E9, "SamplingRule": { "Attributes": {}, "FixedRate": 0.2, "HTTPMethod": "GET", "Host": "*", "Priority": 1, "ReservoirSize": 10, "ResourceARN": "*", "RuleARN": "arn:aws:xray:us-west-2:123456789000:sampling-rule/Rule2", "RuleName": "Rule2", "ServiceName": "FooBar", "ServiceType": "*", "URLPath": "/foo/bar", "Version": 1 } } ] }get-sampling-targets-response-sample.json000066400000000000000000000007661527070072300454270ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler/data{ "LastRuleModification": 1707551387.0, "SamplingTargetDocuments": [ { "FixedRate": 0.10, "Interval": 10, "ReservoirQuota": 30, "ReservoirQuotaTTL": 1707764006.0, "RuleName": "test" }, { "FixedRate": 0.05, "Interval": 10, "ReservoirQuota": 0, "ReservoirQuotaTTL": 1707764006.0, "RuleName": "Default" } ], "UnprocessedStatistics": [] }test-remote-sampler_sampling-rules-response-sample.json000066400000000000000000000025541527070072300503210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler/data{ "NextToken": null, "SamplingRuleRecords": [ { "CreatedAt": 1.676038494E9, "ModifiedAt": 1.676038494E9, "SamplingRule": { "Attributes": {}, "FixedRate": 1.0, "HTTPMethod": "*", "Host": "*", "Priority": 10000, "ReservoirSize": 0, "ResourceARN": "*", "RuleARN": "arn:aws:xray:us-east-1:999999999999:sampling-rule/Default", "RuleName": "Default", "ServiceName": "*", "ServiceType": "*", "URLPath": "*", "Version": 1 } }, { "CreatedAt": 1.67799933E9, "ModifiedAt": 1.67799933E9, "SamplingRule": { "Attributes": { "abc": "1234" }, "FixedRate": 0, "HTTPMethod": "*", "Host": "*", "Priority": 20, "ReservoirSize": 0, "ResourceARN": "*", "RuleARN": "arn:aws:xray:us-east-1:999999999999:sampling-rule/test", "RuleName": "test", "ServiceName": "*", "ServiceType": "*", "URLPath": "*", "Version": 1 } } ] }test-remote-sampler_sampling-targets-response-sample.json000066400000000000000000000010001527070072300506210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler/data{ "LastRuleModification": 1707551387.0, "SamplingTargetDocuments": [ { "FixedRate": 0.0, "Interval": 100000, "ReservoirQuota": 100000, "ReservoirQuotaTTL": 9999999999.0, "RuleName": "test" }, { "FixedRate": 0.0, "Interval": 1000, "ReservoirQuota": 100, "ReservoirQuotaTTL": 9999999999.0, "RuleName": "Default" } ], "UnprocessedStatistics": [] }test_aws_xray_remote_sampler.py000066400000000000000000000366101527070072300430130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import json import os import threading import time from logging import DEBUG from unittest import TestCase from unittest.mock import patch from pytest import mark # pylint: disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace.sampler.aws_xray_remote_sampler import ( AwsXRayRemoteSampler, _InternalAwsXRayRemoteSampler, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import Tracer, TracerProvider from opentelemetry.sdk.trace.sampling import Decision from ._mock_clock import MockClock TEST_DIR = os.path.dirname(os.path.realpath(__file__)) DATA_DIR = os.path.join(TEST_DIR, "data") def create_spans(sampled_array, thread_id, span_attributes, remote_sampler, number_of_spans): sampled = 0 for _ in range(number_of_spans): if remote_sampler.should_sample(None, 0, "name", attributes=span_attributes).decision != Decision.DROP: sampled += 1 sampled_array[thread_id] = sampled def mocked_requests_get(*args, **kwargs): class MockResponse: def __init__(self, json_data, status_code): self.json_data = json_data self.status_code = status_code def json(self): return self.json_data if kwargs["url"] == "http://127.0.0.1:2000/GetSamplingRules": with open( f"{DATA_DIR}/test-remote-sampler_sampling-rules-response-sample.json", encoding="UTF-8", ) as file: sample_response = json.load(file) file.close() return MockResponse(sample_response, 200) if kwargs["url"] == "http://127.0.0.1:2000/SamplingTargets": with open( f"{DATA_DIR}/test-remote-sampler_sampling-targets-response-sample.json", encoding="UTF-8", ) as file: sample_response = json.load(file) file.close() return MockResponse(sample_response, 200) return MockResponse(None, 404) class TestAwsXRayRemoteSampler(TestCase): def setUp(self): self.rs = None def tearDown(self): # Clean up timers if self.rs is not None: self.rs._root._root._rules_timer.cancel() self.rs._root._root._targets_timer.cancel() def test_create_remote_sampler_with_empty_resource(self): self.rs = AwsXRayRemoteSampler(resource=Resource.get_empty()) self.assertIsNotNone(self.rs._root._root._rules_timer) self.assertEqual( self.rs._root._root._InternalAwsXRayRemoteSampler__polling_interval, 300, ) self.assertIsNotNone(self.rs._root._root._InternalAwsXRayRemoteSampler__xray_client) self.assertIsNotNone(self.rs._root._root._InternalAwsXRayRemoteSampler__resource) self.assertTrue( len(self.rs._root._root._InternalAwsXRayRemoteSampler__client_id), 24, ) def test_create_remote_sampler_with_populated_resource(self): self.rs = AwsXRayRemoteSampler( resource=Resource.create( { "service.name": "test-service-name", "cloud.platform": "test-cloud-platform", } ) ) self.assertIsNotNone(self.rs._root._root._rules_timer) self.assertEqual( self.rs._root._root._InternalAwsXRayRemoteSampler__polling_interval, 300, ) self.assertIsNotNone(self.rs._root._root._InternalAwsXRayRemoteSampler__xray_client) self.assertIsNotNone(self.rs._root._root._InternalAwsXRayRemoteSampler__resource) self.assertEqual( self.rs._root._root._InternalAwsXRayRemoteSampler__resource.attributes["service.name"], "test-service-name", ) self.assertEqual( self.rs._root._root._InternalAwsXRayRemoteSampler__resource.attributes["cloud.platform"], "test-cloud-platform", ) def test_create_remote_sampler_with_all_fields_populated(self): self.rs = AwsXRayRemoteSampler( resource=Resource.create( { "service.name": "test-service-name", "cloud.platform": "test-cloud-platform", } ), endpoint="http://abc.com", polling_interval=120, log_level=DEBUG, ) self.assertIsNotNone(self.rs._root._root._rules_timer) self.assertEqual( self.rs._root._root._InternalAwsXRayRemoteSampler__polling_interval, 120, ) self.assertIsNotNone(self.rs._root._root._InternalAwsXRayRemoteSampler__xray_client) self.assertIsNotNone(self.rs._root._root._InternalAwsXRayRemoteSampler__resource) self.assertEqual( self.rs._root._root._InternalAwsXRayRemoteSampler__xray_client._AwsXRaySamplingClient__get_sampling_rules_endpoint, "http://abc.com/GetSamplingRules", ) self.assertEqual( self.rs._root._root._InternalAwsXRayRemoteSampler__resource.attributes["service.name"], "test-service-name", ) self.assertEqual( self.rs._root._root._InternalAwsXRayRemoteSampler__resource.attributes["cloud.platform"], "test-cloud-platform", ) @patch("requests.Session.post", side_effect=mocked_requests_get) @patch( "opentelemetry.sdk.extension.aws.trace.sampler.aws_xray_remote_sampler.DEFAULT_TARGET_POLLING_INTERVAL_SECONDS", 2, ) def test_update_sampling_rules_and_targets_with_pollers_and_should_sample(self, mock_post=None): self.rs = AwsXRayRemoteSampler( resource=Resource.create( { "service.name": "test-service-name", "cloud.platform": "test-cloud-platform", } ) ) self.assertEqual( self.rs._root._root._InternalAwsXRayRemoteSampler__target_polling_interval, 2, ) time.sleep(1.0) self.assertEqual( self.rs._root._root._InternalAwsXRayRemoteSampler__rule_cache._RuleCache__rule_appliers[ 0 ].sampling_rule.RuleName, "test", ) self.assertEqual( self.rs.should_sample(None, 0, "name", attributes={"abc": "1234"}).decision, Decision.DROP, ) # wait 2 more seconds since targets polling was patched to 2 seconds (rather than 10s) time.sleep(2.0) self.assertEqual( self.rs._root._root._InternalAwsXRayRemoteSampler__target_polling_interval, 1000, ) self.assertEqual( self.rs.should_sample(None, 0, "name", attributes={"abc": "1234"}).decision, Decision.RECORD_AND_SAMPLE, ) self.assertEqual( self.rs.should_sample(None, 0, "name", attributes={"abc": "1234"}).decision, Decision.RECORD_AND_SAMPLE, ) self.assertEqual( self.rs.should_sample(None, 0, "name", attributes={"abc": "1234"}).decision, Decision.RECORD_AND_SAMPLE, ) @mark.skip(reason="Uses sleep in test, which could be flaky. Remove this skip for validation locally.") @patch("requests.Session.post", side_effect=mocked_requests_get) @patch( "opentelemetry.sdk.extension.aws.trace.sampler.aws_xray_remote_sampler.DEFAULT_TARGET_POLLING_INTERVAL_SECONDS", 3, ) def test_multithreading_with_large_reservoir_with_otel_sdk(self, mock_post=None): self.rs = AwsXRayRemoteSampler( resource=Resource.create( { "service.name": "test-service-name", "cloud.platform": "test-cloud-platform", } ) ) attributes = {"abc": "1234"} time.sleep(2.0) self.assertEqual( self.rs.should_sample(None, 0, "name", attributes=attributes).decision, Decision.DROP, ) # wait 3 more seconds since targets polling was patched to 2 seconds (rather than 10s) time.sleep(3.0) number_of_spans = 100 thread_count = 1000 sampled_array = [] threads = [] for idx in range(thread_count): sampled_array.append(0) threads.append( threading.Thread( target=create_spans, name="thread_" + str(idx), daemon=True, args=( sampled_array, idx, attributes, self.rs, number_of_spans, ), ) ) threads[idx].start() sum_sampled = 0 for idx in range(thread_count): threads[idx].join() sum_sampled += sampled_array[idx] test_rule_applier = self.rs._root._root._InternalAwsXRayRemoteSampler__rule_cache._RuleCache__rule_appliers[0] self.assertEqual( test_rule_applier._SamplingRuleApplier__reservoir_sampler._RateLimitingSampler__reservoir._quota, 100000, ) self.assertEqual(sum_sampled, 100000) # pylint: disable=no-member @mark.skip(reason="Uses sleep in test, which could be flaky. Remove this skip for validation locally.") @patch("requests.Session.post", side_effect=mocked_requests_get) @patch( "opentelemetry.sdk.extension.aws.trace.sampler.aws_xray_remote_sampler.DEFAULT_TARGET_POLLING_INTERVAL_SECONDS", 2, ) @patch( "opentelemetry.sdk.extension.aws.trace.sampler.aws_xray_remote_sampler._Clock", MockClock, ) def test_multithreading_with_some_reservoir_with_otel_sdk(self, mock_post=None): self.rs = AwsXRayRemoteSampler( resource=Resource.create( { "service.name": "test-service-name", "cloud.platform": "test-cloud-platform", } ) ) attributes = {"abc": "non-matching attribute value, use default rule"} # Using normal clock, finishing all thread jobs will take more than a second, # which will eat up more than 1 second of reservoir. Using MockClock we can freeze time # and pretend all thread jobs start and end at the exact same time, # assume and test exactly 1 second of reservoir (100 quota) only mock_clock: MockClock = self.rs._root._root._clock time.sleep(1.0) mock_clock.add_time(1.0) self.assertEqual(mock_clock.now(), self.rs._root._root._clock.now()) self.assertEqual( self.rs.should_sample(None, 0, "name", attributes=attributes).decision, Decision.RECORD_AND_SAMPLE, ) # wait 2 more seconds since targets polling was patched to 2 seconds (rather than 10s) time.sleep(2.0) mock_clock.add_time(2.0) self.assertEqual(mock_clock.now(), self.rs._root._root._clock.now()) number_of_spans = 100 thread_count = 1000 sampled_array = [] threads = [] for idx in range(thread_count): sampled_array.append(0) threads.append( threading.Thread( target=create_spans, name="thread_" + str(idx), daemon=True, args=( sampled_array, idx, attributes, self.rs, number_of_spans, ), ) ) threads[idx].start() sum_sampled = 0 for idx in range(thread_count): threads[idx].join() sum_sampled += sampled_array[idx] default_rule_applier = self.rs._root._root._InternalAwsXRayRemoteSampler__rule_cache._RuleCache__rule_appliers[ 1 ] self.assertEqual( default_rule_applier._SamplingRuleApplier__reservoir_sampler._RateLimitingSampler__reservoir._quota, 100, ) self.assertEqual(sum_sampled, 100) def test_get_description(self) -> str: self.rs: AwsXRayRemoteSampler = AwsXRayRemoteSampler(resource=Resource.create({"service.name": "dummy_name"})) self.assertEqual( self.rs.get_description(), "AwsXRayRemoteSampler{root:ParentBased{root:_InternalAwsXRayRemoteSampler{remote sampling with AWS X-Ray},remoteParentSampled:AlwaysOnSampler,remoteParentNotSampled:AlwaysOffSampler,localParentSampled:AlwaysOnSampler,localParentNotSampled:AlwaysOffSampler}}", ) @patch("requests.Session.post", side_effect=mocked_requests_get) def test_parent_based_xray_sampler_updates_statistics_once_for_one_parent_span_with_two_children( self, mock_post=None ): self.rs: AwsXRayRemoteSampler = AwsXRayRemoteSampler( resource=Resource.create({"service.name": "use-default-sample-all-rule"}) ) time.sleep(1.0) provider = TracerProvider(sampler=self.rs) tracer: Tracer = provider.get_tracer("test_tracer_1") # child1 and child2 are child spans of root parent0 # For AwsXRayRemoteSampler (ParentBased), expect only parent0 to update statistics with tracer.start_as_current_span("parent0") as _: with tracer.start_as_current_span("child1") as _: pass with tracer.start_as_current_span("child2") as _: pass default_rule_applier = self.rs._root._root._InternalAwsXRayRemoteSampler__rule_cache._RuleCache__rule_appliers[ 1 ] self.assertEqual( default_rule_applier._SamplingRuleApplier__statistics.RequestCount, 1, ) self.assertEqual( default_rule_applier._SamplingRuleApplier__statistics.SampleCount, 1, ) @patch("requests.Session.post", side_effect=mocked_requests_get) def test_non_parent_based_xray_sampler_updates_statistics_thrice_for_one_parent_span_with_two_children( self, mock_post=None ): non_parent_based_xray_sampler: _InternalAwsXRayRemoteSampler = _InternalAwsXRayRemoteSampler( resource=Resource.create({"service.name": "use-default-sample-all-rule"}) ) time.sleep(1.0) provider = TracerProvider(sampler=non_parent_based_xray_sampler) tracer: Tracer = provider.get_tracer("test_tracer_2") # child1 and child2 are child spans of root parent0 # For _InternalAwsXRayRemoteSampler (Non-ParentBased), expect all 3 spans to update statistics with tracer.start_as_current_span("parent0") as _: with tracer.start_as_current_span("child1") as _: pass with tracer.start_as_current_span("child2") as _: pass default_rule_applier = ( non_parent_based_xray_sampler._InternalAwsXRayRemoteSampler__rule_cache._RuleCache__rule_appliers[1] ) self.assertEqual( default_rule_applier._SamplingRuleApplier__statistics.RequestCount, 3, ) self.assertEqual( default_rule_applier._SamplingRuleApplier__statistics.SampleCount, 3, ) non_parent_based_xray_sampler._rules_timer.cancel() non_parent_based_xray_sampler._targets_timer.cancel() test_aws_xray_sampling_client.py000066400000000000000000000203531527070072300431420ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import json import os from logging import getLogger from unittest import TestCase from unittest.mock import patch # pylint: disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace.sampler._aws_xray_sampling_client import ( _AwsXRaySamplingClient, ) SAMPLING_CLIENT_LOGGER_NAME = "opentelemetry.sdk.extension.aws.trace.sampler._aws_xray_sampling_client" _sampling_client_logger = getLogger(SAMPLING_CLIENT_LOGGER_NAME) TEST_DIR = os.path.dirname(os.path.realpath(__file__)) DATA_DIR = os.path.join(TEST_DIR, "data") class TestAwsXRaySamplingClient(TestCase): @patch("requests.Session.post") def test_get_no_sampling_rules(self, mock_post=None): mock_post.return_value.configure_mock(**{"json.return_value": {"SamplingRuleRecords": []}}) client = _AwsXRaySamplingClient("http://127.0.0.1:2000") sampling_rules = client.get_sampling_rules() self.assertTrue(len(sampling_rules) == 0) @patch("requests.Session.post") def test_get_invalid_responses(self, mock_post=None): mock_post.return_value.configure_mock(**{"json.return_value": {}}) client = _AwsXRaySamplingClient("http://127.0.0.1:2000") with self.assertLogs(_sampling_client_logger, level="ERROR"): sampling_rules = client.get_sampling_rules() self.assertTrue(len(sampling_rules) == 0) @patch("requests.Session.post") def test_get_sampling_rule_missing_in_records(self, mock_post=None): mock_post.return_value.configure_mock(**{"json.return_value": {"SamplingRuleRecords": [{}]}}) client = _AwsXRaySamplingClient("http://127.0.0.1:2000") with self.assertLogs(_sampling_client_logger, level="ERROR"): sampling_rules = client.get_sampling_rules() self.assertTrue(len(sampling_rules) == 0) @patch("requests.Session.post") def test_default_values_used_when_missing_properties_in_sampling_rule(self, mock_post=None): mock_post.return_value.configure_mock(**{"json.return_value": {"SamplingRuleRecords": [{"SamplingRule": {}}]}}) client = _AwsXRaySamplingClient("http://127.0.0.1:2000") sampling_rules = client.get_sampling_rules() self.assertTrue(len(sampling_rules) == 1) sampling_rule = sampling_rules[0] self.assertEqual(sampling_rule.Attributes, {}) self.assertEqual(sampling_rule.FixedRate, 0.0) self.assertEqual(sampling_rule.HTTPMethod, "") self.assertEqual(sampling_rule.Host, "") self.assertEqual(sampling_rule.Priority, 10001) self.assertEqual(sampling_rule.ReservoirSize, 0) self.assertEqual(sampling_rule.ResourceARN, "") self.assertEqual(sampling_rule.RuleARN, "") self.assertEqual(sampling_rule.RuleName, "") self.assertEqual(sampling_rule.ServiceName, "") self.assertEqual(sampling_rule.ServiceType, "") self.assertEqual(sampling_rule.URLPath, "") self.assertEqual(sampling_rule.Version, 0) @patch("requests.Session.post") def test_get_correct_number_of_sampling_rules(self, mock_post=None): sampling_records = [] with open( f"{DATA_DIR}/get-sampling-rules-response-sample.json", encoding="UTF-8", ) as file: sample_response = json.load(file) sampling_records = sample_response["SamplingRuleRecords"] mock_post.return_value.configure_mock(**{"json.return_value": sample_response}) file.close() client = _AwsXRaySamplingClient("http://127.0.0.1:2000") sampling_rules = client.get_sampling_rules() self.assertEqual(len(sampling_rules), 3) self.assertEqual(len(sampling_rules), len(sampling_records)) self.validate_match_sampling_rules_properties_with_records(sampling_rules, sampling_records) def validate_match_sampling_rules_properties_with_records(self, sampling_rules, sampling_records): for _, (sampling_rule, sampling_record) in enumerate(zip(sampling_rules, sampling_records)): self.assertIsNotNone(sampling_rule.Attributes) self.assertEqual( sampling_rule.Attributes, sampling_record["SamplingRule"]["Attributes"], ) self.assertIsNotNone(sampling_rule.FixedRate) self.assertEqual( sampling_rule.FixedRate, sampling_record["SamplingRule"]["FixedRate"], ) self.assertIsNotNone(sampling_rule.HTTPMethod) self.assertEqual( sampling_rule.HTTPMethod, sampling_record["SamplingRule"]["HTTPMethod"], ) self.assertIsNotNone(sampling_rule.Host) self.assertEqual(sampling_rule.Host, sampling_record["SamplingRule"]["Host"]) self.assertIsNotNone(sampling_rule.Priority) self.assertEqual( sampling_rule.Priority, sampling_record["SamplingRule"]["Priority"], ) self.assertIsNotNone(sampling_rule.ReservoirSize) self.assertEqual( sampling_rule.ReservoirSize, sampling_record["SamplingRule"]["ReservoirSize"], ) self.assertIsNotNone(sampling_rule.ResourceARN) self.assertEqual( sampling_rule.ResourceARN, sampling_record["SamplingRule"]["ResourceARN"], ) self.assertIsNotNone(sampling_rule.RuleARN) self.assertEqual( sampling_rule.RuleARN, sampling_record["SamplingRule"]["RuleARN"], ) self.assertIsNotNone(sampling_rule.RuleName) self.assertEqual( sampling_rule.RuleName, sampling_record["SamplingRule"]["RuleName"], ) self.assertIsNotNone(sampling_rule.ServiceName) self.assertEqual( sampling_rule.ServiceName, sampling_record["SamplingRule"]["ServiceName"], ) self.assertIsNotNone(sampling_rule.ServiceType) self.assertEqual( sampling_rule.ServiceType, sampling_record["SamplingRule"]["ServiceType"], ) self.assertIsNotNone(sampling_rule.URLPath) self.assertEqual( sampling_rule.URLPath, sampling_record["SamplingRule"]["URLPath"], ) self.assertIsNotNone(sampling_rule.Version) self.assertEqual( sampling_rule.Version, sampling_record["SamplingRule"]["Version"], ) @patch("requests.Session.post") def test_get_sampling_targets(self, mock_post=None): with open( f"{DATA_DIR}/get-sampling-targets-response-sample.json", encoding="UTF-8", ) as file: sample_response = json.load(file) mock_post.return_value.configure_mock(**{"json.return_value": sample_response}) file.close() client = _AwsXRaySamplingClient("http://127.0.0.1:2000") sampling_targets_response = client.get_sampling_targets(statistics=[]) self.assertEqual(len(sampling_targets_response.SamplingTargetDocuments), 2) self.assertEqual(len(sampling_targets_response.UnprocessedStatistics), 0) self.assertEqual(sampling_targets_response.LastRuleModification, 1707551387.0) @patch("requests.Session.post") def test_get_invalid_sampling_targets(self, mock_post=None): mock_post.return_value.configure_mock( **{ "json.return_value": { "LastRuleModification": None, "SamplingTargetDocuments": None, "UnprocessedStatistics": None, } } ) client = _AwsXRaySamplingClient("http://127.0.0.1:2000") sampling_targets_response = client.get_sampling_targets(statistics=[]) self.assertEqual(sampling_targets_response.SamplingTargetDocuments, []) self.assertEqual(sampling_targets_response.UnprocessedStatistics, []) self.assertEqual(sampling_targets_response.LastRuleModification, 0.0) test_clock.py000066400000000000000000000012301527070072300371410ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase # pylint: disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace.sampler._clock import _Clock class TestClock(TestCase): def test_from_timestamp(self): pass def test_time_delta(self): clock = _Clock() dt = clock.from_timestamp(1707551387.0) delta = clock.time_delta(3600) new_dt = dt + delta self.assertTrue(new_dt.timestamp() - dt.timestamp() == 3600) test_fallback_sampler.py000066400000000000000000000062111527070072300413340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import datetime from unittest import TestCase # pylint: disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace.sampler._fallback_sampler import ( _FallbackSampler, ) from opentelemetry.sdk.trace.sampling import ALWAYS_OFF, Decision from ._mock_clock import MockClock class TestRateLimitingSampler(TestCase): # pylint: disable=too-many-branches def test_should_sample(self): time_now = datetime.datetime.fromtimestamp(1707551387.0) clock = MockClock(time_now) sampler = _FallbackSampler(clock) # Ignore testing TraceIdRatioBased sampler._FallbackSampler__fixed_rate_sampler = ALWAYS_OFF sampler.should_sample(None, 1234, "name") # Essentially the same tests as test_rate_limiter.py # 0 seconds passed, 0 quota available sampled = 0 for _ in range(30): if sampler.should_sample(None, 1234, "name").decision != Decision.DROP: sampled += 1 self.assertEqual(sampled, 0) # 0.4 seconds passed, 0.4 quota available sampled = 0 clock.add_time(0.4) for _ in range(30): if sampler.should_sample(None, 1234, "name").decision != Decision.DROP: sampled += 1 self.assertEqual(sampled, 0) # 0.8 seconds passed, 0.8 quota available sampled = 0 clock.add_time(0.4) for _ in range(30): if sampler.should_sample(None, 1234, "name").decision != Decision.DROP: sampled += 1 self.assertEqual(sampled, 0) # 1.2 seconds passed, 1 quota consumed, 0 quota available sampled = 0 clock.add_time(0.4) for _ in range(30): if sampler.should_sample(None, 1234, "name").decision != Decision.DROP: sampled += 1 self.assertEqual(sampled, 1) # 1.6 seconds passed, 0.4 quota available sampled = 0 clock.add_time(0.4) for _ in range(30): if sampler.should_sample(None, 1234, "name").decision != Decision.DROP: sampled += 1 self.assertEqual(sampled, 0) # 2.0 seconds passed, 0.8 quota available sampled = 0 clock.add_time(0.4) for _ in range(30): if sampler.should_sample(None, 1234, "name").decision != Decision.DROP: sampled += 1 self.assertEqual(sampled, 0) # 2.4 seconds passed, one more quota consumed, 0 quota available sampled = 0 clock.add_time(0.4) for _ in range(30): if sampler.should_sample(None, 1234, "name").decision != Decision.DROP: sampled += 1 self.assertEqual(sampled, 1) # 30 seconds passed, only one quota can be consumed sampled = 0 clock.add_time(100) for _ in range(30): if sampler.should_sample(None, 1234, "name").decision != Decision.DROP: sampled += 1 self.assertEqual(sampled, 1) test_matcher.py000066400000000000000000000041541527070072300375010ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase # pylint: disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace.sampler._matcher import _Matcher from opentelemetry.util.types import Attributes class TestMatcher(TestCase): def test_wild_card_match(self): test_cases = [ [None, "*"], ["", "*"], ["HelloWorld", "*"], ["HelloWorld", "HelloWorld"], ["HelloWorld", "Hello*"], ["HelloWorld", "*World"], ["HelloWorld", "?ello*"], ["HelloWorld", "Hell?W*d"], ["Hello.World", "*.World"], ["Bye.World", "*.World"], ] for test_case in test_cases: self.assertTrue(_Matcher.wild_card_match(text=test_case[0], pattern=test_case[1])) def test_wild_card_not_match(self): test_cases = [[None, "Hello*"], ["HelloWorld", None]] for test_case in test_cases: self.assertFalse(_Matcher.wild_card_match(text=test_case[0], pattern=test_case[1])) def test_attribute_matching(self): attributes: Attributes = { "dog": "bark", "cat": "meow", "cow": "mooo", } rule_attributes = { "dog": "bar?", "cow": "mooo", } self.assertTrue(_Matcher.attribute_match(attributes, rule_attributes)) def test_attribute_matching_without_rule_attributes(self): attributes = { "dog": "bark", "cat": "meow", "cow": "mooo", } rule_attributes = {} print("LENGTH %s", len(rule_attributes)) self.assertTrue(_Matcher.attribute_match(attributes, rule_attributes)) def test_attribute_matching_without_span_attributes(self): attributes = {} rule_attributes = { "dog": "bar?", "cow": "mooo", } self.assertFalse(_Matcher.attribute_match(attributes, rule_attributes)) test_rate_limiter.py000066400000000000000000000021771527070072300405410ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import datetime from unittest import TestCase # pylint: disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace.sampler._rate_limiter import ( _RateLimiter, ) from ._mock_clock import MockClock class TestRateLimiter(TestCase): def test_try_spend(self): time_now = datetime.datetime.fromtimestamp(1707551387.0) clock = MockClock(time_now) rate_limiter = _RateLimiter(1, 30, clock) spent = 0 for _ in range(100): if rate_limiter.try_spend(1): spent += 1 self.assertEqual(spent, 0) spent = 0 clock.add_time(0.5) for _ in range(100): if rate_limiter.try_spend(1): spent += 1 self.assertEqual(spent, 15) spent = 0 clock.add_time(1000) for _ in range(100): if rate_limiter.try_spend(1): spent += 1 self.assertEqual(spent, 30) test_rate_limiting_sampler.py000066400000000000000000000056451527070072300424360ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import datetime from unittest import TestCase # pylint: disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace.sampler._rate_limiting_sampler import ( _RateLimitingSampler, ) from opentelemetry.sdk.trace.sampling import Decision from ._mock_clock import MockClock class TestRateLimitingSampler(TestCase): def test_should_sample(self): time_now = datetime.datetime.fromtimestamp(1707551387.0) clock = MockClock(time_now) sampler = _RateLimitingSampler(30, clock) # Essentially the same tests as test_rate_limiter.py sampled = 0 for _ in range(100): if sampler.should_sample(None, 1234, "name").decision != Decision.DROP: sampled += 1 self.assertEqual(sampled, 0) sampled = 0 clock.add_time(0.5) for _ in range(100): if sampler.should_sample(None, 1234, "name").decision != Decision.DROP: sampled += 1 self.assertEqual(sampled, 15) sampled = 0 clock.add_time(1.0) for _ in range(100): if sampler.should_sample(None, 1234, "name").decision != Decision.DROP: sampled += 1 self.assertEqual(sampled, 30) sampled = 0 clock.add_time(2.5) for _ in range(100): if sampler.should_sample(None, 1234, "name").decision != Decision.DROP: sampled += 1 self.assertEqual(sampled, 30) sampled = 0 clock.add_time(1000) for _ in range(100): if sampler.should_sample(None, 1234, "name").decision != Decision.DROP: sampled += 1 self.assertEqual(sampled, 30) def test_should_sample_with_quota_of_one(self): time_now = datetime.datetime.fromtimestamp(1707551387.0) clock = MockClock(time_now) sampler = _RateLimitingSampler(1, clock) sampled = 0 for _ in range(50): if sampler.should_sample(None, 1234, "name").decision != Decision.DROP: sampled += 1 self.assertEqual(sampled, 0) sampled = 0 clock.add_time(0.5) for _ in range(50): if sampler.should_sample(None, 1234, "name").decision != Decision.DROP: sampled += 1 self.assertEqual(sampled, 0) sampled = 0 clock.add_time(0.5) for _ in range(50): if sampler.should_sample(None, 1234, "name").decision != Decision.DROP: sampled += 1 self.assertEqual(sampled, 1) sampled = 0 clock.add_time(1000) for _ in range(50): if sampler.should_sample(None, 1234, "name").decision != Decision.DROP: sampled += 1 self.assertEqual(sampled, 1) test_rule_cache.py000066400000000000000000000262741527070072300401570ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import datetime from threading import Lock from unittest import TestCase # pylint: disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace.sampler._clock import _Clock from opentelemetry.sdk.extension.aws.trace.sampler._rule_cache import ( CACHE_TTL_SECONDS, _RuleCache, ) from opentelemetry.sdk.extension.aws.trace.sampler._sampling_rule import ( _SamplingRule, ) from opentelemetry.sdk.extension.aws.trace.sampler._sampling_rule_applier import ( _SamplingRuleApplier, ) from opentelemetry.sdk.extension.aws.trace.sampler._sampling_statistics_document import ( _SamplingStatisticsDocument, ) from opentelemetry.sdk.extension.aws.trace.sampler._sampling_target import ( _SamplingTargetResponse, ) from opentelemetry.sdk.resources import Resource from ._mock_clock import MockClock CLIENT_ID = "12345678901234567890abcd" # pylint: disable=no-member disable=C0103 class TestRuleCache(TestCase): def test_cache_update_rules_and_sorts_rules(self): cache = _RuleCache(None, None, CLIENT_ID, _Clock(), Lock()) self.assertTrue(len(cache._RuleCache__rule_appliers) == 0) rule1 = _SamplingRule(Priority=200, RuleName="only_one_rule", Version=1) rules = [rule1] cache.update_sampling_rules(rules) self.assertTrue(len(cache._RuleCache__rule_appliers) == 1) rule1 = _SamplingRule(Priority=200, RuleName="abcdef", Version=1) rule2 = _SamplingRule(Priority=100, RuleName="abc", Version=1) rule3 = _SamplingRule(Priority=100, RuleName="Abc", Version=1) rule4 = _SamplingRule(Priority=100, RuleName="ab", Version=1) rule5 = _SamplingRule(Priority=100, RuleName="A", Version=1) rule6 = _SamplingRule(Priority=1, RuleName="abcdef", Version=1) rules = [rule1, rule2, rule3, rule4, rule5, rule6] cache.update_sampling_rules(rules) self.assertTrue(len(cache._RuleCache__rule_appliers) == 6) self.assertEqual(cache._RuleCache__rule_appliers[0].sampling_rule.RuleName, "abcdef") self.assertEqual(cache._RuleCache__rule_appliers[1].sampling_rule.RuleName, "A") self.assertEqual(cache._RuleCache__rule_appliers[2].sampling_rule.RuleName, "Abc") self.assertEqual(cache._RuleCache__rule_appliers[3].sampling_rule.RuleName, "ab") self.assertEqual(cache._RuleCache__rule_appliers[4].sampling_rule.RuleName, "abc") self.assertEqual(cache._RuleCache__rule_appliers[5].sampling_rule.RuleName, "abcdef") def test_rule_cache_expiration_logic(self): dt = datetime cache = _RuleCache(None, Resource.get_empty(), CLIENT_ID, _Clock(), Lock()) self.assertFalse(cache.expired()) cache._last_modified = dt.datetime.now() - dt.timedelta(seconds=CACHE_TTL_SECONDS - 5) self.assertFalse(cache.expired()) cache._last_modified = dt.datetime.now() - dt.timedelta(seconds=CACHE_TTL_SECONDS + 1) self.assertTrue(cache.expired()) def test_update_cache_with_only_one_rule_changed(self): cache = _RuleCache(None, Resource.get_empty(), CLIENT_ID, _Clock(), Lock()) rule1 = _SamplingRule(Priority=1, RuleName="abcdef", Version=1) rule2 = _SamplingRule(Priority=10, RuleName="ab", Version=1) rule3 = _SamplingRule(Priority=100, RuleName="Abc", Version=1) rules = [rule1, rule2, rule3] cache.update_sampling_rules(rules) cache_rules_copy = cache._RuleCache__rule_appliers new_rule3 = _SamplingRule(Priority=5, RuleName="Abc", Version=1) rules = [rule1, rule2, new_rule3] cache.update_sampling_rules(rules) self.assertTrue(len(cache._RuleCache__rule_appliers) == 3) self.assertEqual(cache._RuleCache__rule_appliers[0].sampling_rule.RuleName, "abcdef") self.assertEqual(cache._RuleCache__rule_appliers[1].sampling_rule.RuleName, "Abc") self.assertEqual(cache._RuleCache__rule_appliers[2].sampling_rule.RuleName, "ab") # Compare that only rule1 and rule2 objects have not changed due to new_rule3 even after sorting self.assertTrue(cache_rules_copy[0] is cache._RuleCache__rule_appliers[0]) self.assertTrue(cache_rules_copy[1] is cache._RuleCache__rule_appliers[2]) self.assertTrue(cache_rules_copy[2] is not cache._RuleCache__rule_appliers[1]) def test_update_rules_removes_older_rule(self): cache = _RuleCache(None, None, CLIENT_ID, _Clock(), Lock()) self.assertTrue(len(cache._RuleCache__rule_appliers) == 0) rule1 = _SamplingRule(Priority=200, RuleName="first_rule", Version=1) rules = [rule1] cache.update_sampling_rules(rules) self.assertTrue(len(cache._RuleCache__rule_appliers) == 1) self.assertEqual( cache._RuleCache__rule_appliers[0].sampling_rule.RuleName, "first_rule", ) rule1 = _SamplingRule(Priority=200, RuleName="second_rule", Version=1) rules = [rule1] cache.update_sampling_rules(rules) self.assertTrue(len(cache._RuleCache__rule_appliers) == 1) self.assertEqual( cache._RuleCache__rule_appliers[0].sampling_rule.RuleName, "second_rule", ) def test_update_sampling_targets(self): sampling_rule_1 = _SamplingRule( Attributes={}, FixedRate=0.05, HTTPMethod="*", Host="*", Priority=10000, ReservoirSize=1, ResourceARN="*", RuleARN="arn:aws:xray:us-east-1:999999999999:sampling-rule/default", RuleName="default", ServiceName="*", ServiceType="*", URLPath="*", Version=1, ) sampling_rule_2 = _SamplingRule( Attributes={}, FixedRate=0.20, HTTPMethod="*", Host="*", Priority=20, ReservoirSize=10, ResourceARN="*", RuleARN="arn:aws:xray:us-east-1:999999999999:sampling-rule/test", RuleName="test", ServiceName="*", ServiceType="*", URLPath="*", Version=1, ) time_now = datetime.datetime.fromtimestamp(1707551387.0) mock_clock = MockClock(time_now) rule_cache = _RuleCache(Resource.get_empty(), None, "", mock_clock, Lock()) rule_cache.update_sampling_rules([sampling_rule_1, sampling_rule_2]) # quota should be 1 because of borrowing=true until targets are updated rule_applier_0 = rule_cache._RuleCache__rule_appliers[0] self.assertEqual( rule_applier_0._SamplingRuleApplier__reservoir_sampler._RateLimitingSampler__reservoir._quota, 1, ) self.assertEqual( rule_applier_0._SamplingRuleApplier__fixed_rate_sampler._rate, sampling_rule_2.FixedRate, ) rule_applier_1 = rule_cache._RuleCache__rule_appliers[1] self.assertEqual( rule_applier_1._SamplingRuleApplier__reservoir_sampler._RateLimitingSampler__reservoir._quota, 1, ) self.assertEqual( rule_applier_1._SamplingRuleApplier__fixed_rate_sampler._rate, sampling_rule_1.FixedRate, ) target_1 = { "FixedRate": 0.05, "Interval": 15, "ReservoirQuota": 1, "ReservoirQuotaTTL": mock_clock.now().timestamp() + 10, "RuleName": "default", } target_2 = { "FixedRate": 0.15, "Interval": 12, "ReservoirQuota": 5, "ReservoirQuotaTTL": mock_clock.now().timestamp() + 10, "RuleName": "test", } target_3 = { "FixedRate": 0.15, "Interval": 3, "ReservoirQuota": 5, "ReservoirQuotaTTL": mock_clock.now().timestamp() + 10, "RuleName": "associated rule does not exist", } target_response = _SamplingTargetResponse( mock_clock.now().timestamp() - 10, [target_1, target_2, target_3], [], ) refresh_rules, min_polling_interval = rule_cache.update_sampling_targets(target_response) self.assertFalse(refresh_rules) # target_3 Interval is ignored since it's not associated with a Rule Applier self.assertEqual(min_polling_interval, target_2["Interval"]) # still only 2 rule appliers should exist if for some reason 3 targets are obtained self.assertEqual(len(rule_cache._RuleCache__rule_appliers), 2) # borrowing=false, use quota from targets rule_applier_0 = rule_cache._RuleCache__rule_appliers[0] self.assertEqual( rule_applier_0._SamplingRuleApplier__reservoir_sampler._RateLimitingSampler__reservoir._quota, target_2["ReservoirQuota"], ) self.assertEqual( rule_applier_0._SamplingRuleApplier__fixed_rate_sampler._rate, target_2["FixedRate"], ) rule_applier_1 = rule_cache._RuleCache__rule_appliers[1] self.assertEqual( rule_applier_1._SamplingRuleApplier__reservoir_sampler._RateLimitingSampler__reservoir._quota, target_1["ReservoirQuota"], ) self.assertEqual( rule_applier_1._SamplingRuleApplier__fixed_rate_sampler._rate, target_1["FixedRate"], ) # Test target response modified after Rule cache's last modified date target_response.LastRuleModification = mock_clock.now().timestamp() + 1 refresh_rules, _ = rule_cache.update_sampling_targets(target_response) self.assertTrue(refresh_rules) # pylint:disable=C0103 def test_get_all_statistics(self): time_now = datetime.datetime.fromtimestamp(1707551387.0) mock_clock = MockClock(time_now) rule_applier_1 = _SamplingRuleApplier(_SamplingRule(RuleName="test"), CLIENT_ID, mock_clock) rule_applier_2 = _SamplingRuleApplier(_SamplingRule(RuleName="default"), CLIENT_ID, mock_clock) rule_applier_1._SamplingRuleApplier__statistics = _SamplingStatisticsDocument(CLIENT_ID, "test", 4, 2, 2) rule_applier_2._SamplingRuleApplier__statistics = _SamplingStatisticsDocument(CLIENT_ID, "default", 5, 5, 5) rule_cache = _RuleCache(Resource.get_empty(), None, "", mock_clock, Lock()) rule_cache._RuleCache__rule_appliers = [rule_applier_1, rule_applier_2] mock_clock.add_time(10) statistics = rule_cache.get_all_statistics() self.assertEqual( statistics, [ { "ClientID": CLIENT_ID, "RuleName": "test", "Timestamp": mock_clock.now().timestamp(), "RequestCount": 4, "BorrowCount": 2, "SampleCount": 2, }, { "ClientID": CLIENT_ID, "RuleName": "default", "Timestamp": mock_clock.now().timestamp(), "RequestCount": 5, "BorrowCount": 5, "SampleCount": 5, }, ], ) test_sampling_rule.py000066400000000000000000000065051527070072300407210ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase # pylint: disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace.sampler._sampling_rule import ( _SamplingRule, ) class TestSamplingRule(TestCase): def test_sampling_rule_ordering(self): rule1 = _SamplingRule(Priority=1, RuleName="abcdef", Version=1) rule2 = _SamplingRule(Priority=100, RuleName="A", Version=1) rule3 = _SamplingRule(Priority=100, RuleName="Abc", Version=1) rule4 = _SamplingRule(Priority=100, RuleName="ab", Version=1) rule5 = _SamplingRule(Priority=100, RuleName="abc", Version=1) rule6 = _SamplingRule(Priority=200, RuleName="abcdef", Version=1) self.assertTrue(rule1 < rule2 < rule3 < rule4 < rule5 < rule6) def test_sampling_rule_equality(self): sampling_rule = _SamplingRule( Attributes={"abc": "123", "def": "4?6", "ghi": "*89"}, FixedRate=0.11, HTTPMethod="GET", Host="localhost", Priority=20, ReservoirSize=1, ResourceARN="*", RuleARN="arn:aws:xray:us-east-1:999999999999:sampling-rule/test", RuleName="test", ServiceName="myServiceName", ServiceType="AWS::EKS::Container", URLPath="/helloworld", Version=1, ) sampling_rule_attr_unordered = _SamplingRule( Attributes={"ghi": "*89", "abc": "123", "def": "4?6"}, FixedRate=0.11, HTTPMethod="GET", Host="localhost", Priority=20, ReservoirSize=1, ResourceARN="*", RuleARN="arn:aws:xray:us-east-1:999999999999:sampling-rule/test", RuleName="test", ServiceName="myServiceName", ServiceType="AWS::EKS::Container", URLPath="/helloworld", Version=1, ) self.assertTrue(sampling_rule == sampling_rule_attr_unordered) sampling_rule_updated = _SamplingRule( Attributes={"ghi": "*89", "abc": "123", "def": "4?6"}, FixedRate=0.11, HTTPMethod="GET", Host="localhost", Priority=20, ReservoirSize=1, ResourceARN="*", RuleARN="arn:aws:xray:us-east-1:999999999999:sampling-rule/test", RuleName="test", ServiceName="myServiceName", ServiceType="AWS::EKS::Container", URLPath="/helloworld_new", Version=1, ) sampling_rule_updated_2 = _SamplingRule( Attributes={"abc": "128", "def": "4?6", "ghi": "*89"}, FixedRate=0.11, HTTPMethod="GET", Host="localhost", Priority=20, ReservoirSize=1, ResourceARN="*", RuleARN="arn:aws:xray:us-east-1:999999999999:sampling-rule/test", RuleName="test", ServiceName="myServiceName", ServiceType="AWS::EKS::Container", URLPath="/helloworld", Version=1, ) self.assertFalse(sampling_rule == sampling_rule_updated) self.assertFalse(sampling_rule == sampling_rule_updated_2) test_sampling_rule_applier.py000066400000000000000000000466001527070072300424350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import datetime import json import os from unittest import TestCase from unittest.mock import patch # pylint: disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace.sampler._clock import _Clock from opentelemetry.sdk.extension.aws.trace.sampler._rate_limiting_sampler import ( _RateLimitingSampler, ) from opentelemetry.sdk.extension.aws.trace.sampler._sampling_rule import ( _SamplingRule, ) from opentelemetry.sdk.extension.aws.trace.sampler._sampling_rule_applier import ( _SamplingRuleApplier, ) from opentelemetry.sdk.extension.aws.trace.sampler._sampling_target import ( _SamplingTarget, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace.sampling import ( Decision, SamplingResult, TraceIdRatioBased, ) from opentelemetry.semconv.resource import ResourceAttributes from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.util.types import Attributes from ._mock_clock import MockClock TEST_DIR = os.path.dirname(os.path.realpath(__file__)) DATA_DIR = os.path.join(TEST_DIR, "data") CLIENT_ID = "12345678901234567890abcd" # pylint: disable=no-member class TestSamplingRuleApplier(TestCase): def test_applier_attribute_matching_from_xray_response(self): default_rule = None with open( f"{DATA_DIR}/get-sampling-rules-response-sample-2.json", encoding="UTF-8", ) as file: sample_response = json.load(file) print(sample_response) all_rules = sample_response["SamplingRuleRecords"] default_rule = _SamplingRule(**all_rules[0]["SamplingRule"]) file.close() res = Resource.create( attributes={ ResourceAttributes.SERVICE_NAME: "test_service_name", ResourceAttributes.CLOUD_PLATFORM: "test_cloud_platform", } ) attr: Attributes = { SpanAttributes.URL_PATH: "target", SpanAttributes.HTTP_REQUEST_METHOD: "method", SpanAttributes.URL_FULL: "url", SpanAttributes.SERVER_ADDRESS: "host", "foo": "bar", "abc": "1234", } rule_applier = _SamplingRuleApplier(default_rule, CLIENT_ID, _Clock()) self.assertTrue(rule_applier.matches(res, attr)) # Test again using deprecated Span Attributes attr: Attributes = { SpanAttributes.HTTP_TARGET: "target", SpanAttributes.HTTP_METHOD: "method", SpanAttributes.HTTP_URL: "url", SpanAttributes.HTTP_HOST: "host", "foo": "bar", "abc": "1234", } self.assertTrue(rule_applier.matches(res, attr)) def test_applier_matches_with_all_attributes(self): sampling_rule = _SamplingRule( Attributes={"abc": "123", "def": "4?6", "ghi": "*89"}, FixedRate=0.11, HTTPMethod="GET", Host="localhost", Priority=20, ReservoirSize=1, # Note that ResourceARN is usually only able to be "*" # See: https://docs.aws.amazon.com/xray/latest/devguide/xray-console-sampling.html#xray-console-sampling-options ResourceARN="arn:aws:lambda:us-west-2:123456789012:function:my-function", RuleARN="arn:aws:xray:us-east-1:999999999999:sampling-rule/test", RuleName="test", ServiceName="myServiceName", ServiceType="AWS::Lambda::Function", URLPath="/helloworld", Version=1, ) attributes: Attributes = { "server.address": "localhost", SpanAttributes.HTTP_REQUEST_METHOD: "GET", SpanAttributes.CLOUD_RESOURCE_ID: "arn:aws:lambda:us-west-2:123456789012:function:my-function", "url.full": "http://127.0.0.1:5000/helloworld", "abc": "123", "def": "456", "ghi": "789", # Test that deprecated attributes are not used in matching when above new attributes are set "http.host": "deprecated and will not be used in matching", SpanAttributes.HTTP_METHOD: "deprecated and will not be used in matching", "faas.id": "deprecated and will not be used in matching", "http.url": "deprecated and will not be used in matching", } resource_attr = { ResourceAttributes.SERVICE_NAME: "myServiceName", ResourceAttributes.CLOUD_PLATFORM: "aws_lambda", # CloudPlatformValues.AWS_LAMBDA.value } resource = Resource.create(attributes=resource_attr) rule_applier = _SamplingRuleApplier(sampling_rule, CLIENT_ID, _Clock()) self.assertTrue(rule_applier.matches(resource, attributes)) # Test using deprecated Span Attributes attributes: Attributes = { "http.host": "localhost", SpanAttributes.HTTP_METHOD: "GET", "faas.id": "arn:aws:lambda:us-west-2:123456789012:function:my-function", "http.url": "http://127.0.0.1:5000/helloworld", "abc": "123", "def": "456", "ghi": "789", } self.assertTrue(rule_applier.matches(resource, attributes)) def test_applier_wild_card_attributes_matches_span_attributes(self): sampling_rule = _SamplingRule( Attributes={ "attr1": "*", "attr2": "*", "attr3": "HelloWorld", "attr4": "Hello*", "attr5": "*World", "attr6": "?ello*", "attr7": "Hell?W*d", "attr8": "*.World", "attr9": "*.World", }, FixedRate=0.11, HTTPMethod="*", Host="*", Priority=20, ReservoirSize=1, ResourceARN="*", RuleARN="arn:aws:xray:us-east-1:999999999999:sampling-rule/test", RuleName="test", ServiceName="*", ServiceType="*", URLPath="*", Version=1, ) attributes: Attributes = { "attr1": "", "attr2": "HelloWorld", "attr3": "HelloWorld", "attr4": "HelloWorld", "attr5": "HelloWorld", "attr6": "HelloWorld", "attr7": "HelloWorld", "attr8": "Hello.World", "attr9": "Bye.World", } rule_applier = _SamplingRuleApplier(sampling_rule, CLIENT_ID, _Clock()) self.assertTrue(rule_applier.matches(Resource.get_empty(), attributes)) def test_applier_wild_card_attributes_matches_http_span_attributes(self): sampling_rule = _SamplingRule( Attributes={}, FixedRate=0.11, HTTPMethod="*", Host="*", Priority=20, ReservoirSize=1, ResourceARN="*", RuleARN="arn:aws:xray:us-east-1:999999999999:sampling-rule/test", RuleName="test", ServiceName="*", ServiceType="*", URLPath="*", Version=1, ) attributes: Attributes = { SpanAttributes.SERVER_ADDRESS: "localhost", SpanAttributes.HTTP_REQUEST_METHOD: "GET", SpanAttributes.URL_FULL: "http://127.0.0.1:5000/helloworld", } rule_applier = _SamplingRuleApplier(sampling_rule, CLIENT_ID, _Clock()) self.assertTrue(rule_applier.matches(Resource.get_empty(), attributes)) # Test using deprecated Span Attributes attributes: Attributes = { SpanAttributes.HTTP_HOST: "localhost", SpanAttributes.HTTP_METHOD: "GET", SpanAttributes.HTTP_URL: "http://127.0.0.1:5000/helloworld", } self.assertTrue(rule_applier.matches(Resource.get_empty(), attributes)) def test_applier_wild_card_attributes_matches_with_empty_attributes(self): sampling_rule = _SamplingRule( Attributes={}, FixedRate=0.11, HTTPMethod="*", Host="*", Priority=20, ReservoirSize=1, ResourceARN="*", RuleARN="arn:aws:xray:us-east-1:999999999999:sampling-rule/test", RuleName="test", ServiceName="*", ServiceType="*", URLPath="*", Version=1, ) attributes: Attributes = {} resource_attr: Resource = { ResourceAttributes.SERVICE_NAME: "myServiceName", ResourceAttributes.CLOUD_PLATFORM: "aws_ec2", } resource = Resource.create(attributes=resource_attr) rule_applier = _SamplingRuleApplier(sampling_rule, CLIENT_ID, _Clock()) self.assertTrue(rule_applier.matches(resource, attributes)) self.assertTrue(rule_applier.matches(resource, None)) self.assertTrue(rule_applier.matches(Resource.get_empty(), attributes)) self.assertTrue(rule_applier.matches(Resource.get_empty(), None)) def test_applier_does_not_match_without_http_target(self): sampling_rule = _SamplingRule( Attributes={}, FixedRate=0.11, HTTPMethod="*", Host="*", Priority=20, ReservoirSize=1, ResourceARN="*", RuleARN="arn:aws:xray:us-east-1:999999999999:sampling-rule/test", RuleName="test", ServiceName="*", ServiceType="*", URLPath="/helloworld", Version=1, ) attributes: Attributes = {} resource_attr: Resource = { ResourceAttributes.SERVICE_NAME: "myServiceName", ResourceAttributes.CLOUD_PLATFORM: "aws_ec2", } resource = Resource.create(attributes=resource_attr) rule_applier = _SamplingRuleApplier(sampling_rule, CLIENT_ID, _Clock()) self.assertFalse(rule_applier.matches(resource, attributes)) def test_applier_matches_with_http_target(self): sampling_rule = _SamplingRule( Attributes={}, FixedRate=0.11, HTTPMethod="*", Host="*", Priority=20, ReservoirSize=1, ResourceARN="*", RuleARN="arn:aws:xray:us-east-1:999999999999:sampling-rule/test", RuleName="test", ServiceName="*", ServiceType="*", URLPath="/hello*", Version=1, ) attributes: Attributes = {SpanAttributes.URL_PATH: "/helloworld"} resource_attr: Resource = { ResourceAttributes.SERVICE_NAME: "myServiceName", ResourceAttributes.CLOUD_PLATFORM: "aws_ec2", } resource = Resource.create(attributes=resource_attr) rule_applier = _SamplingRuleApplier(sampling_rule, CLIENT_ID, _Clock()) self.assertTrue(rule_applier.matches(resource, attributes)) # Test again using deprecated Span Attributes attributes: Attributes = {SpanAttributes.HTTP_TARGET: "/helloworld"} self.assertTrue(rule_applier.matches(resource, attributes)) def test_applier_matches_with_span_attributes(self): sampling_rule = _SamplingRule( Attributes={"abc": "123", "def": "456", "ghi": "789"}, FixedRate=0.11, HTTPMethod="*", Host="*", Priority=20, ReservoirSize=1, ResourceARN="*", RuleARN="arn:aws:xray:us-east-1:999999999999:sampling-rule/test", RuleName="test", ServiceName="*", ServiceType="*", URLPath="*", Version=1, ) attributes: Attributes = { "server.address": "localhost", SpanAttributes.HTTP_REQUEST_METHOD: "GET", "url.full": "http://127.0.0.1:5000/helloworld", "abc": "123", "def": "456", "ghi": "789", } resource_attr: Resource = { ResourceAttributes.SERVICE_NAME: "myServiceName", ResourceAttributes.CLOUD_PLATFORM: "aws_eks", } resource = Resource.create(attributes=resource_attr) rule_applier = _SamplingRuleApplier(sampling_rule, CLIENT_ID, _Clock()) self.assertTrue(rule_applier.matches(resource, attributes)) # Test again using deprecated Span Attributes attributes: Attributes = { "http.host": "localhost", SpanAttributes.HTTP_METHOD: "GET", "http.url": "http://127.0.0.1:5000/helloworld", "abc": "123", "def": "456", "ghi": "789", } self.assertTrue(rule_applier.matches(resource, attributes)) def test_applier_does_not_match_with_less_span_attributes(self): sampling_rule = _SamplingRule( Attributes={"abc": "123", "def": "456", "ghi": "789"}, FixedRate=0.11, HTTPMethod="*", Host="*", Priority=20, ReservoirSize=1, ResourceARN="*", RuleARN="arn:aws:xray:us-east-1:999999999999:sampling-rule/test", RuleName="test", ServiceName="*", ServiceType="*", URLPath="*", Version=1, ) attributes: Attributes = { "http.host": "localhost", SpanAttributes.HTTP_REQUEST_METHOD: "GET", "url.full": "http://127.0.0.1:5000/helloworld", "abc": "123", } resource_attr: Resource = { ResourceAttributes.SERVICE_NAME: "myServiceName", ResourceAttributes.CLOUD_PLATFORM: "aws_eks", } resource = Resource.create(attributes=resource_attr) rule_applier = _SamplingRuleApplier(sampling_rule, CLIENT_ID, _Clock()) self.assertFalse(rule_applier.matches(resource, attributes)) def test_update_sampling_applier(self): sampling_rule = _SamplingRule( Attributes={}, FixedRate=0.11, HTTPMethod="*", Host="*", Priority=20, ReservoirSize=1, ResourceARN="*", RuleARN="arn:aws:xray:us-east-1:999999999999:sampling-rule/test", RuleName="test", ServiceName="*", ServiceType="*", URLPath="*", Version=1, ) time_now = datetime.datetime.fromtimestamp(1707551387.0) mock_clock = MockClock(time_now) rule_applier = _SamplingRuleApplier(sampling_rule, CLIENT_ID, mock_clock) self.assertEqual(rule_applier._SamplingRuleApplier__fixed_rate_sampler._rate, 0.11) self.assertEqual( rule_applier._SamplingRuleApplier__reservoir_sampler._RateLimitingSampler__reservoir._quota, 1, ) self.assertEqual( rule_applier._SamplingRuleApplier__reservoir_expiry, datetime.datetime.max, ) target = _SamplingTarget( FixedRate=1.0, Interval=10, ReservoirQuota=30, ReservoirQuotaTTL=1707764006.0, RuleName="test", ) # Update rule applier rule_applier = rule_applier.with_target(target) time_now = datetime.datetime.fromtimestamp(target.ReservoirQuotaTTL) mock_clock.set_time(time_now) self.assertEqual(rule_applier._SamplingRuleApplier__fixed_rate_sampler._rate, 1.0) self.assertEqual( rule_applier._SamplingRuleApplier__reservoir_sampler._RateLimitingSampler__reservoir._quota, 30, ) self.assertEqual( rule_applier._SamplingRuleApplier__reservoir_expiry, mock_clock.now(), ) @staticmethod def fake_reservoir_do_sample(*args, **kwargs): return SamplingResult( decision=Decision.RECORD_AND_SAMPLE, attributes=None, trace_state=None, ) @staticmethod def fake_ratio_do_sample(*args, **kwargs): return SamplingResult( decision=Decision.RECORD_AND_SAMPLE, attributes=None, trace_state=None, ) @staticmethod def fake_ratio_do_not_sample(*args, **kwargs): return SamplingResult( decision=Decision.RECORD_AND_SAMPLE, attributes=None, trace_state=None, ) @patch.object(TraceIdRatioBased, "should_sample", fake_ratio_do_sample) @patch.object(_RateLimitingSampler, "should_sample", fake_reservoir_do_sample) def test_populate_and_get_then_reset_statistics(self): mock_clock = MockClock() rule_applier = _SamplingRuleApplier( _SamplingRule(RuleName="test", ReservoirSize=10), CLIENT_ID, mock_clock, ) rule_applier.should_sample(None, 0, "name") rule_applier.should_sample(None, 0, "name") rule_applier.should_sample(None, 0, "name") statistics = rule_applier.get_then_reset_statistics() self.assertEqual(statistics["ClientID"], CLIENT_ID) self.assertEqual(statistics["RuleName"], "test") self.assertEqual(statistics["Timestamp"], mock_clock.now().timestamp()) self.assertEqual(statistics["RequestCount"], 3) self.assertEqual(statistics["BorrowCount"], 3) self.assertEqual(statistics["SampleCount"], 3) self.assertEqual(rule_applier._SamplingRuleApplier__statistics.RequestCount, 0) self.assertEqual(rule_applier._SamplingRuleApplier__statistics.BorrowCount, 0) self.assertEqual(rule_applier._SamplingRuleApplier__statistics.SampleCount, 0) def test_should_sample_logic_from_reservoir(self): reservoir_size = 10 time_now = datetime.datetime.fromtimestamp(1707551387.0) mock_clock = MockClock(time_now) rule_applier = _SamplingRuleApplier( _SamplingRule(RuleName="test", ReservoirSize=reservoir_size, FixedRate=0.0), CLIENT_ID, mock_clock, ) mock_clock.add_time(seconds=2.0) sampled_count = 0 for _ in range(reservoir_size + 10): if rule_applier.should_sample(None, 0, "name").decision != Decision.DROP: sampled_count += 1 self.assertEqual(sampled_count, 1) # borrow means only 1 sampled target = _SamplingTarget( FixedRate=0.0, Interval=10, ReservoirQuota=10, ReservoirQuotaTTL=mock_clock.now().timestamp() + 10, RuleName="test", ) rule_applier = rule_applier.with_target(target) # Use only 100% of quota (10 out of 10), even if 2 seconds have passed mock_clock.add_time(seconds=2.0) sampled_count = 0 for _ in range(reservoir_size + 10): if rule_applier.should_sample(None, 0, "name").decision != Decision.DROP: sampled_count += 1 self.assertEqual(sampled_count, reservoir_size) # Use only 50% of quota (5 out of 10) mock_clock.add_time(seconds=0.5) sampled_count = 0 for _ in range(reservoir_size + 10): if rule_applier.should_sample(None, 0, "name").decision != Decision.DROP: sampled_count += 1 self.assertEqual(sampled_count, 5) # Expired at 10s, do not sample mock_clock.add_time(seconds=7.5) sampled_count = 0 for _ in range(reservoir_size + 10): if rule_applier.should_sample(None, 0, "name").decision != Decision.DROP: sampled_count += 1 self.assertEqual(sampled_count, 0) test_sampling_statistics_document.py000066400000000000000000000032651527070072300440420ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import datetime from unittest import TestCase # pylint: disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace.sampler._sampling_statistics_document import ( _SamplingStatisticsDocument, ) from ._mock_clock import MockClock class TestSamplingStatisticsDocument(TestCase): def test_sampling_statistics_document_inputs(self): statistics = _SamplingStatisticsDocument("", "") self.assertEqual(statistics.ClientID, "") self.assertEqual(statistics.RuleName, "") self.assertEqual(statistics.BorrowCount, 0) self.assertEqual(statistics.SampleCount, 0) self.assertEqual(statistics.RequestCount, 0) statistics = _SamplingStatisticsDocument("client_id", "rule_name", 1, 2, 3) self.assertEqual(statistics.ClientID, "client_id") self.assertEqual(statistics.RuleName, "rule_name") self.assertEqual(statistics.RequestCount, 1) self.assertEqual(statistics.BorrowCount, 2) self.assertEqual(statistics.SampleCount, 3) clock = MockClock(datetime.datetime.fromtimestamp(1707551387.0)) snapshot = statistics.snapshot(clock) self.assertEqual(snapshot.get("ClientID"), "client_id") self.assertEqual(snapshot.get("RuleName"), "rule_name") self.assertEqual(snapshot.get("Timestamp"), 1707551387.0) self.assertEqual(snapshot.get("RequestCount"), 1) self.assertEqual(snapshot.get("BorrowCount"), 2) self.assertEqual(snapshot.get("SampleCount"), 3) test_sampling_target.py000066400000000000000000000037351527070072300412420ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/sampler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Includes work from: # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase # pylint: disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace.sampler._sampling_target import ( _SamplingTargetResponse, ) class TestSamplingTarget(TestCase): def test_sampling_target_response_with_none_inputs(self): target_response = _SamplingTargetResponse(None, None, None) self.assertEqual(target_response.LastRuleModification, 0.0) self.assertEqual(target_response.SamplingTargetDocuments, []) self.assertEqual(target_response.UnprocessedStatistics, []) def test_sampling_target_response_with_invalid_inputs(self): target_response = _SamplingTargetResponse(1.0, [{}], [{}]) self.assertEqual(target_response.LastRuleModification, 1.0) self.assertEqual(len(target_response.SamplingTargetDocuments), 1) self.assertEqual(target_response.SamplingTargetDocuments[0].FixedRate, 0) self.assertEqual(target_response.SamplingTargetDocuments[0].Interval, None) self.assertEqual(target_response.SamplingTargetDocuments[0].ReservoirQuota, None) self.assertEqual(target_response.SamplingTargetDocuments[0].ReservoirQuotaTTL, None) self.assertEqual(target_response.SamplingTargetDocuments[0].RuleName, "") self.assertEqual(len(target_response.UnprocessedStatistics), 1) self.assertEqual(target_response.UnprocessedStatistics[0].ErrorCode, "") self.assertEqual(target_response.UnprocessedStatistics[0].Message, "") self.assertEqual(target_response.UnprocessedStatistics[0].RuleName, "") target_response = _SamplingTargetResponse(1.0, [{"foo": "bar"}], [{"dog": "cat"}]) self.assertEqual(len(target_response.SamplingTargetDocuments), 0) self.assertEqual(len(target_response.UnprocessedStatistics), 0) test_aws_xray_ids_generator.py000066400000000000000000000022221527070072300411470ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import datetime import time import unittest # pylint: disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace import AwsXRayIdGenerator from opentelemetry.trace.span import INVALID_TRACE_ID class AwsXRayIdGeneratorTest(unittest.TestCase): def test_ids_are_valid(self): id_generator = AwsXRayIdGenerator() for _ in range(1000): trace_id = id_generator.generate_trace_id() self.assertTrue(trace_id != INVALID_TRACE_ID) span_id = id_generator.generate_span_id() self.assertTrue(span_id != INVALID_TRACE_ID) def test_id_timestamps_are_acceptable_for_xray(self): id_generator = AwsXRayIdGenerator() for _ in range(1000): trace_id = id_generator.generate_trace_id() trace_id_time = trace_id >> 96 current_time = int(time.time()) self.assertLessEqual(trace_id_time, current_time) one_month_ago_time = int((datetime.datetime.now() - datetime.timedelta(30)).timestamp()) self.assertGreater(trace_id_time, one_month_ago_time) opentelemetry-python-contrib-0.66b2/test-constraints.txt000066400000000000000000000002231527070072300236270ustar00rootroot00000000000000# Source install of cassandra-driver requires pkg_resources, if # it's not found the install script is broken and fails to build setuptools<82.0.0 opentelemetry-python-contrib-0.66b2/tests/000077500000000000000000000000001527070072300207075ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/000077500000000000000000000000001527070072300262105ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/000077500000000000000000000000001527070072300273525ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/asyncpg/000077500000000000000000000000001527070072300310165ustar00rootroot00000000000000test_asyncpg_functional.py000066400000000000000000000530371527070072300362460ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/asyncpg# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio import contextlib import os from collections import namedtuple from unittest import mock from unittest.mock import patch import asyncpg from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_STATEMENT, DB_SYSTEM, DB_USER, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, NET_TRANSPORT, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_NAMESPACE, DB_QUERY_TEXT, DB_SYSTEM_NAME, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import StatusCode POSTGRES_HOST = os.getenv("POSTGRESQL_HOST", "localhost") POSTGRES_PORT = int(os.getenv("POSTGRESQL_PORT", "5432")) POSTGRES_DB_NAME = os.getenv("POSTGRESQL_DB_NAME", "opentelemetry-tests") POSTGRES_PASSWORD = os.getenv("POSTGRESQL_PASSWORD", "testpassword") POSTGRES_USER = os.getenv("POSTGRESQL_USER", "testuser") def async_call(coro): loop = asyncio.get_event_loop() return loop.run_until_complete(coro) @contextlib.contextmanager def use_semconv_opt_in(sem_conv_mode): env_patch = mock.patch.dict( "os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode}, ) _OpenTelemetrySemanticConventionStability._initialized = False env_patch.start() try: yield finally: env_patch.stop() _OpenTelemetrySemanticConventionStability._initialized = False class CheckSpanMixin: def check_span(self, span, expected_db_name=POSTGRES_DB_NAME): self.assertEqual(span.attributes[DB_SYSTEM], "postgresql") self.assertEqual(span.attributes[DB_NAME], expected_db_name) self.assertEqual(span.attributes[DB_USER], POSTGRES_USER) self.assertEqual(span.attributes[NET_PEER_NAME], POSTGRES_HOST) self.assertEqual(span.attributes[NET_PEER_PORT], POSTGRES_PORT) class TestFunctionalAsyncPG(TestBase, CheckSpanMixin): def setUp(self): super().setUp() self._tracer = self.tracer_provider.get_tracer(__name__) AsyncPGInstrumentor().instrument(tracer_provider=self.tracer_provider) self._connection = async_call( asyncpg.connect( database=POSTGRES_DB_NAME, user=POSTGRES_USER, password=POSTGRES_PASSWORD, host=POSTGRES_HOST, port=POSTGRES_PORT, ) ) def tearDown(self): AsyncPGInstrumentor().uninstrument() super().tearDown() def test_instrumented_execute_method_without_arguments(self, *_, **__): """Should create a span for execute().""" async_call(self._connection.execute("SELECT 42;")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.check_span(spans[0]) self.assertEqual(spans[0].name, "SELECT") self.assertEqual(spans[0].attributes[DB_STATEMENT], "SELECT 42;") def test_instrumented_execute_method_error(self, *_, **__): """Should create an error span for execute() with the database name as the span name.""" with self.assertRaises(AttributeError): async_call(self._connection.execute("")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertIs(StatusCode.ERROR, spans[0].status.status_code) self.check_span(spans[0]) self.assertEqual(spans[0].name, POSTGRES_DB_NAME) self.assertEqual(spans[0].attributes[DB_STATEMENT], "") def test_instrumented_fetch_method_without_arguments(self, *_, **__): """Should create a span from fetch().""" async_call(self._connection.fetch("SELECT 42;")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.check_span(spans[0]) self.assertEqual(spans[0].name, "SELECT") self.assertEqual(spans[0].attributes[DB_STATEMENT], "SELECT 42;") def test_instrumented_fetch_method_without_arguments_stable_semconv(self, *_, **__): """Should create a span from fetch() using stable semconv attributes.""" with use_semconv_opt_in("database"): AsyncPGInstrumentor().uninstrument() AsyncPGInstrumentor().instrument(tracer_provider=self.tracer_provider) async_call(self._connection.fetch("SELECT 42;")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.assertEqual(spans[0].name, "SELECT") self.assertEqual(spans[0].attributes[DB_SYSTEM_NAME], "postgresql") self.assertEqual(spans[0].attributes[DB_NAMESPACE], POSTGRES_DB_NAME) self.assertEqual(spans[0].attributes[DB_QUERY_TEXT], "SELECT 42;") self.assertEqual(spans[0].attributes[SERVER_ADDRESS], POSTGRES_HOST) self.assertEqual(spans[0].attributes[SERVER_PORT], POSTGRES_PORT) self.assertNotIn(DB_STATEMENT, spans[0].attributes) self.assertNotIn(DB_SYSTEM, spans[0].attributes) self.assertNotIn(DB_NAME, spans[0].attributes) def test_instrumented_fetch_method_empty_query(self, *_, **__): """Should create an error span for fetch() with the database name as the span name.""" async_call(self._connection.fetch("")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.check_span(spans[0]) self.assertEqual(spans[0].name, POSTGRES_DB_NAME) self.assertEqual(spans[0].attributes[DB_STATEMENT], "") def test_instrumented_fetchval_method_without_arguments(self, *_, **__): """Should create a span for fetchval().""" async_call(self._connection.fetchval("SELECT 42;")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.check_span(spans[0]) self.assertEqual(spans[0].name, "SELECT") self.assertEqual(spans[0].attributes[DB_STATEMENT], "SELECT 42;") def test_instrumented_fetchval_method_empty_query(self, *_, **__): """Should create an error span for fetchval() with the database name as the span name.""" async_call(self._connection.fetchval("")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.check_span(spans[0]) self.assertEqual(spans[0].name, POSTGRES_DB_NAME) self.assertEqual(spans[0].attributes[DB_STATEMENT], "") def test_instrumented_fetchrow_method_without_arguments(self, *_, **__): """Should create a span for fetchrow().""" async_call(self._connection.fetchrow("SELECT 42;")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.check_span(spans[0]) self.assertEqual(spans[0].name, "SELECT") self.assertEqual(spans[0].attributes[DB_STATEMENT], "SELECT 42;") def test_instrumented_fetchrow_method_empty_query(self, *_, **__): """Should create an error span for fetchrow() with the database name as the span name.""" async_call(self._connection.fetchrow("")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.check_span(spans[0]) self.assertEqual(spans[0].name, POSTGRES_DB_NAME) self.assertEqual(spans[0].attributes[DB_STATEMENT], "") def test_instrumented_cursor_execute_method_without_arguments(self, *_, **__): """Should create spans for the transaction as well as the cursor fetches.""" async def _cursor_execute(): async with self._connection.transaction(): async for record in self._connection.cursor("SELECT generate_series(0, 5);"): pass async_call(_cursor_execute()) spans = self.memory_exporter.get_finished_spans() self.check_span(spans[0]) self.assertEqual(spans[0].name, "BEGIN;") self.assertEqual(spans[0].attributes[DB_STATEMENT], "BEGIN;") self.assertIs(StatusCode.UNSET, spans[0].status.status_code) for span in spans[1:-1]: self.check_span(span) self.assertEqual(span.name, "CURSOR: SELECT") self.assertEqual( span.attributes[DB_STATEMENT], "SELECT generate_series(0, 5);", ) self.assertIs(StatusCode.UNSET, span.status.status_code) self.check_span(spans[-1]) self.assertEqual(spans[-1].name, "COMMIT;") self.assertEqual(spans[-1].attributes[DB_STATEMENT], "COMMIT;") def test_instrumented_cursor_execute_method_empty_query(self, *_, **__): """Should create spans for the transaction and cursor fetches with the database name as the span name.""" async def _cursor_execute(): async with self._connection.transaction(): async for record in self._connection.cursor(""): pass async_call(_cursor_execute()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) self.check_span(spans[0]) self.assertEqual(spans[0].attributes[DB_STATEMENT], "BEGIN;") self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.check_span(spans[1]) self.assertEqual(spans[1].name, f"CURSOR: {POSTGRES_DB_NAME}") self.assertEqual(spans[1].attributes[DB_STATEMENT], "") self.assertIs(StatusCode.UNSET, spans[1].status.status_code) self.check_span(spans[2]) self.assertEqual(spans[2].attributes[DB_STATEMENT], "COMMIT;") def test_instrumented_remove_comments(self, *_, **__): """Should remove comments from the query and set the span name correctly.""" async_call(self._connection.fetch("/* leading comment */ SELECT 42;")) async_call(self._connection.fetch("/* leading comment */ SELECT 42; /* trailing comment */")) async_call(self._connection.fetch("SELECT 42; /* trailing comment */")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) self.check_span(spans[0]) self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.assertEqual(spans[0].name, "SELECT") self.assertEqual( spans[0].attributes[DB_STATEMENT], "/* leading comment */ SELECT 42;", ) self.check_span(spans[1]) self.assertIs(StatusCode.UNSET, spans[1].status.status_code) self.assertEqual(spans[1].name, "SELECT") self.assertEqual( spans[1].attributes[DB_STATEMENT], "/* leading comment */ SELECT 42; /* trailing comment */", ) self.check_span(spans[2]) self.assertIs(StatusCode.UNSET, spans[2].status.status_code) self.assertEqual(spans[2].name, "SELECT") self.assertEqual( spans[2].attributes[DB_STATEMENT], "SELECT 42; /* trailing comment */", ) def test_instrumented_transaction_method(self, *_, **__): """Should create spans for the transaction and the inner execute().""" async def _transaction_execute(): async with self._connection.transaction(): await self._connection.execute("SELECT 42;") async_call(_transaction_execute()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(3, len(spans)) self.check_span(spans[0]) self.assertEqual(spans[0].attributes[DB_STATEMENT], "BEGIN;") self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.check_span(spans[1]) self.assertEqual(spans[1].attributes[DB_STATEMENT], "SELECT 42;") self.assertIs(StatusCode.UNSET, spans[1].status.status_code) self.check_span(spans[2]) self.assertEqual(spans[2].attributes[DB_STATEMENT], "COMMIT;") self.assertIs(StatusCode.UNSET, spans[2].status.status_code) def test_instrumented_failed_transaction_method(self, *_, **__): """Should create spans for the transaction as well as an error span for execute().""" async def _transaction_execute(): async with self._connection.transaction(): await self._connection.execute("SELECT 42::uuid;") with self.assertRaises(asyncpg.CannotCoerceError): async_call(_transaction_execute()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(3, len(spans)) self.check_span(spans[0]) self.assertEqual(spans[0].attributes[DB_STATEMENT], "BEGIN;") self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.check_span(spans[1]) self.assertEqual( spans[1].attributes[DB_STATEMENT], "SELECT 42::uuid;", ) self.assertEqual(StatusCode.ERROR, spans[1].status.status_code) self.check_span(spans[2]) self.assertEqual(spans[2].attributes[DB_STATEMENT], "ROLLBACK;") self.assertIs(StatusCode.UNSET, spans[2].status.status_code) def test_instrumented_method_doesnt_capture_parameters(self, *_, **__): """Should not capture parameters when capture_parameters is False.""" async_call(self._connection.execute("SELECT $1;", "1")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.check_span(spans[0]) self.assertEqual(spans[0].attributes[DB_STATEMENT], "SELECT $1;") class TestFunctionalAsyncPG_CaptureParameters(TestBase, CheckSpanMixin): def setUp(self): super().setUp() self._tracer = self.tracer_provider.get_tracer(__name__) AsyncPGInstrumentor(capture_parameters=True).instrument(tracer_provider=self.tracer_provider) self._connection = async_call( asyncpg.connect( database=POSTGRES_DB_NAME, user=POSTGRES_USER, password=POSTGRES_PASSWORD, host=POSTGRES_HOST, port=POSTGRES_PORT, ) ) def tearDown(self): AsyncPGInstrumentor().uninstrument() super().tearDown() def test_instrumented_execute_method_with_arguments(self, *_, **__): """Should create a span for execute() with captured parameters.""" async_call(self._connection.execute("SELECT $1;", "1")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.check_span(spans[0]) self.assertEqual(spans[0].name, "SELECT") self.assertEqual(spans[0].attributes[DB_STATEMENT], "SELECT $1;") self.assertEqual(spans[0].attributes["db.statement.parameters"], "('1',)") def test_instrumented_fetch_method_with_arguments(self, *_, **__): """Should create a span for fetch() with captured parameters.""" async_call(self._connection.fetch("SELECT $1;", "1")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.check_span(spans[0]) self.assertEqual(spans[0].name, "SELECT") self.assertEqual(spans[0].attributes[DB_STATEMENT], "SELECT $1;") self.assertEqual(spans[0].attributes["db.statement.parameters"], "('1',)") def test_instrumented_executemany_method_with_arguments(self, *_, **__): """Should create a span for executemany with captured parameters.""" async_call(self._connection.executemany("SELECT $1;", [["1"], ["2"]])) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.check_span(spans[0]) self.assertEqual(spans[0].attributes[DB_STATEMENT], "SELECT $1;") self.assertEqual(spans[0].attributes["db.statement.parameters"], "([['1'], ['2']],)") def test_instrumented_execute_interface_error_method(self, *_, **__): """Should create an error span for execute() with captured parameters.""" with self.assertRaises(asyncpg.InterfaceError): async_call(self._connection.execute("SELECT 42;", 1, 2, 3)) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertIs(StatusCode.ERROR, spans[0].status.status_code) self.check_span(spans[0]) self.assertEqual(spans[0].attributes[DB_STATEMENT], "SELECT 42;") self.assertEqual(spans[0].attributes["db.statement.parameters"], "(1, 2, 3)") def test_instrumented_executemany_method_empty_query(self, *_, **__): """Should create a span for executemany() with captured parameters.""" async_call(self._connection.executemany("", [])) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.check_span(spans[0]) self.assertEqual(spans[0].name, POSTGRES_DB_NAME) self.assertEqual(spans[0].attributes[DB_STATEMENT], "") self.assertEqual(spans[0].attributes["db.statement.parameters"], "([],)") def test_instrumented_fetch_method_broken_asyncpg(self, *_, **__): """Should create a span for fetch() with "postgresql" as the span name.""" with patch.object(self._connection, "_params", namedtuple("ConnectionParams", [])): async_call(self._connection.fetch("")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertIs(StatusCode.UNSET, spans[0].status.status_code) self.assertEqual(spans[0].name, "postgresql") self.assertEqual(spans[0].attributes[DB_STATEMENT], "") class _FakeParams: def __init__(self, database="testdb", user="dbuser"): self.database = database self.user = user class _FakeConnection: def __init__(self): self._params = _FakeParams() self._addr = ("db.example.com", 5432) class _FakeCursor: def __init__(self, connection, query, args): self._connection = connection self._query = query self._args = args class TestAsyncPGSamplingAttributes(TestBase): def setUp(self): super().setUp() self.connection = _FakeConnection() self.exporter = InMemorySpanExporter() tracer_provider = TracerProvider() tracer_provider.add_span_processor(SimpleSpanProcessor(self.exporter)) self.tracer = tracer_provider.get_tracer(__name__) self.instrumentor = AsyncPGInstrumentor(capture_parameters=True) self.instrumentor._tracer = self.tracer def test_attributes_available_when_span_not_recording(self): async def _fake_execute(*args, **kwargs): return "ok" async_call( self.instrumentor._do_execute( _fake_execute, self.connection, ("SELECT $1", "42"), {}, ) ) spans = self.exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual( span.attributes, { DB_SYSTEM: "postgresql", DB_NAME: "testdb", DB_USER: "dbuser", NET_PEER_NAME: "db.example.com", NET_PEER_PORT: 5432, NET_TRANSPORT: "ip_tcp", DB_STATEMENT: "SELECT $1", "db.statement.parameters": "('42',)", }, ) self.assertEqual(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "SELECT") def test_cursor_attributes_available_when_span_not_recording(self): async def _fake_cursor_execute(*args, **kwargs): return "ok" cursor = _FakeCursor(self.connection, "SELECT $1", ("99",)) async_call( self.instrumentor._do_cursor_execute( _fake_cursor_execute, cursor, (), {}, ) ) spans = self.exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual( span.attributes, { DB_SYSTEM: "postgresql", DB_NAME: "testdb", DB_USER: "dbuser", NET_PEER_NAME: "db.example.com", NET_PEER_PORT: 5432, NET_TRANSPORT: "ip_tcp", DB_STATEMENT: "SELECT $1", "db.statement.parameters": "('99',)", }, ) self.assertEqual(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "CURSOR: SELECT") opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/celery/000077500000000000000000000000001527070072300306355ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/celery/conftest.py000066400000000000000000000042051527070072300330350ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os from functools import wraps import pytest from opentelemetry import trace as trace_api from opentelemetry.instrumentation.celery import CeleryInstrumentor from opentelemetry.sdk.trace import TracerProvider, export from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) REDIS_HOST = os.getenv("REDIS_HOST", "localhost") REDIS_PORT = int(os.getenv("REDIS_PORT ", "6379")) REDIS_URL = f"redis://{REDIS_HOST}:{REDIS_PORT}" BROKER_URL = f"{REDIS_URL}/0" BACKEND_URL = f"{REDIS_URL}/1" @pytest.fixture(scope="session") def celery_config(): return {"broker_url": BROKER_URL, "result_backend": BACKEND_URL} @pytest.fixture def celery_worker_parameters(): return { # See https://github.com/celery/celery/issues/3642#issuecomment-457773294 "perform_ping_check": False, } @pytest.fixture(autouse=True) def patch_celery_app(celery_app, celery_worker): """Patch task decorator on app fixture to reload worker""" # See https://github.com/celery/celery/issues/3642 def wrap_task(fn): @wraps(fn) def wrapper(*args, **kwargs): result = fn(*args, **kwargs) celery_worker.reload() return result return wrapper celery_app.task = wrap_task(celery_app.task) @pytest.fixture(autouse=True) def instrument(tracer_provider, memory_exporter): CeleryInstrumentor().instrument(tracer_provider=tracer_provider) memory_exporter.clear() yield CeleryInstrumentor().uninstrument() @pytest.fixture(scope="function") def tracer_provider(memory_exporter): original_tracer_provider = trace_api.get_tracer_provider() tracer_provider = TracerProvider() span_processor = export.SimpleSpanProcessor(memory_exporter) tracer_provider.add_span_processor(span_processor) trace_api.set_tracer_provider(tracer_provider) yield tracer_provider trace_api.set_tracer_provider(original_tracer_provider) @pytest.fixture(scope="function") def memory_exporter(): memory_exporter = InMemorySpanExporter() return memory_exporter test_celery_functional.py000066400000000000000000000415701527070072300357030ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/celery# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import celery from celery.exceptions import Retry from pytest import mark import opentelemetry.instrumentation.celery from opentelemetry import trace as trace_api from opentelemetry.instrumentation.celery import CeleryInstrumentor from opentelemetry.sdk import resources from opentelemetry.sdk.trace import TracerProvider, export from opentelemetry.semconv._incubating.attributes.exception_attributes import ( EXCEPTION_MESSAGE, EXCEPTION_TYPE, ) from opentelemetry.semconv._incubating.attributes.messaging_attributes import ( MESSAGING_MESSAGE_ID, ) from opentelemetry.trace import StatusCode # set a high timeout for async executions due to issues in CI ASYNC_GET_TIMEOUT = 120 class MyException(Exception): pass @mark.skip(reason="inconsistent test results") def test_instrumentation_info(celery_app, memory_exporter): @celery_app.task def fn_task(): return 42 result = fn_task.apply_async() assert result.get(timeout=ASYNC_GET_TIMEOUT) == 42 spans = memory_exporter.get_finished_spans() assert len(spans) == 2 async_span, run_span = spans assert run_span.parent == async_span.context assert run_span.parent.span_id == async_span.context.span_id assert run_span.context.trace_id == async_span.context.trace_id assert async_span.instrumentation_info.name == f"apply_async/{opentelemetry.instrumentation.celery.__name__}" assert async_span.instrumentation_info.version == f"apply_async/{opentelemetry.instrumentation.celery.__version__}" assert run_span.instrumentation_info.name == f"run/{opentelemetry.instrumentation.celery.__name__}" assert run_span.instrumentation_info.version == f"run/{opentelemetry.instrumentation.celery.__version__}" def test_fn_task_run(celery_app, memory_exporter): @celery_app.task def fn_task(): return 42 t = fn_task.run() assert t == 42 spans = memory_exporter.get_finished_spans() assert len(spans) == 0 def test_fn_task_call(celery_app, memory_exporter): @celery_app.task def fn_task(): return 42 t = fn_task() assert t == 42 spans = memory_exporter.get_finished_spans() assert len(spans) == 0 def test_fn_task_apply(celery_app, memory_exporter): @celery_app.task def fn_task(): return 42 t = fn_task.apply() assert t.successful() is True assert t.result == 42 spans = memory_exporter.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.status.is_ok is True assert span.name == "run/test_celery_functional.fn_task" assert span.attributes.get(MESSAGING_MESSAGE_ID) == t.task_id assert span.attributes.get("celery.task_name") == "test_celery_functional.fn_task" assert span.attributes.get("celery.action") == "run" assert span.attributes.get("celery.state") == "SUCCESS" def test_fn_task_apply_bind(celery_app, memory_exporter): @celery_app.task(bind=True) def fn_task(self): return self t = fn_task.apply() assert t.successful() is True assert "fn_task" in t.result.name spans = memory_exporter.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.status.is_ok is True assert span.name == "run/test_celery_functional.fn_task" assert span.attributes.get(MESSAGING_MESSAGE_ID) == t.task_id assert span.attributes.get("celery.task_name") == "test_celery_functional.fn_task" assert span.attributes.get("celery.action") == "run" assert span.attributes.get("celery.state") == "SUCCESS" @mark.skip(reason="inconsistent test results") def test_fn_task_apply_async(celery_app, memory_exporter): @celery_app.task def fn_task_parameters(user, force_logout=False): return (user, force_logout) result = fn_task_parameters.apply_async(args=["user"], kwargs={"force_logout": True}) assert result.get(timeout=ASYNC_GET_TIMEOUT) == ["user", True] spans = memory_exporter.get_finished_spans() assert len(spans) == 2 async_span, run_span = spans assert run_span.context.trace_id != async_span.context.trace_id assert async_span.status.is_ok is True assert async_span.name == "apply_async/test_celery_functional.fn_task_parameters" assert async_span.attributes.get("celery.action") == "apply_async" assert async_span.attributes.get(MESSAGING_MESSAGE_ID) == result.task_id assert async_span.attributes.get("celery.task_name") == "test_celery_functional.fn_task_parameters" assert run_span.status.is_ok is True assert run_span.name == "test_celery_functional.fn_task_parameters" assert run_span.attributes.get("celery.action") == "run" assert run_span.attributes.get("celery.state") == "SUCCESS" assert run_span.attributes.get(MESSAGING_MESSAGE_ID) == result.task_id assert run_span.attributes.get("celery.task_name") == "test_celery_functional.fn_task_parameters" @mark.skip(reason="inconsistent test results") def test_concurrent_delays(celery_app, memory_exporter): @celery_app.task def fn_task(): return 42 results = [fn_task.delay() for _ in range(100)] for result in results: assert result.get(timeout=ASYNC_GET_TIMEOUT) == 42 spans = memory_exporter.get_finished_spans() assert len(spans) == 200 @mark.skip(reason="inconsistent test results") def test_fn_task_delay(celery_app, memory_exporter): @celery_app.task def fn_task_parameters(user, force_logout=False): return (user, force_logout) result = fn_task_parameters.delay("user", force_logout=True) assert result.get(timeout=ASYNC_GET_TIMEOUT) == ["user", True] spans = memory_exporter.get_finished_spans() assert len(spans) == 2 async_span, run_span = spans assert run_span.context.trace_id != async_span.context.trace_id assert async_span.status.is_ok is True assert async_span.name == "apply_async/test_celery_functional.fn_task_parameters" assert async_span.attributes.get("celery.action") == "apply_async" assert async_span.attributes.get(MESSAGING_MESSAGE_ID) == result.task_id assert async_span.attributes.get("celery.task_name") == "test_celery_functional.fn_task_parameters" assert run_span.status.is_ok is True assert run_span.name == "run/test_celery_functional.fn_task_parameters" assert run_span.attributes.get("celery.action") == "run" assert run_span.attributes.get("celery.state") == "SUCCESS" assert run_span.attributes.get(MESSAGING_MESSAGE_ID) == result.task_id assert run_span.attributes.get("celery.task_name") == "test_celery_functional.fn_task_parameters" assert len(run_span.events) == 0 def test_fn_exception(celery_app, memory_exporter): @celery_app.task def fn_exception(): raise Exception("Task class is failing") result = fn_exception.apply() assert result.failed() is True assert "Task class is failing" in result.traceback spans = memory_exporter.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.status.is_ok is False assert span.name == "run/test_celery_functional.fn_exception" assert span.attributes.get("celery.action") == "run" assert span.attributes.get("celery.state") == "FAILURE" assert span.attributes.get("celery.task_name") == "test_celery_functional.fn_exception" assert span.status.status_code == StatusCode.ERROR assert len(span.events) == 1 event = span.events[0] assert event.name == "exception" assert event.attributes[EXCEPTION_TYPE] == "Exception" assert EXCEPTION_MESSAGE in event.attributes assert span.attributes.get(MESSAGING_MESSAGE_ID) == result.task_id assert "Task class is failing" in span.status.description def test_fn_exception_expected(celery_app, memory_exporter): @celery_app.task(throws=(MyException,)) def fn_exception(): raise MyException("Task class is failing") result = fn_exception.apply() assert result.failed() is True assert "Task class is failing" in result.traceback spans = memory_exporter.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.status.is_ok is False assert span.status.status_code == StatusCode.ERROR assert span.name == "run/test_celery_functional.fn_exception" assert span.attributes.get("celery.action") == "run" assert span.attributes.get("celery.state") == "FAILURE" assert span.attributes.get("celery.task_name") == "test_celery_functional.fn_exception" assert span.attributes.get(MESSAGING_MESSAGE_ID) == result.task_id def test_fn_retry_exception(celery_app, memory_exporter): @celery_app.task def fn_exception(): raise Retry("Task class is being retried") result = fn_exception.apply() assert result.failed() is False assert "Task class is being retried" in result.traceback spans = memory_exporter.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.status.is_ok is True assert span.status.status_code == StatusCode.UNSET assert span.name == "run/test_celery_functional.fn_exception" assert span.attributes.get("celery.action") == "run" assert span.attributes.get("celery.state") == "RETRY" assert span.attributes.get("celery.task_name") == "test_celery_functional.fn_exception" assert span.attributes.get(MESSAGING_MESSAGE_ID) == result.task_id def test_class_task(celery_app, memory_exporter): class BaseTask(celery_app.Task): def run(self): return 42 task = BaseTask() # register the Task class if it's available (required in Celery 4.0+) register_task = getattr(celery_app, "register_task", None) if register_task is not None: register_task(task) result = task.apply() assert result.successful() is True assert result.result == 42 spans = memory_exporter.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.status.is_ok is True assert span.name == "run/test_celery_functional.BaseTask" assert span.attributes.get("celery.task_name") == "test_celery_functional.BaseTask" assert span.attributes.get("celery.action") == "run" assert span.attributes.get("celery.state") == "SUCCESS" assert span.attributes.get(MESSAGING_MESSAGE_ID) == result.task_id def test_class_task_exception(celery_app, memory_exporter): class BaseTask(celery_app.Task): def run(self): raise Exception("Task class is failing") task = BaseTask() # register the Task class if it's available (required in Celery 4.0+) register_task = getattr(celery_app, "register_task", None) if register_task is not None: register_task(task) result = task.apply() assert result.failed() is True assert "Task class is failing" in result.traceback spans = memory_exporter.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.status.is_ok is False assert span.name == "run/test_celery_functional.BaseTask" assert span.attributes.get("celery.task_name") == "test_celery_functional.BaseTask" assert span.attributes.get("celery.action") == "run" assert span.attributes.get("celery.state") == "FAILURE" assert span.status.status_code == StatusCode.ERROR assert span.attributes.get(MESSAGING_MESSAGE_ID) == result.task_id assert "Task class is failing" in span.status.description def test_class_task_exception_expected(celery_app, memory_exporter): class BaseTask(celery_app.Task): throws = (MyException,) def run(self): raise MyException("Task class is failing") task = BaseTask() # register the Task class if it's available (required in Celery 4.0+) register_task = getattr(celery_app, "register_task", None) if register_task is not None: register_task(task) result = task.apply() assert result.failed() is True assert "Task class is failing" in result.traceback spans = memory_exporter.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.status.is_ok is False assert span.status.status_code == StatusCode.ERROR assert span.name == "run/test_celery_functional.BaseTask" assert span.attributes.get("celery.action") == "run" assert span.attributes.get("celery.state") == "FAILURE" assert span.attributes.get(MESSAGING_MESSAGE_ID) == result.task_id def test_shared_task(celery_app, memory_exporter): """Ensure Django Shared Task are supported""" @celery.shared_task def add(x, y): return x + y result = add.apply([2, 2]) assert result.result == 4 spans = memory_exporter.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.status.is_ok is True assert span.name == "run/test_celery_functional.add" assert span.attributes.get("celery.task_name") == "test_celery_functional.add" assert span.attributes.get("celery.action") == "run" assert span.attributes.get("celery.state") == "SUCCESS" assert span.attributes.get(MESSAGING_MESSAGE_ID) == result.task_id @mark.skip(reason="inconsistent test results") def test_apply_async_previous_style_tasks(celery_app, celery_worker, memory_exporter): """Ensures apply_async is properly patched if Celery 1.0 style tasks are used even in newer versions. This should extend support to previous versions of Celery.""" class CelerySuperClass(celery.task.Task): abstract = True @classmethod def apply_async(cls, args=None, kwargs=None, **kwargs_): return super().apply_async(args=args, kwargs=kwargs, **kwargs_) def run(self, *args, **kwargs): if "stop" in kwargs: # avoid call loop return CelerySubClass.apply_async(args=[], kwargs={"stop": True}).get(timeout=ASYNC_GET_TIMEOUT) class CelerySubClass(CelerySuperClass): pass celery_worker.reload() task = CelerySubClass() result = task.apply() spans = memory_exporter.get_finished_spans() assert len(spans) == 3 async_span, async_run_span, run_span = spans assert run_span.status.is_ok is True assert run_span.name == "run/test_celery_functional.CelerySubClass" assert run_span.attributes.get("celery.task_name") == "test_celery_functional.CelerySubClass" assert run_span.attributes.get("celery.action") == "run" assert run_span.attributes.get("celery.state") == "SUCCESS" assert run_span.attributes.get(MESSAGING_MESSAGE_ID) == result.task_id assert async_run_span.status.is_ok is True assert async_run_span.name == "run/test_celery_functional.CelerySubClass" assert async_run_span.attributes.get("celery.task_name") == "test_celery_functional.CelerySubClass" assert async_run_span.attributes.get("celery.action") == "run" assert async_run_span.attributes.get("celery.state") == "SUCCESS" assert async_run_span.attributes.get(MESSAGING_MESSAGE_ID) != result.task_id assert async_span.status.is_ok is True assert async_span.name == "apply_async/test_celery_functional.CelerySubClass" assert async_span.attributes.get("celery.task_name") == "test_celery_functional.CelerySubClass" assert async_span.attributes.get("celery.action") == "apply_async" assert async_span.attributes.get(MESSAGING_MESSAGE_ID) != result.task_id assert async_span.attributes.get(MESSAGING_MESSAGE_ID) == async_run_span.attributes.get(MESSAGING_MESSAGE_ID) def test_custom_tracer_provider(celery_app, memory_exporter): @celery_app.task def fn_task(): return 42 resource = resources.Resource.create({}) tracer_provider = TracerProvider(resource=resource) span_processor = export.SimpleSpanProcessor(memory_exporter) tracer_provider.add_span_processor(span_processor) trace_api.set_tracer_provider(tracer_provider) CeleryInstrumentor().uninstrument() CeleryInstrumentor().instrument(tracer_provider=tracer_provider) fn_task.apply() spans_list = memory_exporter.get_finished_spans() assert len(spans_list) == 1 span = spans_list[0] assert span.resource == resource def test_use_span_links(celery_app, tracer_provider, memory_exporter): @celery_app.task def fn_task(): return 42 CeleryInstrumentor().uninstrument() CeleryInstrumentor().instrument(tracer_provider=tracer_provider, use_span_links=True) result = fn_task.apply_async() assert result.get(timeout=ASYNC_GET_TIMEOUT) == 42 spans = memory_exporter.get_finished_spans() assert len(spans) == 2 run_span = next(s for s in spans if s.attributes.get("celery.action") == "run") async_span = next(s for s in spans if s.attributes.get("celery.action") == "apply_async") # The run span should not be a child of the async span when using links. assert run_span.parent is None assert run_span.context.trace_id != async_span.context.trace_id assert len(run_span.links) == 1 link = run_span.links[0] assert link.context.span_id == async_span.context.span_id assert link.context.trace_id == async_span.context.trace_id opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/check_availability.py000066400000000000000000000104251527070072300335350ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import os import time import kafka import mysql.connector import psycopg2 import pymongo import pyodbc import redis MONGODB_COLLECTION_NAME = "test" MONGODB_DB_NAME = os.getenv("MONGODB_DB_NAME", "opentelemetry-tests") MONGODB_HOST = os.getenv("MONGODB_HOST", "localhost") MONGODB_PORT = int(os.getenv("MONGODB_PORT", "27017")) MYSQL_DB_NAME = os.getenv("MYSQL_DB_NAME", "opentelemetry-tests") MYSQL_HOST = os.getenv("MYSQL_HOST", "localhost") MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306")) MYSQL_USER = os.getenv("MYSQL_USER", "testuser") MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "testpassword") POSTGRES_DB_NAME = os.getenv("POSTGRESQL_DB_NAME", "opentelemetry-tests") POSTGRES_HOST = os.getenv("POSTGRESQL_HOST", "localhost") POSTGRES_PASSWORD = os.getenv("POSTGRESQL_PASSWORD", "testpassword") POSTGRES_PORT = int(os.getenv("POSTGRESQL_PORT", "5432")) POSTGRES_USER = os.getenv("POSTGRESQL_USER", "testuser") REDIS_HOST = os.getenv("REDIS_HOST", "localhost") REDIS_PORT = int(os.getenv("REDIS_PORT ", "6379")) MSSQL_DB_NAME = os.getenv("MSSQL_DB_NAME", "opentelemetry-tests") MSSQL_HOST = os.getenv("MSSQL_HOST", "localhost") MSSQL_PORT = int(os.getenv("MSSQL_PORT", "1433")) MSSQL_USER = os.getenv("MSSQL_USER", "sa") MSSQL_PASSWORD = os.getenv("MSSQL_PASSWORD", "yourStrong(!)Password") KAFKA_HOST = os.getenv("KAFKA_HOST", "localhost") KAFKA_PORT = int(os.getenv("KAFKA_PORT", "9092")) RETRY_COUNT = 5 RETRY_INITIAL_INTERVAL = 2 # Seconds logger = logging.getLogger(__name__) def retryable(func): def wrapper(): # Try to connect to DB for i in range(RETRY_COUNT): try: func() return except Exception as ex: # pylint: disable=broad-except # Exponential backoff backoff_interval = RETRY_INITIAL_INTERVAL * (2**i) logger.error( "waiting for %s, retry %d/%d [%s]", func.__name__, i + 1, RETRY_COUNT, ex, ) time.sleep(backoff_interval) raise Exception(f"waiting for {func.__name__} failed") return wrapper @retryable def check_pymongo_connection(): client = pymongo.MongoClient(MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000) db = client[MONGODB_DB_NAME] collection = db[MONGODB_COLLECTION_NAME] collection.find_one() client.close() @retryable def check_mysql_connection(): connection = mysql.connector.connect( user=MYSQL_USER, password=MYSQL_PASSWORD, host=MYSQL_HOST, port=MYSQL_PORT, database=MYSQL_DB_NAME, ) connection.close() @retryable def check_postgres_connection(): connection = psycopg2.connect( dbname=POSTGRES_DB_NAME, user=POSTGRES_USER, password=POSTGRES_PASSWORD, host=POSTGRES_HOST, port=POSTGRES_PORT, ) connection.close() @retryable def check_redis_connection(): connection = redis.Redis(host=REDIS_HOST, port=REDIS_PORT) connection.hgetall("*") @retryable def check_kafka_connection(): client = kafka.KafkaAdminClient(bootstrap_servers=[f"{KAFKA_HOST}:{KAFKA_PORT}"]) client.close() def new_mssql_connection() -> pyodbc.Connection: connection = pyodbc.connect( f"DRIVER={{ODBC Driver 18 for SQL Server}};SERVER={MSSQL_HOST}," f"{MSSQL_PORT};DATABASE=master;UID={MSSQL_USER};TrustServerCertificate=yes;" f"PWD={MSSQL_PASSWORD};", autocommit=True, ) return connection @retryable def check_mssql_connection(): conn = new_mssql_connection() conn.close() def setup_mssql_db(): conn = new_mssql_connection() cur = conn.cursor() cur.execute(f"CREATE DATABASE [{MSSQL_DB_NAME}]") conn.close() def check_docker_services_availability(): # Check if Docker services accept connections check_pymongo_connection() check_mysql_connection() check_postgres_connection() check_redis_connection() check_kafka_connection() # make accepting EULA for ms sql odbc driver optional if "ODBC Driver 18 for SQL Server" in pyodbc.drivers(): check_mssql_connection() setup_mssql_db() check_docker_services_availability() opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/django/000077500000000000000000000000001527070072300306145ustar00rootroot00000000000000test_django_functional.py000066400000000000000000000116601527070072300356360ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/django# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from sys import modules from django import VERSION, conf from django.http import HttpResponse from django.test.client import Client from django.test.utils import setup_test_environment, teardown_test_environment from opentelemetry import metrics as metrics_api from opentelemetry.instrumentation.django import DjangoInstrumentor from opentelemetry.sdk.metrics import AlwaysOnExemplarFilter from opentelemetry.test.globals_test import ( reset_metrics_globals, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import ( INVALID_SPAN_ID, INVALID_TRACE_ID, ) DJANGO_2_0 = VERSION >= (2, 0) if DJANGO_2_0: from django.urls import re_path else: from django.conf.urls import url as re_path def view_test_route(request): # pylint: disable=unused-argument return HttpResponse("Test response") urlpatterns = [ re_path(r"^test/", view_test_route), ] class TestFunctionalDjango(TestBase): def setUp(self): super().setUp() self.memory_exporter.clear() # This is done because set_meter_provider cannot override the # current meter provider. reset_metrics_globals() ( self.meter_provider, self.memory_metrics_reader, ) = self.create_meter_provider( exemplar_filter=AlwaysOnExemplarFilter(), ) metrics_api.set_meter_provider(self.meter_provider) conf.settings.configure( ROOT_URLCONF=modules[__name__], DATABASES={ "default": {}, }, ) setup_test_environment() self._client = Client() DjangoInstrumentor().instrument( meter_provider=self.meter_provider, ) def tearDown(self): DjangoInstrumentor().uninstrument() teardown_test_environment() conf.settings = conf.LazySettings() super().tearDown() def test_duration_metrics_exemplars(self): """Should generate exemplars with trace and span IDs for Django HTTP requests.""" self._client.get("/test/") self._client.get("/test/") self._client.get("/test/") metrics_data = self.memory_metrics_reader.get_metrics_data() self.assertIsNotNone(metrics_data) self.assertTrue(len(metrics_data.resource_metrics) > 0) duration_metric = None metric_names = [] for resource_metric in metrics_data.resource_metrics: for scope_metric in resource_metric.scope_metrics: for metric in scope_metric.metrics: metric_names.append(metric.name) if metric.name in [ "http.server.request.duration", "http.server.duration", ]: duration_metric = metric break if duration_metric: break if duration_metric: break self.assertGreater( len(metric_names), 0, "No metrics were generated", ) self.assertTrue( any(name in ["http.server.request.duration", "http.server.duration"] for name in metric_names), f"Expected duration metric not found. Available metrics: {metric_names}", ) self.assertIn( "http.server.active_requests", metric_names, f"Expected active_requests metric not found. Available metrics: {metric_names}", ) self.assertIsNotNone(duration_metric) data_points = list(duration_metric.data.data_points) self.assertTrue(len(data_points) > 0) exemplar_count = 0 for data_point in data_points: if hasattr(data_point, "exemplars") and data_point.exemplars: for exemplar in data_point.exemplars: exemplar_count += 1 # Exemplar has required fields and valid span context self.assertIsNotNone(exemplar.value) self.assertIsNotNone(exemplar.time_unix_nano) self.assertIsNotNone(exemplar.span_id) self.assertNotEqual(exemplar.span_id, INVALID_SPAN_ID) self.assertIsNotNone(exemplar.trace_id) self.assertNotEqual(exemplar.trace_id, INVALID_TRACE_ID) # Trace and span ID of exemplar are part of finished spans finished_spans = self.memory_exporter.get_finished_spans() finished_span_ids = [span.context.span_id for span in finished_spans] finished_trace_ids = [span.context.trace_id for span in finished_spans] self.assertIn(exemplar.span_id, finished_span_ids) self.assertIn(exemplar.trace_id, finished_trace_ids) # At least one exemplar was generated self.assertGreater(exemplar_count, 0) opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/docker-compose.yml000066400000000000000000000040361527070072300330120ustar00rootroot00000000000000services: otmongo: ports: - "27017:27017" image: mongo:latest otmysql: ports: - "3306:3306" image: mysql:latest restart: always environment: MYSQL_USER: testuser MYSQL_PASSWORD: testpassword MYSQL_ALLOW_EMPTY_PASSWORD: "yes" MYSQL_DATABASE: opentelemetry-tests otpostgres: image: postgres ports: - "5432:5432" environment: POSTGRES_USER: testuser POSTGRES_PASSWORD: testpassword POSTGRES_DB: opentelemetry-tests otredis: image: redis/redis-stack:7.2.0-v12 ports: - "127.0.0.1:6379:6379" otrediscluster: image: grokzen/redis-cluster:6.2.0 environment: - IP=0.0.0.0 ports: - "127.0.0.1:7000:7000" - "127.0.0.1:7001:7001" - "127.0.0.1:7002:7002" - "127.0.0.1:7003:7003" - "127.0.0.1:7004:7004" - "127.0.0.1:7005:7005" otjaeger: image: jaegertracing/all-in-one:1.8 environment: COLLECTOR_ZIPKIN_HTTP_PORT: "9411" ports: - "5775:5775/udp" - "6831:6831/udp" - "6832:6832/udp" - "5778:5778" - "16686:16686" - "14268:14268" - "9411:9411" otkafka: image: apache/kafka:3.9.1 ports: - "9092:9092" environment: KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: broker,controller KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT KAFKA_CONTROLLER_QUORUM_VOTERS: 1@localhost:9093 KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 otmssql: image: mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04 ports: - "1433:1433" environment: ACCEPT_EULA: "Y" SA_PASSWORD: "yourStrong(!)Password" command: /opt/mssql/bin/sqlservr opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/flask/000077500000000000000000000000001527070072300304525ustar00rootroot00000000000000test_flask_functional.py000066400000000000000000000074651527070072300353420ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/flask# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from flask import Flask from opentelemetry import metrics as metrics_api from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.sdk.metrics import AlwaysOnExemplarFilter from opentelemetry.test.globals_test import ( reset_metrics_globals, ) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import ( INVALID_SPAN_ID, INVALID_TRACE_ID, ) class TestFunctionalFlask(TestBase): def setUp(self): super().setUp() self.memory_exporter.clear() # This is done because set_meter_provider cannot override the # current meter provider. reset_metrics_globals() ( self.meter_provider, self.memory_metrics_reader, ) = self.create_meter_provider( exemplar_filter=AlwaysOnExemplarFilter(), ) metrics_api.set_meter_provider(self.meter_provider) self._app = Flask(__name__) @self._app.route("/test/") def test_route(): return "Test response" self._client = self._app.test_client() FlaskInstrumentor().instrument_app( self._app, meter_provider=self.meter_provider, ) def tearDown(self): FlaskInstrumentor().uninstrument() super().tearDown() def test_duration_metrics_exemplars(self): """Should generate exemplars with trace and span IDs for Flask HTTP requests.""" self._client.get("/test/") self._client.get("/test/") self._client.get("/test/") metrics_data = self.memory_metrics_reader.get_metrics_data() self.assertIsNotNone(metrics_data) self.assertTrue(len(metrics_data.resource_metrics) > 0) duration_metric = None metric_names = [] for resource_metric in metrics_data.resource_metrics: for scope_metric in resource_metric.scope_metrics: for metric in scope_metric.metrics: metric_names.append(metric.name) if metric.name in [ "http.server.request.duration", "http.server.duration", ]: duration_metric = metric break if duration_metric: break if duration_metric: break self.assertIsNotNone(duration_metric) data_points = list(duration_metric.data.data_points) self.assertTrue(len(data_points) > 0) exemplar_count = 0 for data_point in data_points: if hasattr(data_point, "exemplars") and data_point.exemplars: for exemplar in data_point.exemplars: exemplar_count += 1 # Exemplar has required fields and valid span context self.assertIsNotNone(exemplar.value) self.assertIsNotNone(exemplar.time_unix_nano) self.assertIsNotNone(exemplar.span_id) self.assertNotEqual(exemplar.span_id, INVALID_SPAN_ID) self.assertIsNotNone(exemplar.trace_id) self.assertNotEqual(exemplar.trace_id, INVALID_TRACE_ID) # Trace and span ID of exemplar are part of finished spans finished_spans = self.memory_exporter.get_finished_spans() finished_span_ids = [span.context.span_id for span in finished_spans] finished_trace_ids = [span.context.trace_id for span in finished_spans] self.assertIn(exemplar.span_id, finished_span_ids) self.assertIn(exemplar.trace_id, finished_trace_ids) # At least one exemplar was generated self.assertGreater(exemplar_count, 0) opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/kafka/000077500000000000000000000000001527070072300304275ustar00rootroot00000000000000test_kafka_functional.py000066400000000000000000000064511527070072300352660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/kafka# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os from kafka import KafkaAdminClient, KafkaProducer from kafka.admin import NewTopic from kafka.errors import TopicAlreadyExistsError from opentelemetry import trace as trace_api from opentelemetry.instrumentation.kafka import KafkaInstrumentor from opentelemetry.semconv._incubating.attributes import messaging_attributes from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase KAFKA_HOST = os.getenv("KAFKA_HOST", "localhost") KAFKA_PORT = int(os.getenv("KAFKA_PORT", "9092")) KAFKA_BOOTSTRAP_SERVERS = f"{KAFKA_HOST}:{KAFKA_PORT}" KAFKA_TOPIC = "otel-partition-test" KAFKA_PARTITION_COUNT = 8 class TestFunctionalKafka(TestBase): @classmethod def setUpClass(cls): admin = KafkaAdminClient(bootstrap_servers=[KAFKA_BOOTSTRAP_SERVERS]) try: admin.create_topics( [ NewTopic( KAFKA_TOPIC, num_partitions=KAFKA_PARTITION_COUNT, replication_factor=1, ) ] ) except TopicAlreadyExistsError: pass finally: admin.close() def setUp(self): super().setUp() KafkaInstrumentor().instrument(tracer_provider=self.tracer_provider) self.producer = KafkaProducer(bootstrap_servers=[KAFKA_BOOTSTRAP_SERVERS]) def tearDown(self): self.producer.close() KafkaInstrumentor().uninstrument() super().tearDown() def test_send_records_delivered_partition(self): """The partition recorded on the span must match the partition the broker actually delivered the message to, read back from the ``send()`` future rather than recomputed with the partitioner.""" futures = [self.producer.send(KAFKA_TOPIC, b"payload-%d" % index) for index in range(20)] metadatas = [future.get(timeout=30) for future in futures] spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), len(metadatas)) for span, metadata in zip(spans, metadatas): self.assertEqual(span.name, f"{KAFKA_TOPIC} send") self.assertIs(span.kind, trace_api.SpanKind.PRODUCER) self.assertEqual( span.attributes[messaging_attributes.MESSAGING_SYSTEM], "kafka", ) self.assertEqual( span.attributes[SpanAttributes.MESSAGING_DESTINATION], KAFKA_TOPIC, ) partition = span.attributes[SpanAttributes.MESSAGING_KAFKA_PARTITION] self.assertIsInstance(partition, int) self.assertEqual(partition, metadata.partition) def test_send_with_explicit_partition(self): explicit_partition = KAFKA_PARTITION_COUNT - 1 future = self.producer.send(KAFKA_TOPIC, b"payload", partition=explicit_partition) metadata = future.get(timeout=30) self.assertEqual(metadata.partition, explicit_partition) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual( spans[0].attributes[SpanAttributes.MESSAGING_KAFKA_PARTITION], explicit_partition, ) opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/mysql/000077500000000000000000000000001527070072300305175ustar00rootroot00000000000000test_mysql_functional.py000066400000000000000000000110261527070072300354400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/mysql# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import mysql.connector from opentelemetry import trace as trace_api from opentelemetry.instrumentation.mysql import MySQLInstrumentor from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_SYSTEM, DB_USER, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.test.test_base import TestBase MYSQL_USER = os.getenv("MYSQL_USER", "testuser") MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "testpassword") MYSQL_HOST = os.getenv("MYSQL_HOST", "localhost") MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306")) MYSQL_DB_NAME = os.getenv("MYSQL_DB_NAME", "opentelemetry-tests") class TestFunctionalMysql(TestBase): def setUp(self): super().setUp() self._tracer = self.tracer_provider.get_tracer(__name__) MySQLInstrumentor().instrument() self._connection = mysql.connector.connect( user=MYSQL_USER, password=MYSQL_PASSWORD, host=MYSQL_HOST, port=MYSQL_PORT, database=MYSQL_DB_NAME, ) self._cursor = self._connection.cursor() def tearDown(self): self._cursor.close() self._connection.close() MySQLInstrumentor().uninstrument() super().tearDown() def validate_spans(self, span_name): spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) for span in spans: if span.name == "rootSpan": root_span = span else: db_span = span self.assertIsInstance(span.start_time, int) self.assertIsInstance(span.end_time, int) self.assertIsNotNone(root_span) self.assertIsNotNone(db_span) self.assertEqual(root_span.name, "rootSpan") self.assertEqual(db_span.name, span_name) self.assertIsNotNone(db_span.parent) self.assertIs(db_span.parent, root_span.get_span_context()) self.assertIs(db_span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(db_span.attributes[DB_SYSTEM], "mysql") self.assertEqual(db_span.attributes[DB_NAME], MYSQL_DB_NAME) self.assertEqual(db_span.attributes[DB_USER], MYSQL_USER) self.assertEqual(db_span.attributes[NET_PEER_NAME], MYSQL_HOST) self.assertEqual(db_span.attributes[NET_PEER_PORT], MYSQL_PORT) def test_execute(self): """Should create a child span for execute""" stmt = "CREATE TABLE IF NOT EXISTS test (id INT)" with self._tracer.start_as_current_span("rootSpan"): self._cursor.execute(stmt) self.validate_spans("CREATE") def test_execute_with_connection_context_manager(self): """Should create a child span for execute with connection context""" stmt = "CREATE TABLE IF NOT EXISTS test (id INT)" with self._tracer.start_as_current_span("rootSpan"): with self._connection as conn: cursor = conn.cursor() cursor.execute(stmt) self.validate_spans("CREATE") def test_execute_with_cursor_context_manager(self): """Should create a child span for execute with cursor context""" stmt = "CREATE TABLE IF NOT EXISTS test (id INT)" with self._tracer.start_as_current_span("rootSpan"): with self._connection.cursor() as cursor: cursor.execute(stmt) self.validate_spans("CREATE") def test_executemany(self): """Should create a child span for executemany""" stmt = "INSERT INTO test (id) VALUES (%s)" with self._tracer.start_as_current_span("rootSpan"): data = (("1",), ("2",), ("3",)) self._cursor.executemany(stmt, data) self.validate_spans("INSERT") def test_executemany_with_cursor_iteration(self): """Should create a child span for executemany while iterating over the cursor""" stmt = "SELECT * FROM test" with self._tracer.start_as_current_span("rootSpan"): with self._connection.cursor() as cursor: cursor.execute(stmt) for _row in cursor: pass self.validate_spans("SELECT") def test_callproc(self): """Should create a child span for callproc""" with ( self._tracer.start_as_current_span("rootSpan"), self.assertRaises(Exception), ): self._cursor.callproc("test", ()) self.validate_spans("test") test_mysql_sqlcommenter.py000066400000000000000000000062161527070072300360140ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/mysql# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import mysql.connector from opentelemetry.instrumentation.mysql import MySQLInstrumentor from opentelemetry.test.test_base import TestBase MYSQL_USER = os.getenv("MYSQL_USER", "testuser") MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "testpassword") MYSQL_HOST = os.getenv("MYSQL_HOST", "localhost") MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306")) MYSQL_DB_NAME = os.getenv("MYSQL_DB_NAME", "opentelemetry-tests") class TestFunctionalMySqlCommenter(TestBase): def test_commenter_enabled_direct_reference(self): MySQLInstrumentor().instrument(enable_commenter=True) cnx = mysql.connector.connect( user=MYSQL_USER, password=MYSQL_PASSWORD, host=MYSQL_HOST, port=MYSQL_PORT, database=MYSQL_DB_NAME, ) cursor = cnx.cursor() cursor.execute("SELECT 1;") cursor.fetchall() self.assertRegex( cursor.statement, r"SELECT 1 /\*db_driver='mysql\.connector[^']*',dbapi_level='\d\.\d',dbapi_threadsafety=\d,driver_paramstyle='[^']*',mysql_client_version='[^']*',traceparent='[^']*'\*/;", ) self.assertRegex(cursor.statement, r"mysql_client_version='(?!unknown)[^']+") cursor.close() cnx.close() MySQLInstrumentor().uninstrument() def test_commenter_enabled_connection_proxy(self): cnx = mysql.connector.connect( user=MYSQL_USER, password=MYSQL_PASSWORD, host=MYSQL_HOST, port=MYSQL_PORT, database=MYSQL_DB_NAME, ) instrumented_cnx = MySQLInstrumentor().instrument_connection( connection=cnx, enable_commenter=True, ) cursor = instrumented_cnx.cursor() cursor.execute("SELECT 1;") cursor.fetchall() self.assertRegex( cursor.statement, r"SELECT 1 /\*db_driver='mysql\.connector[^']*',dbapi_level='\d\.\d',dbapi_threadsafety=\d,driver_paramstyle='[^']*',mysql_client_version='[^']*',traceparent='[^']*'\*/;", ) self.assertRegex(cursor.statement, r"mysql_client_version='(?!unknown)[^']+") cursor.close() MySQLInstrumentor().uninstrument_connection(instrumented_cnx) cnx.close() def test_commenter_mysql_client_version_fallback_to_unknown(self): """Test that mysql_client_version falls back to 'unknown' with pure Python implementation""" MySQLInstrumentor().instrument(enable_commenter=True) cnx = mysql.connector.connect( user=MYSQL_USER, password=MYSQL_PASSWORD, host=MYSQL_HOST, port=MYSQL_PORT, database=MYSQL_DB_NAME, use_pure=True, # Use pure Python - no _cmysql module ) cursor = cnx.cursor() cursor.execute("SELECT 1;") cursor.fetchall() # With use_pure=True, _cmysql is not available, should fall back to 'unknown' self.assertRegex(cursor.statement, r"mysql_client_version='unknown'") cursor.close() cnx.close() MySQLInstrumentor().uninstrument() opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/postgres/000077500000000000000000000000001527070072300312205ustar00rootroot00000000000000test_aiopg_functional.py000066400000000000000000000217611527070072300361020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/postgres# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio import os import aiopg import psycopg2 import pytest from opentelemetry import trace as trace_api from opentelemetry.instrumentation.aiopg import AiopgInstrumentor from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_SYSTEM, DB_USER, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.test.test_base import TestBase POSTGRES_HOST = os.getenv("POSTGRESQL_HOST", "127.0.0.1") POSTGRES_PORT = int(os.getenv("POSTGRESQL_PORT", "5432")) POSTGRES_DB_NAME = os.getenv("POSTGRESQL_DB_NAME", "opentelemetry-tests") POSTGRES_PASSWORD = os.getenv("POSTGRESQL_PASSWORD", "testpassword") POSTGRES_USER = os.getenv("POSTGRESQL_USER", "testuser") def async_call(coro): loop = asyncio.get_event_loop() return loop.run_until_complete(coro) class TestFunctionalAiopgConnect(TestBase): def setUp(self): super().setUp() self._tracer = self.tracer_provider.get_tracer(__name__) AiopgInstrumentor().instrument(tracer_provider=self.tracer_provider) self._connection = async_call( aiopg.connect( dbname=POSTGRES_DB_NAME, user=POSTGRES_USER, password=POSTGRES_PASSWORD, host=POSTGRES_HOST, port=POSTGRES_PORT, ) ) self._cursor = async_call(self._connection.cursor()) def tearDown(self): self._cursor.close() self._connection.close() AiopgInstrumentor().uninstrument() super().tearDown() def validate_spans(self, span_name): spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) for span in spans: if span.name == "rootSpan": root_span = span else: child_span = span self.assertIsInstance(span.start_time, int) self.assertIsInstance(span.end_time, int) self.assertIsNotNone(root_span) self.assertIsNotNone(child_span) self.assertEqual(root_span.name, "rootSpan") self.assertEqual(child_span.name, span_name) self.assertIsNotNone(child_span.parent) self.assertIs(child_span.parent, root_span.get_span_context()) self.assertIs(child_span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(child_span.attributes[DB_SYSTEM], "postgresql") self.assertEqual(child_span.attributes[DB_NAME], POSTGRES_DB_NAME) self.assertEqual(child_span.attributes[DB_USER], POSTGRES_USER) self.assertEqual(child_span.attributes[NET_PEER_NAME], POSTGRES_HOST) self.assertEqual(child_span.attributes[NET_PEER_PORT], POSTGRES_PORT) def test_execute(self): """Should create a child span for execute method""" stmt = "CREATE TABLE IF NOT EXISTS test (id integer)" with self._tracer.start_as_current_span("rootSpan"): async_call(self._cursor.execute(stmt)) self.validate_spans("CREATE") def test_executemany(self): """Should create a child span for executemany""" stmt = "INSERT INTO test (id) VALUES (%s)" with pytest.raises(psycopg2.ProgrammingError): with self._tracer.start_as_current_span("rootSpan"): data = (("1",), ("2",), ("3",)) async_call(self._cursor.executemany(stmt, data)) self.validate_spans("INSERT") def test_callproc(self): """Should create a child span for callproc""" with ( self._tracer.start_as_current_span("rootSpan"), self.assertRaises(Exception), ): async_call(self._cursor.callproc("test", ())) self.validate_spans("test") class TestFunctionalAiopgInstrumentConnection(TestBase): def setUp(self): super().setUp() async def connect(): connection = await aiopg.connect( dbname=POSTGRES_DB_NAME, user=POSTGRES_USER, password=POSTGRES_PASSWORD, host=POSTGRES_HOST, port=POSTGRES_PORT, ) return AiopgInstrumentor().instrument_connection(connection, tracer_provider=self.tracer_provider) self._connection = async_call(connect()) self._cursor = async_call(self._connection.cursor()) def tearDown(self): self._cursor.close() self._connection.close() super().tearDown() def test_execute(self): """Should emit connection attributes for a manually instrumented connection.""" async_call(self._cursor.execute("SELECT 1")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.name, "SELECT") self.assertEqual(span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(span.attributes[DB_SYSTEM], "postgresql") self.assertEqual(span.attributes[DB_NAME], POSTGRES_DB_NAME) self.assertEqual(span.attributes[DB_USER], POSTGRES_USER) self.assertEqual(span.attributes[NET_PEER_NAME], POSTGRES_HOST) self.assertEqual(span.attributes[NET_PEER_PORT], POSTGRES_PORT) class TestFunctionalAiopgCreatePool(TestBase): def setUp(self): super().setUp() self._tracer = self.tracer_provider.get_tracer(__name__) AiopgInstrumentor().instrument(tracer_provider=self.tracer_provider) self._dsn = ( f"dbname='{POSTGRES_DB_NAME}' user='{POSTGRES_USER}' password='{POSTGRES_PASSWORD}'" f" host='{POSTGRES_HOST}' port='{POSTGRES_PORT}'" ) self._pool = async_call( aiopg.create_pool( dbname=POSTGRES_DB_NAME, user=POSTGRES_USER, password=POSTGRES_PASSWORD, host=POSTGRES_HOST, port=POSTGRES_PORT, ) ) self._connection = async_call(self._pool.acquire()) self._cursor = async_call(self._connection.cursor()) def tearDown(self): self._cursor.close() self._connection.close() self._pool.close() AiopgInstrumentor().uninstrument() super().tearDown() def validate_spans(self, span_name): spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) for span in spans: if span.name == "rootSpan": root_span = span else: child_span = span self.assertIsInstance(span.start_time, int) self.assertIsInstance(span.end_time, int) self.assertIsNotNone(root_span) self.assertIsNotNone(child_span) self.assertEqual(root_span.name, "rootSpan") self.assertEqual(child_span.name, span_name) self.assertIsNotNone(child_span.parent) self.assertIs(child_span.parent, root_span.get_span_context()) self.assertIs(child_span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(child_span.attributes[DB_SYSTEM], "postgresql") self.assertEqual(child_span.attributes[DB_NAME], POSTGRES_DB_NAME) self.assertEqual(child_span.attributes[DB_USER], POSTGRES_USER) self.assertEqual(child_span.attributes[NET_PEER_NAME], POSTGRES_HOST) self.assertEqual(child_span.attributes[NET_PEER_PORT], POSTGRES_PORT) def test_execute(self): """Should create a child span for execute method""" stmt = "CREATE TABLE IF NOT EXISTS test (id integer)" with self._tracer.start_as_current_span("rootSpan"): async_call(self._cursor.execute(stmt)) self.validate_spans("CREATE") def test_executemany(self): """Should create a child span for executemany""" stmt = "INSERT INTO test (id) VALUES (%s)" with pytest.raises(psycopg2.ProgrammingError): with self._tracer.start_as_current_span("rootSpan"): data = (("1",), ("2",), ("3",)) async_call(self._cursor.executemany(stmt, data)) self.validate_spans("INSERT") def test_callproc(self): """Should create a child span for callproc""" with ( self._tracer.start_as_current_span("rootSpan"), self.assertRaises(Exception), ): async_call(self._cursor.callproc("test", ())) self.validate_spans("test") def test_instrumented_pool_with_multiple_acquires(self, *_, **__): async def double_acquire(): pool = await aiopg.create_pool(dsn=self._dsn) async with pool.acquire() as conn: async with conn.cursor() as cursor: query = "SELECT 1" await cursor.execute(query) async with pool.acquire() as conn: async with conn.cursor() as cursor: query = "SELECT 1" await cursor.execute(query) async_call(double_acquire()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) test_psycopg_functional.py000066400000000000000000000142341527070072300364640ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/postgres# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import psycopg2 from psycopg2 import sql from opentelemetry import trace as trace_api from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_STATEMENT, DB_SYSTEM, DB_USER, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.test.test_base import TestBase POSTGRES_HOST = os.getenv("POSTGRESQL_HOST", "localhost") POSTGRES_PORT = int(os.getenv("POSTGRESQL_PORT", "5432")) POSTGRES_DB_NAME = os.getenv("POSTGRESQL_DB_NAME", "opentelemetry-tests") POSTGRES_PASSWORD = os.getenv("POSTGRESQL_PASSWORD", "testpassword") POSTGRES_USER = os.getenv("POSTGRESQL_USER", "testuser") class TestFunctionalPsycopg(TestBase): def setUp(self): super().setUp() self._tracer = self.tracer_provider.get_tracer(__name__) Psycopg2Instrumentor().instrument(tracer_provider=self.tracer_provider) self._connection = psycopg2.connect( dbname=POSTGRES_DB_NAME, user=POSTGRES_USER, password=POSTGRES_PASSWORD, host=POSTGRES_HOST, port=POSTGRES_PORT, ) self._connection.set_session(autocommit=True) self._cursor = self._connection.cursor() def tearDown(self): self._cursor.close() self._connection.close() Psycopg2Instrumentor().uninstrument() super().tearDown() def validate_spans(self, span_name): spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) for span in spans: if span.name == "rootSpan": root_span = span else: child_span = span self.assertIsInstance(span.start_time, int) self.assertIsInstance(span.end_time, int) self.assertIsNotNone(root_span) self.assertIsNotNone(child_span) self.assertEqual(root_span.name, "rootSpan") self.assertEqual(child_span.name, span_name) self.assertIsNotNone(child_span.parent) self.assertIs(child_span.parent, root_span.get_span_context()) self.assertIs(child_span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(child_span.attributes[DB_SYSTEM], "postgresql") self.assertEqual(child_span.attributes[DB_NAME], POSTGRES_DB_NAME) self.assertEqual(child_span.attributes[DB_USER], POSTGRES_USER) self.assertEqual(child_span.attributes[NET_PEER_NAME], POSTGRES_HOST) self.assertEqual(child_span.attributes[NET_PEER_PORT], POSTGRES_PORT) def test_execute(self): """Should create a child span for execute method""" stmt = "CREATE TABLE IF NOT EXISTS test (id integer)" with self._tracer.start_as_current_span("rootSpan"): self._cursor.execute(stmt) self.validate_spans("CREATE") def test_execute_with_connection_context_manager(self): """Should create a child span for execute with connection context""" stmt = "CREATE TABLE IF NOT EXISTS test (id INT)" with self._tracer.start_as_current_span("rootSpan"): with self._connection as conn: cursor = conn.cursor() cursor.execute(stmt) self.validate_spans("CREATE") def test_execute_with_cursor_context_manager(self): """Should create a child span for execute with cursor context""" stmt = "CREATE TABLE IF NOT EXISTS test (id INT)" with self._tracer.start_as_current_span("rootSpan"): with self._connection.cursor() as cursor: cursor.execute(stmt) self.validate_spans("CREATE") self.assertTrue(cursor.closed) def test_executemany(self): """Should create a child span for executemany""" stmt = "INSERT INTO test (id) VALUES (%s)" with self._tracer.start_as_current_span("rootSpan"): data = (("1",), ("2",), ("3",)) self._cursor.executemany(stmt, data) self.validate_spans("INSERT") def test_callproc(self): """Should create a child span for callproc""" with ( self._tracer.start_as_current_span("rootSpan"), self.assertRaises(Exception), ): self._cursor.callproc("test", ()) self.validate_spans("test") def test_register_types(self): psycopg2.extras.register_default_jsonb(conn_or_curs=self._cursor, loads=lambda x: x) def test_composed_queries(self): stmt = "CREATE TABLE IF NOT EXISTS users (id integer, name varchar)" with self._tracer.start_as_current_span("rootSpan"): self._cursor.execute(stmt) self.validate_spans("CREATE") self._cursor.execute( sql.SQL("SELECT FROM {table} where {field}='{value}'").format( table=sql.Identifier("users"), field=sql.Identifier("name"), value=sql.Identifier("abc"), ) ) spans = self.memory_exporter.get_finished_spans() span = spans[2] self.assertEqual(span.name, "SELECT") self.assertEqual( span.attributes[DB_STATEMENT], 'SELECT FROM "users" where "name"=\'"abc"\'', ) def test_commenter_enabled(self): stmt = "CREATE TABLE IF NOT EXISTS users (id integer, name varchar)" with self._tracer.start_as_current_span("rootSpan"): self._cursor.execute(stmt) self.validate_spans("CREATE") Psycopg2Instrumentor().uninstrument() Psycopg2Instrumentor().instrument(enable_commenter=True) self._cursor.execute( sql.SQL("SELECT FROM {table} where {field}='{value}'").format( table=sql.Identifier("users"), field=sql.Identifier("name"), value=sql.Identifier("abc"), ) ) spans = self.memory_exporter.get_finished_spans() span = spans[2] self.assertEqual(span.name, "SELECT") self.assertEqual( span.attributes[DB_STATEMENT], 'SELECT FROM "users" where "name"=\'"abc"\'', ) test_psycopg_sqlcommenter.py000066400000000000000000000061301527070072300370270ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/postgres# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import psycopg import psycopg2 from test_psycopg_functional import ( POSTGRES_DB_NAME, POSTGRES_HOST, POSTGRES_PASSWORD, POSTGRES_PORT, POSTGRES_USER, ) from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor from opentelemetry.test.test_base import TestBase class TestFunctionalPsycopg2(TestBase): def setUp(self): super().setUp() self._tracer = self.tracer_provider.get_tracer(__name__) Psycopg2Instrumentor().instrument(enable_commenter=True) self._connection = psycopg2.connect( dbname=POSTGRES_DB_NAME, user=POSTGRES_USER, password=POSTGRES_PASSWORD, host=POSTGRES_HOST, port=POSTGRES_PORT, ) self._connection.set_session(autocommit=True) self._cursor = self._connection.cursor() def tearDown(self): self._cursor.close() self._connection.close() Psycopg2Instrumentor().uninstrument() super().tearDown() def test_commenter_enabled(self): self._cursor.execute("SELECT 1;") self.assertRegex( self._cursor.query.decode("ascii"), r"SELECT 1 /\*db_driver='psycopg2(.*)',dbapi_level='\d.\d',dbapi_threadsafety=\d,driver_paramstyle=(.*),libpq_version=\d*,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) class TestFunctionalPsycopg(TestBase): def setUp(self): super().setUp() PsycopgInstrumentor().instrument(enable_commenter=True) def tearDown(self): PsycopgInstrumentor().uninstrument() super().tearDown() def test_commenter_enabled_root_module(self): connection = psycopg.connect( dbname=POSTGRES_DB_NAME, user=POSTGRES_USER, password=POSTGRES_PASSWORD, host=POSTGRES_HOST, port=POSTGRES_PORT, ) cursor = connection.cursor() cursor.execute("SELECT 1;") self.assertRegex( cursor._query.query.decode("ascii"), r"SELECT 1 /\*db_driver='psycopg(.*)',dbapi_level='\d.\d',dbapi_threadsafety=\d,driver_paramstyle=(.*),libpq_version=\d*,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) cursor.close() connection.close() def test_commenter_enabled_not_root_module(self): connection = psycopg.Connection.connect( dbname=POSTGRES_DB_NAME, user=POSTGRES_USER, password=POSTGRES_PASSWORD, host=POSTGRES_HOST, port=POSTGRES_PORT, ) cursor = connection.cursor() cursor.execute("SELECT 1;") self.assertRegex( cursor._query.query.decode("ascii"), r"SELECT 1 /\*db_driver='Connection%%3Aunknown',dbapi_level='\d.\d',dbapi_threadsafety='unknown',driver_paramstyle='unknown',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) cursor.close() connection.close() opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/pymongo/000077500000000000000000000000001527070072300310425ustar00rootroot00000000000000test_pymongo_functional.py000066400000000000000000000220521527070072300363070ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/pymongo# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import contextlib import os from unittest import mock from pymongo import MongoClient from pymongo.errors import OperationFailure from opentelemetry import trace as trace_api from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.pymongo import PymongoInstrumentor from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_MONGODB_COLLECTION, DB_NAME, DB_STATEMENT, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_COLLECTION_NAME, DB_NAMESPACE, DB_QUERY_TEXT, DB_SYSTEM_NAME, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.test.test_base import TestBase MONGODB_HOST = os.getenv("MONGODB_HOST", "localhost") MONGODB_PORT = int(os.getenv("MONGODB_PORT", "27017")) MONGODB_DB_NAME = os.getenv("MONGODB_DB_NAME", "opentelemetry-tests") MONGODB_COLLECTION_NAME = "test" @contextlib.contextmanager def use_semconv_opt_in(sem_conv_mode): env_patch = mock.patch.dict( "os.environ", {OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode}, ) _OpenTelemetrySemanticConventionStability._initialized = False env_patch.start() try: yield finally: env_patch.stop() _OpenTelemetrySemanticConventionStability._initialized = False class TestFunctionalPymongo(TestBase): def setUp(self): super().setUp() self._tracer = self.tracer_provider.get_tracer(__name__) PymongoInstrumentor._instance = None PymongoInstrumentor._commandtracer_instance = None _OpenTelemetrySemanticConventionStability._initialized = False self.instrumentor = PymongoInstrumentor() self.instrumentor.instrument() self.instrumentor._commandtracer_instance._tracer = self._tracer self.instrumentor._commandtracer_instance.capture_statement = True client = MongoClient(MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000) db = client[MONGODB_DB_NAME] self._collection = db[MONGODB_COLLECTION_NAME] def tearDown(self): self.instrumentor.uninstrument() PymongoInstrumentor._instance = None PymongoInstrumentor._commandtracer_instance = None _OpenTelemetrySemanticConventionStability._initialized = False super().tearDown() def validate_spans(self, expected_db_statement): spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) for span in spans: if span.name == "rootSpan": root_span = span else: pymongo_span = span self.assertIsInstance(span.start_time, int) self.assertIsInstance(span.end_time, int) self.assertIsNot(root_span, None) self.assertIsNot(pymongo_span, None) self.assertIsNotNone(pymongo_span.parent) self.assertIs(pymongo_span.parent, root_span.get_span_context()) self.assertIs(pymongo_span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(pymongo_span.attributes[DB_NAME], MONGODB_DB_NAME) self.assertEqual(pymongo_span.attributes[NET_PEER_NAME], MONGODB_HOST) self.assertEqual(pymongo_span.attributes[NET_PEER_PORT], MONGODB_PORT) self.assertEqual( pymongo_span.attributes[DB_MONGODB_COLLECTION], MONGODB_COLLECTION_NAME, ) self.assertEqual( pymongo_span.attributes[DB_STATEMENT], expected_db_statement, ) def test_insert(self): """Should create a child span for insert""" with self._tracer.start_as_current_span("rootSpan"): insert_result = self._collection.insert_one({"name": "testName", "value": "testValue"}) insert_result_id = insert_result.inserted_id expected_db_statement = ( f"insert [{{'name': 'testName', 'value': 'testValue', '_id': ObjectId('{insert_result_id}')}}]" ) self.validate_spans(expected_db_statement) def test_update(self): """Should create a child span for update""" with self._tracer.start_as_current_span("rootSpan"): self._collection.update_one({"name": "testName"}, {"$set": {"value": "someOtherValue"}}) expected_db_statement = ( "update [SON([('q', {'name': 'testName'}), ('u', " "{'$set': {'value': 'someOtherValue'}}), ('multi', False), ('upsert', False)])]" ) self.validate_spans(expected_db_statement) def test_find(self): """Should create a child span for find""" with self._tracer.start_as_current_span("rootSpan"): self._collection.find_one({"name": "testName"}) expected_db_statement = "find {'name': 'testName'}" self.validate_spans(expected_db_statement) def test_delete(self): """Should create a child span for delete""" with self._tracer.start_as_current_span("rootSpan"): self._collection.delete_one({"name": "testName"}) expected_db_statement = "delete [SON([('q', {'name': 'testName'}), ('limit', 1)])]" self.validate_spans(expected_db_statement) def test_find_without_capture_statement(self): """Should create a child span for find""" self.instrumentor._commandtracer_instance.capture_statement = False with self._tracer.start_as_current_span("rootSpan"): self._collection.find_one({"name": "testName"}) expected_db_statement = "find" self.validate_spans(expected_db_statement) def test_uninstrument(self): # check that integration is working self._collection.find_one() spans = self.memory_exporter.get_finished_spans() self.memory_exporter.clear() self.assertEqual(len(spans), 1) # uninstrument and check not new spans are created PymongoInstrumentor().uninstrument() self._collection.find_one() spans = self.memory_exporter.get_finished_spans() self.memory_exporter.clear() self.assertEqual(len(spans), 0) # re-enable and check that it works again PymongoInstrumentor().instrument() self._collection.find_one() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) def test_find_stable_semconv(self): with use_semconv_opt_in("database"): self.instrumentor.uninstrument() self.instrumentor._commandtracer_instance = None self.instrumentor.instrument() self.instrumentor._commandtracer_instance._tracer = self._tracer self.instrumentor._commandtracer_instance.capture_statement = True client = MongoClient(MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000) collection = client[MONGODB_DB_NAME][MONGODB_COLLECTION_NAME] with self._tracer.start_as_current_span("rootSpan"): collection.find_one({"name": "testName"}) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) pymongo_span = next(s for s in spans if s.name != "rootSpan") self.assertEqual(pymongo_span.attributes[DB_SYSTEM_NAME], "mongodb") self.assertEqual(pymongo_span.attributes[DB_NAMESPACE], MONGODB_DB_NAME) self.assertEqual( pymongo_span.attributes[DB_QUERY_TEXT], "find {'name': 'testName'}", ) self.assertEqual(pymongo_span.attributes[SERVER_ADDRESS], MONGODB_HOST) self.assertEqual(pymongo_span.attributes[SERVER_PORT], MONGODB_PORT) self.assertEqual( pymongo_span.attributes[DB_COLLECTION_NAME], MONGODB_COLLECTION_NAME, ) def test_failed_command_stable_semconv_records_error_type(self): with use_semconv_opt_in("database"): self.instrumentor.uninstrument() self.instrumentor._commandtracer_instance = None self.instrumentor.instrument() self.instrumentor._commandtracer_instance._tracer = self._tracer client = MongoClient(MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000) db = client[MONGODB_DB_NAME] with self._tracer.start_as_current_span("rootSpan"): with self.assertRaises(OperationFailure): db.command({"fakeCommand": 1}) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) pymongo_span = next(s for s in spans if s.name != "rootSpan") self.assertEqual( pymongo_span.status.status_code, trace_api.StatusCode.ERROR, ) self.assertEqual(pymongo_span.status.description, "no such command: 'fakeCommand'") self.assertEqual( pymongo_span.attributes[ERROR_TYPE], "CommandNotFound", ) opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/pymysql/000077500000000000000000000000001527070072300310705ustar00rootroot00000000000000test_pymysql_functional.py000066400000000000000000000113451527070072300363660ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/pymysql# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import pymysql as pymy from opentelemetry import trace as trace_api from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_SYSTEM, DB_USER, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.test.test_base import TestBase MYSQL_USER = os.getenv("MYSQL_USER", "testuser") MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "testpassword") MYSQL_HOST = os.getenv("MYSQL_HOST", "localhost") MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306")) MYSQL_DB_NAME = os.getenv("MYSQL_DB_NAME", "opentelemetry-tests") class TestFunctionalPyMysql(TestBase): def setUp(self): super().setUp() self._tracer = self.tracer_provider.get_tracer(__name__) PyMySQLInstrumentor().instrument() self._connection = pymy.connect( user=MYSQL_USER, password=MYSQL_PASSWORD, host=MYSQL_HOST, port=MYSQL_PORT, database=MYSQL_DB_NAME, ) self._cursor = self._connection.cursor() def tearDown(self): self._cursor.close() self._connection.close() PyMySQLInstrumentor().uninstrument() super().tearDown() def validate_spans(self, span_name): spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) for span in spans: if span.name == "rootSpan": root_span = span else: db_span = span self.assertIsInstance(span.start_time, int) self.assertIsInstance(span.end_time, int) self.assertIsNotNone(root_span) self.assertIsNotNone(db_span) self.assertEqual(root_span.name, "rootSpan") self.assertEqual(db_span.name, span_name) self.assertIsNotNone(db_span.parent) self.assertIs(db_span.parent, root_span.get_span_context()) self.assertIs(db_span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(db_span.attributes[DB_SYSTEM], "mysql") self.assertEqual(db_span.attributes[DB_NAME], MYSQL_DB_NAME) self.assertEqual(db_span.attributes[DB_USER], MYSQL_USER) self.assertEqual(db_span.attributes[NET_PEER_NAME], MYSQL_HOST) self.assertEqual(db_span.attributes[NET_PEER_PORT], MYSQL_PORT) def test_execute(self): """Should create a child span for execute""" stmt = "CREATE TABLE IF NOT EXISTS test (id INT)" with self._tracer.start_as_current_span("rootSpan"): self._cursor.execute(stmt) self.validate_spans("CREATE") def test_execute_with_cursor_context_manager(self): """Should create a child span for execute with cursor context""" stmt = "CREATE TABLE IF NOT EXISTS test (id INT)" with self._tracer.start_as_current_span("rootSpan"): with self._connection.cursor() as cursor: cursor.execute(stmt) self.validate_spans("CREATE") def test_executemany(self): """Should create a child span for executemany""" stmt = "INSERT INTO test (id) VALUES (%s)" with self._tracer.start_as_current_span("rootSpan"): data = (("1",), ("2",), ("3",)) self._cursor.executemany(stmt, data) self.validate_spans("INSERT") def test_executemany_with_cursor_iteration(self): """Should create a child span for executemany while iterating over the cursor""" stmt = "SELECT * FROM test" with self._tracer.start_as_current_span("rootSpan"): with self._connection.cursor() as cursor: cursor.execute(stmt) for _row in cursor: pass self.validate_spans("SELECT") def test_callproc(self): """Should create a child span for callproc""" with ( self._tracer.start_as_current_span("rootSpan"), self.assertRaises(Exception), ): self._cursor.callproc("test", ()) self.validate_spans("test") def test_commit(self): stmt = "INSERT INTO test (id) VALUES (%s)" with self._tracer.start_as_current_span("rootSpan"): data = (("4",), ("5",), ("6",)) self._cursor.executemany(stmt, data) self._connection.commit() self.validate_spans("INSERT") def test_rollback(self): stmt = "INSERT INTO test (id) VALUES (%s)" with self._tracer.start_as_current_span("rootSpan"): data = (("7",), ("8",), ("9",)) self._cursor.executemany(stmt, data) self._connection.rollback() self.validate_spans("INSERT") opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/redis/000077500000000000000000000000001527070072300304605ustar00rootroot00000000000000test_redis_functional.py000066400000000000000000000602221527070072300353440ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/redis# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio from time import time_ns import redis import redis.asyncio from redis.commands.search.field import ( TextField, VectorField, ) from redis.commands.search.index_definition import IndexDefinition, IndexType from redis.commands.search.query import Query from redis.exceptions import ResponseError from opentelemetry import trace from opentelemetry.instrumentation.redis import RedisInstrumentor from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_REDIS_DATABASE_INDEX, DB_STATEMENT, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from opentelemetry.test.test_base import TestBase class TestRedisInstrument(TestBase): def setUp(self): super().setUp() self.redis_client = redis.Redis(port=6379) self.redis_client.flushall() RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) def tearDown(self): RedisInstrumentor().uninstrument() super().tearDown() def _check_span(self, span, name): self.assertEqual(span.name, name) self.assertIs(span.status.status_code, trace.StatusCode.UNSET) self.assertEqual(span.attributes.get(DB_REDIS_DATABASE_INDEX), 0) self.assertEqual(span.attributes[NET_PEER_NAME], "localhost") self.assertEqual(span.attributes[NET_PEER_PORT], 6379) def test_long_command_sanitized(self): RedisInstrumentor().uninstrument() RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) self.redis_client.mget(*range(2000)) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self._check_span(span, "MGET") self.assertTrue(span.attributes.get(DB_STATEMENT).startswith("MGET ? ? ? ?")) self.assertTrue(span.attributes.get(DB_STATEMENT).endswith("...")) def test_long_command(self): self.redis_client.mget(*range(1000)) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self._check_span(span, "MGET") self.assertTrue(span.attributes.get(DB_STATEMENT).startswith("MGET ? ? ? ?")) self.assertTrue(span.attributes.get(DB_STATEMENT).endswith("...")) def test_basics_sanitized(self): RedisInstrumentor().uninstrument() RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) self.assertIsNone(self.redis_client.get("cheese")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self._check_span(span, "GET") self.assertEqual(span.attributes.get(DB_STATEMENT), "GET ?") self.assertEqual(span.attributes.get("db.redis.args_length"), 2) def test_basics(self): self.assertIsNone(self.redis_client.get("cheese")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self._check_span(span, "GET") self.assertEqual(span.attributes.get(DB_STATEMENT), "GET ?") self.assertEqual(span.attributes.get("db.redis.args_length"), 2) def test_pipeline_traced_sanitized(self): RedisInstrumentor().uninstrument() RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) with self.redis_client.pipeline(transaction=False) as pipeline: pipeline.set("blah", 32) pipeline.rpush("foo", "éé") pipeline.hgetall("xxx") pipeline.execute() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self._check_span(span, "SET RPUSH HGETALL") self.assertEqual( span.attributes.get(DB_STATEMENT), "SET ? ?\nRPUSH ? ?\nHGETALL ?", ) self.assertEqual(span.attributes.get("db.redis.pipeline_length"), 3) def test_pipeline_traced(self): with self.redis_client.pipeline(transaction=False) as pipeline: pipeline.set("blah", 32) pipeline.rpush("foo", "éé") pipeline.hgetall("xxx") pipeline.execute() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self._check_span(span, "SET RPUSH HGETALL") self.assertEqual( span.attributes.get(DB_STATEMENT), "SET ? ?\nRPUSH ? ?\nHGETALL ?", ) self.assertEqual(span.attributes.get("db.redis.pipeline_length"), 3) def test_pipeline_immediate_sanitized(self): RedisInstrumentor().uninstrument() RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) with self.redis_client.pipeline() as pipeline: pipeline.set("a", 1) pipeline.immediate_execute_command("SET", "b", 2) pipeline.execute() spans = self.memory_exporter.get_finished_spans() # expecting two separate spans here, rather than a # single span for the whole pipeline self.assertEqual(len(spans), 2) span = spans[0] self._check_span(span, "SET") self.assertEqual(span.attributes.get(DB_STATEMENT), "SET ? ?") def test_pipeline_immediate(self): with self.redis_client.pipeline() as pipeline: pipeline.set("a", 1) pipeline.immediate_execute_command("SET", "b", 2) pipeline.execute() spans = self.memory_exporter.get_finished_spans() # expecting two separate spans here, rather than a # single span for the whole pipeline self.assertEqual(len(spans), 2) span = spans[0] self._check_span(span, "SET") self.assertEqual(span.attributes.get(DB_STATEMENT), "SET ? ?") def test_parent(self): """Ensure OpenTelemetry works with redis.""" ot_tracer = trace.get_tracer("redis_svc") with ot_tracer.start_as_current_span("redis_get"): self.assertIsNone(self.redis_client.get("cheese")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) child_span, parent_span = spans[0], spans[1] # confirm the parenting self.assertIsNone(parent_span.parent) self.assertIs(child_span.parent, parent_span.get_span_context()) self.assertEqual(parent_span.name, "redis_get") self.assertEqual(parent_span.instrumentation_info.name, "redis_svc") self.assertEqual(child_span.name, "GET") class TestRedisClusterInstrument(TestBase): def setUp(self): super().setUp() self.redis_client = redis.cluster.RedisCluster(host="localhost", port=7000) self.redis_client.flushall() RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) def tearDown(self): super().tearDown() RedisInstrumentor().uninstrument() def _check_span(self, span, name): self.assertEqual(span.name, name) self.assertIs(span.status.status_code, trace.StatusCode.UNSET) def test_basics(self): self.assertIsNone(self.redis_client.get("cheese")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self._check_span(span, "GET") self.assertEqual(span.attributes.get(DB_STATEMENT), "GET ?") self.assertEqual(span.attributes.get("db.redis.args_length"), 2) def test_pipeline_traced(self): with self.redis_client.pipeline(transaction=False) as pipeline: pipeline.set("blah", 32) pipeline.rpush("foo", "éé") pipeline.hgetall("xxx") pipeline.execute() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self._check_span(span, "SET RPUSH HGETALL") self.assertEqual( span.attributes.get(DB_STATEMENT), "SET ? ?\nRPUSH ? ?\nHGETALL ?", ) self.assertEqual(span.attributes.get("db.redis.pipeline_length"), 3) def test_cluster_pipeline_span_metadata_regression_4084(self): """Regression test for issue #4084 against a real cluster. redis-py 6+ refactored ClusterPipeline so queued commands live on ``_execution_strategy.command_queue`` and no longer populate ``command_stack``. This previously produced a ClusterPipeline span with an empty ``db.statement`` and a ``db.redis.pipeline_length`` of 0. """ with self.redis_client.pipeline(transaction=False) as pipeline: pipeline.set("blah", 32) pipeline.rpush("foo", "éé") pipeline.hgetall("xxx") # On redis-py 6+ the queued commands are tracked on the execution # strategy rather than command_stack; assert the regression path is # genuinely exercised before checking the emitted span metadata. if hasattr(pipeline, "_execution_strategy") and hasattr(pipeline._execution_strategy, "command_queue"): self.assertEqual(pipeline.command_stack, []) self.assertEqual(len(pipeline._execution_strategy.command_queue), 3) pipeline.execute() spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self._check_span(span, "SET RPUSH HGETALL") self.assertEqual( span.attributes.get(DB_STATEMENT), "SET ? ?\nRPUSH ? ?\nHGETALL ?", ) self.assertEqual(span.attributes.get("db.redis.pipeline_length"), 3) def test_parent(self): """Ensure OpenTelemetry works with redis.""" ot_tracer = trace.get_tracer("redis_svc") with ot_tracer.start_as_current_span("redis_get"): self.assertIsNone(self.redis_client.get("cheese")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) child_span, parent_span = spans[0], spans[1] # confirm the parenting self.assertIsNone(parent_span.parent) self.assertIs(child_span.parent, parent_span.get_span_context()) self.assertEqual(parent_span.name, "redis_get") self.assertEqual(parent_span.instrumentation_info.name, "redis_svc") self.assertEqual(child_span.name, "GET") def async_call(coro): loop = asyncio.get_event_loop() return loop.run_until_complete(coro) class TestAsyncRedisInstrument(TestBase): def setUp(self): super().setUp() self.redis_client = redis.asyncio.Redis(port=6379) async_call(self.redis_client.flushall()) RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) def tearDown(self): RedisInstrumentor().uninstrument() super().tearDown() def _check_span(self, span, name): self.assertEqual(span.name, name) self.assertIs(span.status.status_code, trace.StatusCode.UNSET) self.assertEqual(span.attributes.get(DB_REDIS_DATABASE_INDEX), 0) self.assertEqual(span.attributes[NET_PEER_NAME], "localhost") self.assertEqual(span.attributes[NET_PEER_PORT], 6379) def test_long_command(self): async_call(self.redis_client.mget(*range(1000))) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self._check_span(span, "MGET") self.assertTrue(span.attributes.get(DB_STATEMENT).startswith("MGET ? ? ? ?")) self.assertTrue(span.attributes.get(DB_STATEMENT).endswith("...")) def test_basics(self): self.assertIsNone(async_call(self.redis_client.get("cheese"))) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self._check_span(span, "GET") self.assertEqual(span.attributes.get(DB_STATEMENT), "GET ?") self.assertEqual(span.attributes.get("db.redis.args_length"), 2) def test_execute_command_traced_full_time(self): """Command should be traced for coroutine execution time, not creation time.""" coro_created_time = None finish_time = None async def pipeline_simple(): nonlocal coro_created_time nonlocal finish_time # delay coroutine creation from coroutine execution coro = self.redis_client.get("foo") coro_created_time = time_ns() await coro finish_time = time_ns() async_call(pipeline_simple()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertTrue(span.start_time > coro_created_time) self.assertTrue(span.end_time < finish_time) def test_pipeline_traced(self): async def pipeline_simple(): async with self.redis_client.pipeline(transaction=False) as pipeline: pipeline.set("blah", 32) pipeline.rpush("foo", "éé") pipeline.hgetall("xxx") await pipeline.execute() async_call(pipeline_simple()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self._check_span(span, "SET RPUSH HGETALL") self.assertEqual( span.attributes.get(DB_STATEMENT), "SET ? ?\nRPUSH ? ?\nHGETALL ?", ) self.assertEqual(span.attributes.get("db.redis.pipeline_length"), 3) def test_pipeline_traced_full_time(self): """Command should be traced for coroutine execution time, not creation time.""" coro_created_time = None finish_time = None async def pipeline_simple(): async with self.redis_client.pipeline(transaction=False) as pipeline: nonlocal coro_created_time nonlocal finish_time pipeline.set("blah", 32) pipeline.rpush("foo", "éé") pipeline.hgetall("xxx") # delay coroutine creation from coroutine execution coro = pipeline.execute() coro_created_time = time_ns() await coro finish_time = time_ns() async_call(pipeline_simple()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertTrue(span.start_time > coro_created_time) self.assertTrue(span.end_time < finish_time) def test_pipeline_immediate(self): async def pipeline_immediate(): async with self.redis_client.pipeline() as pipeline: pipeline.set("a", 1) await pipeline.immediate_execute_command("SET", "b", 2) await pipeline.execute() async_call(pipeline_immediate()) spans = self.memory_exporter.get_finished_spans() # expecting two separate spans here, rather than a # single span for the whole pipeline self.assertEqual(len(spans), 2) span = spans[0] self._check_span(span, "SET") self.assertEqual(span.attributes.get(DB_STATEMENT), "SET ? ?") def test_pipeline_immediate_traced_full_time(self): """Command should be traced for coroutine execution time, not creation time.""" coro_created_time = None finish_time = None async def pipeline_simple(): async with self.redis_client.pipeline(transaction=False) as pipeline: nonlocal coro_created_time nonlocal finish_time pipeline.set("a", 1) # delay coroutine creation from coroutine execution coro = pipeline.immediate_execute_command("SET", "b", 2) coro_created_time = time_ns() await coro finish_time = time_ns() async_call(pipeline_simple()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertTrue(span.start_time > coro_created_time) self.assertTrue(span.end_time < finish_time) def test_parent(self): """Ensure OpenTelemetry works with redis.""" ot_tracer = trace.get_tracer("redis_svc") with ot_tracer.start_as_current_span("redis_get"): self.assertIsNone(async_call(self.redis_client.get("cheese"))) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) child_span, parent_span = spans[0], spans[1] # confirm the parenting self.assertIsNone(parent_span.parent) self.assertIs(child_span.parent, parent_span.get_span_context()) self.assertEqual(parent_span.name, "redis_get") self.assertEqual(parent_span.instrumentation_info.name, "redis_svc") self.assertEqual(child_span.name, "GET") class TestAsyncRedisClusterInstrument(TestBase): def setUp(self): super().setUp() self.redis_client = redis.asyncio.cluster.RedisCluster(host="localhost", port=7000) async_call(self.redis_client.flushall()) RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) def tearDown(self): super().tearDown() RedisInstrumentor().uninstrument() def _check_span(self, span, name): self.assertEqual(span.name, name) self.assertIs(span.status.status_code, trace.StatusCode.UNSET) def test_basics(self): self.assertIsNone(async_call(self.redis_client.get("cheese"))) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self._check_span(span, "GET") self.assertEqual(span.attributes.get(DB_STATEMENT), "GET ?") self.assertEqual(span.attributes.get("db.redis.args_length"), 2) def test_execute_command_traced_full_time(self): """Command should be traced for coroutine execution time, not creation time.""" coro_created_time = None finish_time = None async def pipeline_simple(): nonlocal coro_created_time nonlocal finish_time # delay coroutine creation from coroutine execution coro = self.redis_client.get("foo") coro_created_time = time_ns() await coro finish_time = time_ns() async_call(pipeline_simple()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertTrue(span.start_time > coro_created_time) self.assertTrue(span.end_time < finish_time) def test_pipeline_traced(self): async def pipeline_simple(): async with self.redis_client.pipeline(transaction=False) as pipeline: pipeline.set("blah", 32) pipeline.rpush("foo", "éé") pipeline.hgetall("xxx") await pipeline.execute() async_call(pipeline_simple()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self._check_span(span, "SET RPUSH HGETALL") self.assertEqual( span.attributes.get(DB_STATEMENT), "SET ? ?\nRPUSH ? ?\nHGETALL ?", ) self.assertEqual(span.attributes.get("db.redis.pipeline_length"), 3) def test_pipeline_traced_full_time(self): """Command should be traced for coroutine execution time, not creation time.""" coro_created_time = None finish_time = None async def pipeline_simple(): async with self.redis_client.pipeline(transaction=False) as pipeline: nonlocal coro_created_time nonlocal finish_time pipeline.set("blah", 32) pipeline.rpush("foo", "éé") pipeline.hgetall("xxx") # delay coroutine creation from coroutine execution coro = pipeline.execute() coro_created_time = time_ns() await coro finish_time = time_ns() async_call(pipeline_simple()) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self.assertTrue(span.start_time > coro_created_time) self.assertTrue(span.end_time < finish_time) def test_parent(self): """Ensure OpenTelemetry works with redis.""" ot_tracer = trace.get_tracer("redis_svc") with ot_tracer.start_as_current_span("redis_get"): self.assertIsNone(async_call(self.redis_client.get("cheese"))) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 2) child_span, parent_span = spans[0], spans[1] # confirm the parenting self.assertIsNone(parent_span.parent) self.assertIs(child_span.parent, parent_span.get_span_context()) self.assertEqual(parent_span.name, "redis_get") self.assertEqual(parent_span.instrumentation_info.name, "redis_svc") self.assertEqual(child_span.name, "GET") class TestRedisDBIndexInstrument(TestBase): def setUp(self): super().setUp() self.redis_client = redis.Redis(port=6379, db=10) self.redis_client.flushall() RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) def tearDown(self): RedisInstrumentor().uninstrument() super().tearDown() def _check_span(self, span, name): self.assertEqual(span.name, name) self.assertIs(span.status.status_code, trace.StatusCode.UNSET) self.assertEqual(span.attributes[NET_PEER_NAME], "localhost") self.assertEqual(span.attributes[NET_PEER_PORT], 6379) self.assertEqual(span.attributes[DB_REDIS_DATABASE_INDEX], 10) def test_get(self): self.assertIsNone(self.redis_client.get("foo")) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] self._check_span(span, "GET") self.assertEqual(span.attributes.get(DB_STATEMENT), "GET ?") class TestRedisearchInstrument(TestBase): def setUp(self): super().setUp() self.redis_client = redis.Redis(port=6379) self.redis_client.flushall() self.embedding_dim = 256 RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) self.prepare_data() self.create_index() def tearDown(self): RedisInstrumentor().uninstrument() super().tearDown() def prepare_data(self): try: self.redis_client.ft("idx:test_vss").dropindex(True) except ResponseError: print("No such index") item = { "name": "test", "value": "test_value", "embeddings": [0.1] * 256, } pipeline = self.redis_client.pipeline() pipeline.json().set("test:001", "$", item) res = pipeline.execute() assert False not in res def create_index(self): schema = ( TextField("$.name", no_stem=True, as_name="name"), TextField("$.value", no_stem=True, as_name="value"), VectorField( "$.embeddings", "FLAT", { "TYPE": "FLOAT32", "DIM": self.embedding_dim, "DISTANCE_METRIC": "COSINE", }, as_name="vector", ), ) definition = IndexDefinition(prefix=["test:"], index_type=IndexType.JSON) res = self.redis_client.ft("idx:test_vss").create_index(fields=schema, definition=definition) assert "OK" in str(res) def test_redis_create_index(self): spans = self.memory_exporter.get_finished_spans() span = next(span for span in spans if span.name == "redis.create_index") assert "redis.create_index.fields" in span.attributes def test_redis_query(self): query = "@name:test" self.redis_client.ft("idx:test_vss").search(Query(query)) spans = self.memory_exporter.get_finished_spans() span = next(span for span in spans if span.name == "redis.search") assert span.attributes.get("redis.search.query") == query assert span.attributes.get("redis.search.total") == 1 opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/000077500000000000000000000000001527070072300327365ustar00rootroot00000000000000__init__.py000066400000000000000000000001141527070072300347640ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 mixins.py000066400000000000000000000212211527070072300345360ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import contextlib import logging import threading from sqlalchemy import Column, Integer, String, create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import close_all_sessions, scoped_session, sessionmaker from opentelemetry import trace from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_STATEMENT, DB_SYSTEM, ) from opentelemetry.test.test_base import TestBase Base = declarative_base() def _create_engine(engine_args): # create a SQLAlchemy engine config = dict(engine_args) url = config.pop("url") return create_engine(url, **config) class Player(Base): """Player entity used to test SQLAlchemy ORM""" __tablename__ = "players" id = Column(Integer, primary_key=True) name = Column(String(20)) class SQLAlchemyTestMixin(TestBase): __test__ = False """SQLAlchemy test mixin that includes a complete set of tests that must be executed for different engine. When a new test (or a regression test) should be added to SQLAlchemy test suite, a new entry must be appended here so that it will be executed for all available and supported engines. If the test is specific to only one engine, that test must be added to the specific `TestCase` implementation. To support a new engine, create a new `TestCase` that inherits from `SQLAlchemyTestMixin` and `TestCase`. Then you must define the following static class variables: * VENDOR: the database vendor name * SQL_DB: the `db.type` tag that we expect (it's the name of the database available in the `.env` file) * SERVICE: the service that we expect by default * ENGINE_ARGS: all arguments required to create the engine To check specific tags in each test, you must implement the `check_meta(self, span)` method. """ VENDOR = None SQL_DB = None SERVICE = None ENGINE_ARGS = None @contextlib.contextmanager def connection(self): # context manager that provides a connection # to the underlying database try: conn = self.engine.connect() yield conn finally: conn.close() def check_meta(self, span): """function that can be implemented according to the specific engine implementation """ def setUp(self): super().setUp() # create an engine with the given arguments self.engine = _create_engine(self.ENGINE_ARGS) # create the database / entities and prepare a session for the test Base.metadata.drop_all(bind=self.engine) Base.metadata.create_all(self.engine, checkfirst=False) self.session = sessionmaker(bind=self.engine)() # trace the engine SQLAlchemyInstrumentor().instrument(engine=self.engine, tracer_provider=self.tracer_provider) self.memory_exporter.clear() def tearDown(self): # pylint: disable=invalid-name # clear the database and dispose the engine self.session.close() Base.metadata.drop_all(bind=self.engine) self.engine.dispose() SQLAlchemyInstrumentor().uninstrument() super().tearDown() def _check_span(self, span, name): if self.SQL_DB: name = f"{name} {self.SQL_DB}" self.assertEqual(span.name, name) self.assertEqual(span.attributes.get(DB_NAME), self.SQL_DB) self.assertIs(span.status.status_code, trace.StatusCode.UNSET) self.assertGreater((span.end_time - span.start_time), 0) def test_orm_insert(self): # ensures that the ORM session is traced wayne = Player(id=1, name="wayne") self.session.add(wayne) self.session.commit() spans = self.memory_exporter.get_finished_spans() # one span for the connection and one for the query self.assertEqual(len(spans), 2) span = spans[1] stmt = "INSERT INTO players (id, name) VALUES " if span.attributes.get(DB_SYSTEM) == "sqlite": stmt += "(?, ?)" else: stmt += "(%(id)s, %(name)s)" self._check_span(span, "INSERT") self.assertIn( "INSERT INTO players", span.attributes.get(DB_STATEMENT), ) self.check_meta(span) def test_session_query(self): # ensures that the Session queries are traced out = list(self.session.query(Player).filter_by(name="wayne")) self.assertEqual(len(out), 0) spans = self.memory_exporter.get_finished_spans() # one span for the connection and one for the query self.assertEqual(len(spans), 2) span = spans[1] stmt = "SELECT players.id AS players_id, players.name AS players_name \nFROM players \nWHERE players.name = " if span.attributes.get(DB_SYSTEM) == "sqlite": stmt += "?" else: stmt += "%(name_1)s" self._check_span(span, "SELECT") self.assertIn( "SELECT players.id AS players_id, players.name AS players_name \nFROM players \nWHERE players.name", span.attributes.get(DB_STATEMENT), ) self.check_meta(span) def test_engine_connect_execute(self): # ensures that engine.connect() is properly traced stmt = "SELECT * FROM players" with self.connection() as conn: rows = conn.execute(stmt).fetchall() self.assertEqual(len(rows), 0) spans = self.memory_exporter.get_finished_spans() # one span for the connection and one for the query self.assertEqual(len(spans), 2) span = spans[1] self._check_span(span, "SELECT") self.assertEqual( span.attributes.get(DB_STATEMENT), "SELECT * FROM players", ) self.check_meta(span) def test_parent(self): """Ensure that sqlalchemy works with opentelemetry.""" stmt = "SELECT * FROM players" tracer = self.tracer_provider.get_tracer("sqlalch_svc") with tracer.start_as_current_span("sqlalch_op"): with self.connection() as conn: rows = conn.execute(stmt).fetchall() self.assertEqual(len(rows), 0) spans = self.memory_exporter.get_finished_spans() # one span for the connection and two for the queries self.assertEqual(len(spans), 3) _, child_span, parent_span = spans # confirm the parenting self.assertIsNone(parent_span.parent) self.assertIs(child_span.parent, parent_span.get_span_context()) self.assertEqual(parent_span.name, "sqlalch_op") self.assertEqual(parent_span.instrumentation_info.name, "sqlalch_svc") self.assertEqual(child_span.name, "SELECT " + self.SQL_DB) def test_multithreading(self): """Ensure spans are captured correctly in a multithreading scenario We also expect no logged warnings about calling end() on an ended span. """ if self.VENDOR == "sqlite": return def insert_player(session): _session = session() player = Player(name="Player") _session.add(player) _session.commit() _session.query(Player).all() def insert_players(session): _session = session() players = [] for player_number in range(3): players.append(Player(name=f"Player {player_number}")) _session.add_all(players) _session.commit() session_factory = sessionmaker(bind=self.engine) # pylint: disable=invalid-name Session = scoped_session(session_factory) thread_one = threading.Thread(target=insert_player, args=(Session,)) thread_two = threading.Thread(target=insert_players, args=(Session,)) logger = logging.getLogger("opentelemetry.sdk.trace") with self.assertRaises(AssertionError): with self.assertLogs(logger, level="WARNING"): thread_one.start() thread_two.start() thread_one.join() thread_two.join() close_all_sessions() spans = self.memory_exporter.get_finished_spans() # SQLAlchemy 1.4 uses the `execute_values` extension of the psycopg2 dialect to # batch inserts together which means `insert_players` only generates one span. # See https://docs.sqlalchemy.org/en/14/changelog/migration_14.html#orm-batch-inserts-with-psycopg2-now-batch-statements-with-returning-in-most-cases self.assertEqual(len(spans), 8 if self.VENDOR not in ["postgresql"] else 6) test_instrument.py000066400000000000000000000035171527070072300365060ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import sqlalchemy from opentelemetry import trace from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from opentelemetry.test.test_base import TestBase POSTGRES_CONFIG = { "host": "127.0.0.1", "port": int(os.getenv("TEST_POSTGRES_PORT", "5432")), "user": os.getenv("TEST_POSTGRES_USER", "testuser"), "password": os.getenv("TEST_POSTGRES_PASSWORD", "testpassword"), "dbname": os.getenv("TEST_POSTGRES_DB", "opentelemetry-tests"), } class SQLAlchemyInstrumentTestCase(TestBase): """TestCase that checks if the engine is properly traced when the `instrument()` method is used. """ def setUp(self): super().setUp() # create a traced engine with the given arguments SQLAlchemyInstrumentor().instrument() dsn = "postgresql://%(user)s:%(password)s@%(host)s:%(port)s/%(dbname)s" % POSTGRES_CONFIG self.engine = sqlalchemy.create_engine(dsn) # prepare a connection self.conn = self.engine.connect() def tearDown(self): # clear the database and dispose the engine self.conn.close() self.engine.dispose() SQLAlchemyInstrumentor().uninstrument() super().tearDown() def test_engine_traced(self): # ensures that the engine is traced rows = self.conn.execute("SELECT").fetchall() self.assertEqual(len(rows), 1) spans = self.memory_exporter.get_finished_spans() # trace composition self.assertEqual(len(spans), 2) span = spans[1] # check subset of span fields self.assertEqual(span.name, "SELECT opentelemetry-tests") self.assertIs(span.status.status_code, trace.StatusCode.UNSET) self.assertGreater((span.end_time - span.start_time), 0) test_mssql.py000066400000000000000000000070161527070072300354330ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import pyodbc import pytest from sqlalchemy.exc import ProgrammingError from opentelemetry import trace from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_STATEMENT, DB_USER, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from .mixins import Player, SQLAlchemyTestMixin MSSQL_CONFIG = { "host": "127.0.0.1", "port": int(os.getenv("TEST_MSSQL_PORT", "1433")), "user": os.getenv("TEST_MSSQL_USER", "sa"), "password": os.getenv("TEST_MSSQL_PASSWORD", "yourStrong(!)Password"), "database": os.getenv("TEST_MSSQL_DATABASE", "opentelemetry-tests"), "driver": os.getenv("TEST_MSSQL_DRIVER", "ODBC+Driver+18+for+SQL+Server"), "trusted_connection": os.getenv("TEST_MSSQL_TRUSTED_CONNECTION", "yes"), } @pytest.mark.skipif( "ODBC Driver 18 for SQL Server" not in pyodbc.drivers(), reason="No MS SQL ODBC driver installed", ) class MssqlConnectorTestCase(SQLAlchemyTestMixin): """TestCase for pyodbc engine""" __test__ = True VENDOR = "mssql" SQL_DB = "opentelemetry-tests" ENGINE_ARGS = { "url": "mssql+pyodbc://%(user)s:%(password)s@%(host)s:%(port)s/%(database)s?driver=%(driver)s&TrustServerCertificate=%(trusted_connection)s" % MSSQL_CONFIG } def check_meta(self, span): # check database connection tags self.assertEqual( span.attributes.get(NET_PEER_NAME), MSSQL_CONFIG["host"], ) self.assertEqual( span.attributes.get(NET_PEER_PORT), MSSQL_CONFIG["port"], ) self.assertEqual( span.attributes.get(DB_NAME), MSSQL_CONFIG["database"], ) self.assertEqual(span.attributes.get(DB_USER), MSSQL_CONFIG["user"]) def test_engine_execute_errors(self): # ensures that SQL errors are reported with pytest.raises(ProgrammingError): with self.connection() as conn: conn.execute("SELECT * FROM a_wrong_table").fetchall() spans = self.memory_exporter.get_finished_spans() # one span for the connection and one for the query self.assertEqual(len(spans), 2) self.check_meta(spans[0]) span = spans[1] # span fields self.assertEqual(span.name, "SELECT opentelemetry-tests") self.assertEqual( span.attributes.get(DB_STATEMENT), "SELECT * FROM a_wrong_table", ) self.assertEqual(span.attributes.get(DB_NAME), self.SQL_DB) self.check_meta(span) self.assertTrue(span.end_time - span.start_time > 0) # check the error self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) self.assertIn("a_wrong_table", span.status.description) def test_orm_insert(self): # ensures that the ORM session is traced wayne = Player(id=1, name="wayne") self.session.add(wayne) self.session.commit() spans = self.memory_exporter.get_finished_spans() # connect, identity insert on before the insert, insert, and identity insert off after the insert self.assertEqual(len(spans), 4) self.check_meta(spans[0]) span = spans[2] self._check_span(span, "INSERT") self.assertIn( "INSERT INTO players", span.attributes.get(DB_STATEMENT), ) self.check_meta(span) test_mysql.py000066400000000000000000000050271527070072300354410ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import pytest from sqlalchemy.exc import ProgrammingError from opentelemetry import trace from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_STATEMENT, DB_USER, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from .mixins import SQLAlchemyTestMixin MYSQL_CONFIG = { "host": "127.0.0.1", "port": int(os.getenv("TEST_MYSQL_PORT", "3306")), "user": os.getenv("TEST_MYSQL_USER", "testuser"), "password": os.getenv("TEST_MYSQL_PASSWORD", "testpassword"), "database": os.getenv("TEST_MYSQL_DATABASE", "opentelemetry-tests"), } class MysqlConnectorTestCase(SQLAlchemyTestMixin): """TestCase for mysql-connector engine""" __test__ = True VENDOR = "mysql" SQL_DB = "opentelemetry-tests" ENGINE_ARGS = {"url": "mysql+mysqlconnector://%(user)s:%(password)s@%(host)s:%(port)s/%(database)s" % MYSQL_CONFIG} def check_meta(self, span): # check database connection tags self.assertEqual( span.attributes.get(NET_PEER_NAME), MYSQL_CONFIG["host"], ) self.assertEqual( span.attributes.get(NET_PEER_PORT), MYSQL_CONFIG["port"], ) self.assertEqual( span.attributes.get(DB_NAME), MYSQL_CONFIG["database"], ) self.assertEqual(span.attributes.get(DB_USER), MYSQL_CONFIG["user"]) def test_engine_execute_errors(self): # ensures that SQL errors are reported with pytest.raises(ProgrammingError): with self.connection() as conn: conn.execute("SELECT * FROM a_wrong_table").fetchall() spans = self.memory_exporter.get_finished_spans() # one span for the connection and one for the query self.assertEqual(len(spans), 2) self.check_meta(spans[0]) span = spans[1] # span fields self.assertEqual(span.name, "SELECT opentelemetry-tests") self.assertEqual( span.attributes.get(DB_STATEMENT), "SELECT * FROM a_wrong_table", ) self.assertEqual(span.attributes.get(DB_NAME), self.SQL_DB) self.check_meta(span) self.assertTrue(span.end_time - span.start_time > 0) # check the error self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) self.assertIn("a_wrong_table", span.status.description) test_postgres.py000066400000000000000000000074401527070072300361430ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import psycopg2 import pytest from sqlalchemy.exc import ProgrammingError from opentelemetry import trace from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_STATEMENT, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_NAME, NET_PEER_PORT, ) from .mixins import SQLAlchemyTestMixin SCOPE = "opentelemetry.instrumentation.sqlalchemy" POSTGRES_CONFIG = { "host": "127.0.0.1", "port": int(os.getenv("TEST_POSTGRES_PORT", "5432")), "user": os.getenv("TEST_POSTGRES_USER", "testuser"), "password": os.getenv("TEST_POSTGRES_PASSWORD", "testpassword"), "dbname": os.getenv("TEST_POSTGRES_DB", "opentelemetry-tests"), } class PostgresTestCase(SQLAlchemyTestMixin): """TestCase for Postgres Engine""" __test__ = True VENDOR = "postgresql" SQL_DB = "opentelemetry-tests" ENGINE_ARGS = {"url": "postgresql://%(user)s:%(password)s@%(host)s:%(port)s/%(dbname)s" % POSTGRES_CONFIG} def check_meta(self, span): # check database connection tags self.assertEqual( span.attributes.get(NET_PEER_NAME), POSTGRES_CONFIG["host"], ) self.assertEqual( span.attributes.get(NET_PEER_PORT), POSTGRES_CONFIG["port"], ) def test_engine_execute_errors(self): # ensures that SQL errors are reported with pytest.raises(ProgrammingError): with self.connection() as conn: conn.execute("SELECT * FROM a_wrong_table").fetchall() spans = self.memory_exporter.get_finished_spans() # one span for the connection and one for the query self.assertEqual(len(spans), 2) span = spans[1] # span fields self.assertEqual(span.name, "SELECT opentelemetry-tests") self.assertEqual( span.attributes.get(DB_STATEMENT), "SELECT * FROM a_wrong_table", ) self.assertEqual(span.attributes.get(DB_NAME), self.SQL_DB) self.check_meta(span) self.assertTrue(span.end_time - span.start_time > 0) # check the error self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) self.assertIn("a_wrong_table", span.status.description) class PostgresCreatorTestCase(PostgresTestCase): """TestCase for Postgres Engine that includes the same tests set of `PostgresTestCase`, but it uses a specific `creator` function. """ VENDOR = "postgresql" SQL_DB = "opentelemetry-tests" ENGINE_ARGS = { "url": "postgresql://", "creator": lambda: psycopg2.connect(**POSTGRES_CONFIG), } class PostgresMetricsTestCase(PostgresTestCase): __test__ = True VENDOR = "postgresql" SQL_DB = "opentelemetry-tests" ENGINE_ARGS = {"url": "postgresql://%(user)s:%(password)s@%(host)s:%(port)s/%(dbname)s" % POSTGRES_CONFIG} def test_metrics_pool_name(self): with self.connection() as conn: conn.execute("SELECT 1 + 1").fetchall() pool_name = "{}://{}:{}/{}".format( self.VENDOR, POSTGRES_CONFIG["host"], POSTGRES_CONFIG["port"], self.SQL_DB, ) metrics = self.get_sorted_metrics(SCOPE) self.assertEqual(len(metrics), 1) self.assert_metric_expected( metrics[0], [ self.create_number_data_point( value=0, attributes={"pool.name": pool_name, "state": "idle"}, ), self.create_number_data_point( value=0, attributes={"pool.name": pool_name, "state": "used"}, ), ], ) test_sqlite.py000066400000000000000000000027641527070072300356020ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import pytest from sqlalchemy.exc import OperationalError from opentelemetry import trace from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_STATEMENT, ) from .mixins import SQLAlchemyTestMixin class SQLiteTestCase(SQLAlchemyTestMixin): """TestCase for the SQLite engine""" __test__ = True VENDOR = "sqlite" SQL_DB = ":memory:" ENGINE_ARGS = {"url": "sqlite:///:memory:"} def test_engine_execute_errors(self): # ensures that SQL errors are reported stmt = "SELECT * FROM a_wrong_table" with pytest.raises(OperationalError): with self.connection() as conn: conn.execute(stmt).fetchall() spans = self.memory_exporter.get_finished_spans() # one span for the connection and one span for the query self.assertEqual(len(spans), 2) span = spans[1] # span fields self.assertEqual(span.name, "SELECT :memory:") self.assertEqual( span.attributes.get(DB_STATEMENT), "SELECT * FROM a_wrong_table", ) self.assertEqual(span.attributes.get(DB_NAME), self.SQL_DB) self.assertTrue((span.end_time - span.start_time) > 0) # check the error self.assertIs( span.status.status_code, trace.StatusCode.ERROR, ) self.assertEqual(span.status.description, "no such table: a_wrong_table") opentelemetry-python-contrib-0.66b2/tests/opentelemetry-docker-tests/tests/test-requirements.txt000066400000000000000000000021601527070072300336120ustar00rootroot00000000000000aiopg==1.4.0 amqp==5.2.0 asgiref==3.8.1 async-timeout==4.0.3 asyncpg==0.31.0 billiard==4.2.0 celery==5.3.6 certifi==2024.7.4 chardet==3.0.4 click==8.1.7 click-didyoumean==0.3.0 click-plugins==1.1.1 click-repl==0.3.0 Django==4.2.30 dnspython==2.6.1 exceptiongroup==1.2.0 flaky==3.7.0 flask==3.1.3 greenlet==3.3.1 grpcio==1.81.1 idna==3.15 iniconfig==2.0.0 kafka-python==3.0.8 kombu==5.3.5 mysql-connector-python==8.3.0 mysqlclient==2.1.1 packaging==24.0 pluggy==1.6.0 prometheus_client==0.20.0 prompt-toolkit==3.0.43 # prerequisite: install libpq-dev (debian) or postgresql-devel (rhel), postgresql (mac) # see https://www.psycopg.org/docs/install.html#build-prerequisites # you might have to install additional packages depending on your OS psycopg==3.3.4 psycopg2==2.9.12 psycopg2-binary==2.9.12 pymongo==4.6.3 PyMySQL==1.1.1 # prerequisite: install unixodbc pyodbc==5.3.0 pytest==8.0.2 pytest-celery==0.0.0 python-dateutil==2.9.0.post0 pytz==2024.1 redis==6.4.0 requests==2.33.1 six==1.16.0 SQLAlchemy==1.4.52 tomli==2.0.1 typing_extensions==4.13.2 tzdata==2024.1 urllib3==2.7.0 vine==5.1.0 wcwidth==0.2.13 wrapt==1.16.0 zipp==3.19.1 opentelemetry-python-contrib-0.66b2/tox-uv.toml000066400000000000000000000001271527070072300217040ustar00rootroot00000000000000# https://docs.astral.sh/uv/reference/settings/#pip_no-sources [pip] no-sources = true opentelemetry-python-contrib-0.66b2/tox.ini000066400000000000000000001737201527070072300210720ustar00rootroot00000000000000[tox] requires = tox-uv>=1 isolated_build = True skipsdist = True skip_missing_interpreters = True envlist = ; Environments are organized by individual package, allowing ; for specifying supported Python versions per package. ; instrumentation-openai py3{10,11,12,13,14}-test-instrumentation-openai-v2-{oldest,latest} pypy3-test-instrumentation-openai-v2-{oldest,latest} lint-instrumentation-openai-v2 ; instrumentation-openai-agents py3{10,11,12,13,14}-test-instrumentation-openai_agents-v2-{oldest,latest} lint-instrumentation-openai_agents-v2 ; instrumentation-vertexai py3{10,11,12,13,14}-test-instrumentation-vertexai-{oldest,latest} # Disabling pypy3 as shapely does not have wheels and fails to compile # pypy3-test-instrumentation-vertexai-{oldest,latest} lint-instrumentation-vertexai ; opentelemetry-resource-detector-containerid py3{10,11,12,13,14}-test-resource-detector-containerid pypy3-test-resource-detector-containerid lint-resource-detector-containerid ; opentelemetry-resource-detector-azure py3{10,11,12,13,14}-test-resource-detector-azure-{0,1} pypy3-test-resource-detector-azure-{0,1} lint-resource-detector-azure ; opentelemetry-resourcedetector-gcp py3{10,11,12,13,14}-test-resourcedetector-gcp-{0,1} pypy3-test-resourcedetector-gcp-{0,1} lint-resourcedetector-gcp ; opentelemetry-sdk-extension-aws py3{10,11,12,13,14}-test-sdk-extension-aws-{0,1} pypy3-test-sdk-extension-aws-{0,1} lint-sdk-extension-aws benchmark-sdk-extension-aws ; opentelemetry-distro py3{10,11,12,13,14}-test-distro pypy3-test-distro lint-distro ; opentelemetry-instrumentation py3{10,11,12,13,14}-test-opentelemetry-instrumentation-{wrapt1,wrapt2} pypy3-test-opentelemetry-instrumentation-wrapt2 lint-opentelemetry-instrumentation ; opentelemetry-instrumentation-aiohttp-client py3{10,11,12,13,14}-test-instrumentation-aiohttp-client pypy3-test-instrumentation-aiohttp-client lint-instrumentation-aiohttp-client ; opentelemetry-instrumentation-aiohttp-server py3{10,11,12,13,14}-test-instrumentation-aiohttp-server pypy3-test-instrumentation-aiohttp-server lint-instrumentation-aiohttp-server ; opentelemetry-instrumentation-aiopg py3{10,11,12,13,14}-test-instrumentation-aiopg-{wrapt1,wrapt2} ; instrumentation-aiopg intentionally excluded from pypy3 lint-instrumentation-aiopg ; opentelemetry-instrumentation-aws-lambda py3{10,11,12,13,14}-test-instrumentation-aws-lambda pypy3-test-instrumentation-aws-lambda lint-instrumentation-aws-lambda ; opentelemetry-instrumentation-botocore py3{10,11,12,13,14}-test-instrumentation-botocore-{0,1}-{wrapt1,wrapt2} py3{10,11,12,13,14}-test-instrumentation-botocore-{2,3} pypy3-test-instrumentation-botocore-1-wrapt2 pypy3-test-instrumentation-botocore-3 lint-instrumentation-botocore ; opentelemetry-instrumentation-boto3sqs py3{10,11,12,13,14}-test-instrumentation-boto3sqs pypy3-test-instrumentation-boto3sqs lint-instrumentation-boto3sqs ; opentelemetry-instrumentation-django ; Only officially supported Python versions are tested for each Django ; major release. Updated list can be found at: ; https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django ; The numbers at the end of the environment names ; below mean these dependencies are being used: ; 0: django~=2.0 ; 1: django~=3.0 ; 2: django~=4.0 ; 3: django>=5.2<6.0 py3{10,11,12}-test-instrumentation-django-{0,1,2,3} py3{13,14}-test-instrumentation-django-3 pypy3-test-instrumentation-django-{0,1,2,3} lint-instrumentation-django ; opentelemetry-instrumentation-dbapi py3{10,11,12,13,14}-test-instrumentation-dbapi-{wrapt1,wrapt2} pypy3-test-instrumentation-dbapi-wrapt2 lint-instrumentation-dbapi ; opentelemetry-instrumentation-asyncclick py3{10,11,12,13,14}-test-instrumentation-asyncclick pypy3-test-instrumentation-asyncclick lint-instrumentation-asyncclick ; opentelemetry-instrumentation-click py3{10,11,12,13,14}-test-instrumentation-click pypy3-test-instrumentation-click lint-instrumentation-click ; opentelemetry-instrumentation-falcon ; TODO: py310 does not work with falcon 1 - Drop requirements-0.txt and bump supported version to >2 ; The numbers at the end of the environment names ; below mean these dependencies are being used: ; 1: falcon >=2.0.0,<3.0.0 ; 2: falcon >=3.0.0,<3.1.2 ; 3: falcon >=3.1.2,<4.0.0 ; 4: falcon >=4.0.0,<5.0.0 py3{10,11,12}-test-instrumentation-falcon-{1,2,3,4} py3{13,14}-test-instrumentation-falcon-4 pypy3-test-instrumentation-falcon-{1,2,3,4} lint-instrumentation-falcon ; opentelemetry-instrumentation-fastapi ; oldest is not really the oldest we can support, it's the oldest to run Python 3.14 py3{10,11,12,13,14}-test-instrumentation-fastapi-{oldest,latest} pypy3-test-instrumentation-fastapi-{oldest,latest} lint-instrumentation-fastapi ; opentelemetry-instrumentation-flask ; The numbers at the end of the environment names ; below mean these dependencies are being used: ; 0: Flask ==2.1.3 Werkzeug <3.0.0 ; 1: Flask ==2.2.0 Werkzeug <3.0.0 ; 2: Flask >=3.0.0 Werkzeug >=3.0.0 ; 3: Flask >=3.1.0 Werkzeug >=3.1.0 py3{10,11,12,13,14}-test-instrumentation-flask-{0,1,2,3} pypy3-test-instrumentation-flask-{0,1} lint-instrumentation-flask ; opentelemetry-instrumentation-urllib py3{10,11,12,13,14}-test-instrumentation-urllib pypy3-test-instrumentation-urllib lint-instrumentation-urllib ; opentelemetry-instrumentation-urllib3 ; The numbers at the end of the environment names ; below mean these dependencies are being used: ; 0: urllib3 >=1.0.0,<2.0.0 ; 1: urllib3 >=2.0.0,<3.0.0 py3{10,11,12,13,14}-test-instrumentation-urllib3-{0,1} pypy3-test-instrumentation-urllib3-{0,1} lint-instrumentation-urllib3 benchmark-instrumentation-urllib3 ; opentelemetry-instrumentation-requests py3{10,11,12,13,14}-test-instrumentation-requests ;pypy3-test-instrumentation-requests lint-instrumentation-requests benchmark-instrumentation-requests ; opentelemetry-instrumentation-starlette py3{10,11,12,13,14}-test-instrumentation-starlette-{oldest,latest} pypy3-test-instrumentation-starlette-{oldest,latest} lint-instrumentation-starlette ; opentelemetry-instrumentation-structlog py3{10,11,12,13,14}-test-instrumentation-structlog-{oldest,latest} pypy3-test-instrumentation-structlog-{oldest,latest} lint-instrumentation-structlog ; opentelemetry-instrumentation-jinja2 py3{10,11,12,13,14}-test-instrumentation-jinja2 pypy3-test-instrumentation-jinja2 lint-instrumentation-jinja2 ; opentelemetry-instrumentation-logging py3{10,11,12,13,14}-test-instrumentation-logging pypy3-test-instrumentation-logging lint-instrumentation-logging benchmark-instrumentation-logging ; opentelemetry-instrumentation-exceptions py3{10,11,12,13,14}-test-instrumentation-exceptions pypy3-test-instrumentation-exceptions lint-instrumentation-exceptions ; opentelemetry-exporter-richconsole py3{10,11,12,13,14}-test-exporter-richconsole pypy3-test-exporter-richconsole lint-exporter-richconsole ; opentelemetry-exporter-prometheus-remote-write py3{10,11,12,13,14}-test-exporter-prometheus-remote-write # excluded from pypy3 due to missing wheel pypy3-test-exporter-prometheus-remote-write lint-exporter-prometheus-remote-write ; opentelemetry-instrumentation-mysql ; The numbers at the end of the environment names ; below mean these dependencies are being used: ; 0: mysql-connector-python >=8.0.0,<9.0.0 ; 1: mysql-connector-python ~=9.0.0 ; 2: mysql-connector-python >=26.7.0 py3{10,11,12,13,14}-test-instrumentation-mysql-{0,1,2} pypy3-test-instrumentation-mysql-{0,1,2} lint-instrumentation-mysql ; opentelemetry-instrumentation-mysqlclient py3{10,11,12,13,14}-test-instrumentation-mysqlclient pypy3-test-instrumentation-mysqlclient ; prerequisite: follow the instructions here ; https://github.com/PyMySQL/mysqlclient#install ; for your OS to install the required dependencies lint-instrumentation-mysqlclient ; opentelemetry-instrumentation-psycopg2 py3{10,11,12,13,14}-test-instrumentation-psycopg2 py3{10,11,12,13,14}-test-instrumentation-psycopg2-binary ; ext-psycopg2 intentionally excluded from pypy3 lint-instrumentation-psycopg2 ; opentelemetry-instrumentation-psycopg py3{10,11,12,13,14}-test-instrumentation-psycopg pypy3-test-instrumentation-psycopg lint-instrumentation-psycopg ; opentelemetry-instrumentation-pymemcache ; The numbers at the end of the environment names ; below mean these dependencies are being used: ; 0: pymemcache ==1.3.5 ; 1: pymemcache >2.0.0,<3.0.0 ; 2: pymemcache >3.0.0,<3.4.2 ; 3: pymemcache ==3.4.2 ; 4: pymemcache ==4.0.0 py3{10,11,12,13,14}-test-instrumentation-pymemcache-{0,1,2,3,4} pypy3-test-instrumentation-pymemcache-{0,1,2,3,4} lint-instrumentation-pymemcache ; opentelemetry-instrumentation-pymongo py3{10,11,12,13,14}-test-instrumentation-pymongo pypy3-test-instrumentation-pymongo lint-instrumentation-pymongo ; opentelemetry-instrumentation-pymysql py3{10,11,12,13,14}-test-instrumentation-pymysql pypy3-test-instrumentation-pymysql lint-instrumentation-pymysql ; opentelemetry-instrumentation-pymssql py3{10,11,12,13,14}-test-instrumentation-pymssql ; pymssql has no support for pypy: see https://github.com/pymssql/pymssql/pull/517 ; pypy3-test-instrumentation-pymssql lint-instrumentation-pymssql ; opentelemetry-instrumentation-pyramid py3{10,11,12,13,14}-test-instrumentation-pyramid pypy3-test-instrumentation-pyramid lint-instrumentation-pyramid ; opentelemetry-instrumentation-asgi py3{10,11,12,13,14}-test-instrumentation-asgi pypy3-test-instrumentation-asgi lint-instrumentation-asgi ; opentelemetry-instrumentation-asyncpg py3{10,11,12,13,14}-test-instrumentation-asyncpg-{wrapt1,wrapt2} ; ext-asyncpg intentionally excluded from pypy3 lint-instrumentation-asyncpg ; opentelemetry-instrumentation-sqlite3 py3{10,11,12,13,14}-test-instrumentation-sqlite3 pypy3-test-instrumentation-sqlite3 lint-instrumentation-sqlite3 ; opentelemetry-instrumentation-wsgi py3{10,11,12,13,14}-test-instrumentation-wsgi pypy3-test-instrumentation-wsgi lint-instrumentation-wsgi ; opentelemetry-instrumentation-grpc ; The numbers at the end of the environment names ; below mean these dependencies are being used: ; 0: grpcio==1.62.0 ; 1: grpcio==1.75.1 ; TODO #4201 simplify tests-requirements here to use latest|oldest testing pattern py3{10,11,12}-test-instrumentation-grpc-{0,1}-{wrapt1,wrapt2} py3{13,14}-test-instrumentation-grpc-1-{wrapt1,wrapt2} lint-instrumentation-grpc ; opentelemetry-instrumentation-sqlalchemy ; The numbers at the end of the environment names ; below mean these dependencies are being used: ; 0: sqlalchemy>=1.1,<1.2 - TODO: drop test-requirements-0.txt and update to >=1.4,<2.0 once 1.1 support is removed ; 1: sqlalchemy~=1.4 aiosqlite ; 2: sqlalchemy~=2.0.0 py3{10,11,12,13}-test-instrumentation-sqlalchemy-{1,2} py314-test-instrumentation-sqlalchemy-2 pypy3-test-instrumentation-sqlalchemy-{1,2} lint-instrumentation-sqlalchemy ; opentelemetry-instrumentation-redis py3{10,11,12,13,14}-test-instrumentation-redis pypy3-test-instrumentation-redis lint-instrumentation-redis ; opentelemetry-instrumentation-remoulade py3{10,11,12,13,14}-test-instrumentation-remoulade ; instrumentation-remoulade intentionally excluded from pypy3 lint-instrumentation-remoulade ; opentelemetry-instrumentation-celery py3{10,11,12,13,14}-test-instrumentation-celery pypy3-test-instrumentation-celery lint-instrumentation-celery ; opentelemetry-instrumentation-system-metrics py3{10,11,12,13,14}-test-instrumentation-system-metrics pypy3-test-instrumentation-system-metrics lint-instrumentation-system-metrics ; opentelemetry-instrumentation-threading py3{10,11,12,13,14}-test-instrumentation-threading pypy3-test-instrumentation-threading lint-instrumentation-threading ; opentelemetry-instrumentation-tornado py3{10,11,12,13,14}-test-instrumentation-tornado pypy3-test-instrumentation-tornado lint-instrumentation-tornado ; opentelemetry-instrumentation-tortoiseorm py3{10,11,12,13,14}-test-instrumentation-tortoiseorm pypy3-test-instrumentation-tortoiseorm lint-instrumentation-tortoiseorm ; opentelemetry-instrumentation-httpx ; The numbers at the end of the environment names ; below mean these dependencies are being used: ; 0: httpx>=0.18.0,<0.19.0 respx~=0.17.0 ; 1: httpx>=0.19.0 respx~=0.20.1 ; 2: httpx2>=2.0.0 ; 3: httpx>=0.19.0 respx~=0.20.1 httpx2>=2.0.0 py3{10,11,12}-test-instrumentation-httpx-{0,1}-{wrapt1,wrapt2} py3{10,11,12,13,14}-test-instrumentation-httpx-2-{wrapt1,wrapt2} py3{10,11,12,13,14}-test-instrumentation-httpx-3-{wrapt1,wrapt2} py3{13,14}-test-instrumentation-httpx-1-{wrapt1,wrapt2} pypy3-test-instrumentation-httpx-{0,1}-wrapt2 lint-instrumentation-httpx ; opentelemetry-util-http py3{10,11,12,13,14}-test-util-http pypy3-test-util-http lint-util-http ; opentelemetry-exporter-credential-provider-gcp py3{10,11,12,13,14}-test-exporter-credential-provider-gcp lint-exporter-credential-provider-gcp ; opentelemetry-util-genai py3{10,11,12,13,14}-test-util-genai pypy3-test-util-genai lint-util-genai ; opentelemetry-propagator-aws-xray py3{10,11,12,13,14}-test-propagator-aws-xray-{0,1} pypy3-test-propagator-aws-xray-{0,1} lint-propagator-aws-xray benchmark-propagator-aws-xray ; opentelemetry-propagator-ot-trace py3{10,11,12,13,14}-test-propagator-ot-trace pypy3-test-propagator-ot-trace lint-propagator-ot-trace ; opentelemetry-instrumentation-sio-pika ; The numbers at the end of the environment names ; below mean these dependencies are being used: ; 0: pika>=0.12.0,<1.0.0 ; 1: pika>=1.0.0 py3{10,11,12,13,14}-test-instrumentation-sio-pika-{0,1}-{wrapt1,wrapt2} pypy3-test-instrumentation-sio-pika-{0,1}-wrapt2 lint-instrumentation-sio-pika ; opentelemetry-instrumentation-aio-pika ; The environment names below mean these ; dependencies are being used: ; oldest: aio_pika==7.2.0 ; latest: aio_pika==9.6.2 for python3.10 aio_pika==10.0.1 otherwise py3{10,11,12,13,14}-test-instrumentation-aio-pika-oldest py3{10,11,12,13,14}-test-instrumentation-aio-pika-latest pypy3-test-instrumentation-aio-pika-{oldest,latest} lint-instrumentation-aio-pika ; opentelemetry-instrumentation-aiokafka py3{10,11,12,13,14}-test-instrumentation-aiokafka pypy3-test-instrumentation-aiokafka lint-instrumentation-aiokafka ; opentelemetry-instrumentation-kafka-python py3{10,11,12,13,14}-test-instrumentation-kafka-python-{oldest,latest} pypy3-test-instrumentation-kafka-python-{oldest,latest} lint-instrumentation-kafka-python ; opentelemetry-instrumentation-confluent-kafka py3{10,11,12,13,14}-test-instrumentation-confluent-kafka lint-instrumentation-confluent-kafka ; opentelemetry-instrumentation-asyncio py3{10,11,12,13,14}-test-instrumentation-asyncio lint-instrumentation-asyncio ; opentelemetry-instrumentation-cassandra py3{10,11,12,13,14}-test-instrumentation-cassandra-{driver,scylla} pypy3-test-instrumentation-cassandra-scylla lint-instrumentation-cassandra ; opentelemetry-processor-baggage py3{10,11,12,13,14}-test-processor-baggage pypy3-test-processor-baggage ; requires snappy headers to be available on the system lint-processor-baggage ; opentelemetry-opamp-client py3{10,11,12,13,14}-test-opamp-client-{latest,lowest} ; https://github.com/kevin1024/vcrpy/pull/775#issuecomment-1847849962 ; pypy3-test-opamp-client lint-opamp-client lint-license-header-check spellcheck docker-tests docs generate generate-workflows shellcheck precommit typecheck [testenv] test_deps = opentelemetry-api@{env:CORE_REPO_API} opentelemetry-semantic-conventions@{env:CORE_REPO_SEMCONV} opentelemetry-sdk@{env:CORE_REPO_SDK} opentelemetry-test-utils@{env:CORE_REPO_TEST_UTILS} deps = lint: -r dev-requirements.txt coverage: pytest coverage: pytest-cov opentelemetry-instrumentation: {[testenv]test_deps} opentelemetry-instrumentation-wrapt1: -r {toxinidir}/opentelemetry-instrumentation/test-requirements-wrapt1.txt opentelemetry-instrumentation-wrapt2: -r {toxinidir}/opentelemetry-instrumentation/test-requirements-wrapt2.txt lint-opentelemetry-instrumentation: -r {toxinidir}/opentelemetry-instrumentation/test-requirements-wrapt2.txt distro: {[testenv]test_deps} distro: -r {toxinidir}/opentelemetry-distro/test-requirements.txt # packages that are released individually should provide a test-requirements.txt with the lowest version of OTel API # and SDK supported to test we are honoring it openai-oldest: -r {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/requirements.oldest.txt # and the latest version of OTel API and SDK openai-latest: {[testenv]test_deps} openai-latest: -r {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/requirements.latest.txt lint-instrumentation-openai-v2: -r {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/requirements.oldest.txt openai_agents-oldest: -r {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/requirements.oldest.txt openai_agents-latest: {[testenv]test_deps} openai_agents-latest: -r {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/requirements.latest.txt lint-instrumentation-openai_agents-v2: -r {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/requirements.oldest.txt vertexai-oldest: -r {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/requirements.oldest.txt vertexai-latest: {[testenv]test_deps} vertexai-latest: -r {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/requirements.latest.txt lint-instrumentation-vertexai: -r {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/requirements.oldest.txt asgi: {[testenv]test_deps} asgi: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi/test-requirements.txt celery: {[testenv]test_deps} py3{10,11,12,13,14}-test-instrumentation-celery: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/test-requirements-1.txt pypy3-test-instrumentation-celery: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/test-requirements-1.txt lint-instrumentation-celery: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/test-requirements-1.txt asyncclick: {[testenv]test_deps} asyncclick: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncclick/test-requirements.txt click: {[testenv]test_deps} click: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-click/test-requirements.txt sio-pika: {[testenv]test_deps} sio-pika-0-wrapt1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pika/test-requirements-0-wrapt1.txt sio-pika-0-wrapt2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pika/test-requirements-0-wrapt2.txt sio-pika-1-wrapt1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pika/test-requirements-1-wrapt1.txt sio-pika-1-wrapt2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pika/test-requirements-1-wrapt2.txt lint-instrumentation-sio-pika: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pika/test-requirements-1-wrapt2.txt aio-pika: {[testenv]test_deps} aio-pika-oldest: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-oldest.txt aio-pika-latest: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-latest.txt lint-instrumentation-aio-pika: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-latest.txt aiokafka: {[testenv]test_deps} aiokafka: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aiokafka/test-requirements.txt kafka-python: {[testenv]test_deps} kafka-python-oldest: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.oldest.txt kafka-python-latest: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.latest.txt lint-instrumentation-kafka-python: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.latest.txt confluent-kafka: {[testenv]test_deps} confluent-kafka: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-confluent-kafka/test-requirements.txt grpc: {[testenv]test_deps} grpc-0-wrapt1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc/test-requirements-0-wrapt1.txt grpc-0-wrapt2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc/test-requirements-0-wrapt2.txt grpc-1-wrapt1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc/test-requirements-1-wrapt1.txt grpc-1-wrapt2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc/test-requirements-1-wrapt2.txt lint-instrumentation-grpc: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc/test-requirements-1-wrapt2.txt wsgi: {[testenv]test_deps} wsgi: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi/test-requirements.txt asyncpg: {[testenv]test_deps} asyncpg-wrapt1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg/test-requirements-wrapt1.txt asyncpg-wrapt2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg/test-requirements-wrapt2.txt lint-instrumentation-asyncpg: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg/test-requirements-wrapt2.txt aws-lambda: {[testenv]test_deps} aws-lambda: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda/test-requirements.txt boto3sqs: {[testenv]test_deps} boto3sqs: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs/test-requirements.txt falcon: {[testenv]test_deps} falcon-0: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-0.txt falcon-1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-1.txt falcon-2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-2.txt falcon-3: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-3.txt falcon-4: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-4.txt lint-instrumentation-falcon: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-4.txt flask: {[testenv]test_deps} flask-0: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-flask/test-requirements-0.txt flask-1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-flask/test-requirements-1.txt flask-2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-flask/test-requirements-2.txt flask-3: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-flask/test-requirements-3.txt lint-instrumentation-flask: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-flask/test-requirements-2.txt urllib: {[testenv]test_deps} urllib: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib/test-requirements.txt urllib3: {[testenv]test_deps} urllib3-0: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-0.txt urllib3-1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-1.txt lint-instrumentation-urllib3: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-1.txt benchmark-instrumentation-urllib3: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3/benchmark-requirements.txt botocore: {[testenv]test_deps} botocore-0-wrapt1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/test-requirements-0-wrapt1.txt botocore-0-wrapt2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/test-requirements-0-wrapt2.txt botocore-1-wrapt1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/test-requirements-1-wrapt1.txt botocore-1-wrapt2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/test-requirements-1-wrapt2.txt ; Note: aiobotocore is not compatible with wrapt2 botocore-2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/test-requirements-2.txt botocore-3: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/test-requirements-3.txt lint-instrumentation-botocore: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/test-requirements-3.txt cassandra: {[testenv]test_deps} cassandra-driver: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra/test-requirements.txt cassandra-scylla: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra/test-requirements-scylla.txt lint-instrumentation-cassandra: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra/test-requirements.txt dbapi: {[testenv]test_deps} dbapi-wrapt1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi/test-requirements-wrapt1.txt dbapi-wrapt2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi/test-requirements-wrapt2.txt lint-instrumentation-dbapi: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi/test-requirements-wrapt2.txt django: {[testenv]test_deps} django-0: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-django/test-requirements-0.txt django-1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-django/test-requirements-1.txt django-2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-django/test-requirements-2.txt django-3: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-django/test-requirements-3.txt lint-instrumentation-django: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-django/test-requirements-3.txt fastapi: {[testenv]test_deps} fastapi-oldest: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.oldest.txt fastapi-{latest,lint}: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.latest.txt mysql: {[testenv]test_deps} mysql-0: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql/test-requirements-0.txt mysql-1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql/test-requirements-1.txt mysql-2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql/test-requirements-2.txt lint-instrumentation-mysql: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql/test-requirements-2.txt mysqlclient: {[testenv]test_deps} mysqlclient: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-mysqlclient/test-requirements.txt pymemcache: {[testenv]test_deps} pymemcache-0: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-0.txt pymemcache-1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-1.txt pymemcache-2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-2.txt pymemcache-3: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-3.txt pymemcache-4: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-4.txt lint-instrumentation-pymemcache: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-4.txt pymongo: {[testenv]test_deps} pymongo: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo/test-requirements.txt psycopg: {[testenv]test_deps} py3{10,11,12,13,14}-test-instrumentation-psycopg: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-1.txt pypy3-test-instrumentation-psycopg: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-1.txt lint-instrumentation-psycopg: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-1.txt psycopg2: {[testenv]test_deps} psycopg2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2/test-requirements.txt psycopg2-binary: {[testenv]test_deps} psycopg2-binary: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2/test-requirements-binary.txt pymysql: {[testenv]test_deps} pymysql: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql/test-requirements.txt pymssql: {[testenv]test_deps} pymssql: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pymssql/test-requirements.txt pyramid: {[testenv]test_deps} pyramid: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid/test-requirements.txt sqlite3: {[testenv]test_deps} sqlite3: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3/test-requirements.txt redis: {[testenv]test_deps} redis: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt remoulade: {[testenv]test_deps} remoulade: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade/test-requirements.txt requests: {[testenv]test_deps} requests: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-requests/test-requirements.txt benchmark-instrumentation-requests: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-requests/benchmark-requirements.txt starlette: {[testenv]test_deps} starlette-{oldest,lint}: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette/test-requirements.oldest.txt starlette-latest: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette/test-requirements.latest.txt structlog: {[testenv]test_deps} structlog-{oldest,lint}: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-structlog/test-requirements.oldest.txt structlog-latest: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-structlog/test-requirements.latest.txt system-metrics: {[testenv]test_deps} system-metrics: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics/test-requirements.txt threading: {[testenv]test_deps} threading: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-threading/test-requirements.txt tornado: {[testenv]test_deps} tornado: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado/test-requirements.txt tortoiseorm: {[testenv]test_deps} tortoiseorm: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-tortoiseorm/test-requirements.txt jinja2: {[testenv]test_deps} jinja2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2/test-requirements.txt logging: {[testenv]test_deps} logging: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-logging/test-requirements.txt benchmark-instrumentation-logging: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-logging/benchmark-requirements.txt exceptions: {[testenv]test_deps} exceptions: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-exceptions/test-requirements.txt aiohttp-client: {[testenv]test_deps} aiohttp-client: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client/test-requirements.txt aiohttp-server: {[testenv]test_deps} aiohttp-server: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt aiopg: {[testenv]test_deps} aiopg-wrapt1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg/test-requirements-wrapt1.txt aiopg-wrapt2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg/test-requirements-wrapt2.txt lint-instrumentation-aiopg: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg/test-requirements-wrapt2.txt richconsole: {[testenv]test_deps} richconsole: -r {toxinidir}/exporter/opentelemetry-exporter-richconsole/test-requirements.txt prometheus: {[testenv]test_deps} prometheus: -r {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write/test-requirements.txt sqlalchemy: {[testenv]test_deps} sqlalchemy-0: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-0.txt sqlalchemy-1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-1.txt sqlalchemy-2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-2.txt lint-instrumentation-sqlalchemy: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-2.txt asyncio: {[testenv]test_deps} asyncio: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncio/test-requirements.txt httpx: {[testenv]test_deps} httpx-0-wrapt1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-0-wrapt1.txt httpx-0-wrapt2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-0-wrapt2.txt httpx-1-wrapt1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-1-wrapt1.txt httpx-1-wrapt2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-1-wrapt2.txt httpx-2-wrapt1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-2-wrapt1.txt httpx-2-wrapt2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-2-wrapt2.txt httpx-3-wrapt1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-3-wrapt1.txt httpx-3-wrapt2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-3-wrapt2.txt lint-instrumentation-httpx: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-1-wrapt2.txt # packages that are released individually should provide a test-requirements.txt with the lowest version of OTel API # and SDK supported to test we are honoring it sdk-extension-aws-0: -r {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws/test-requirements-0.txt # and the latest version of OTel API and SDK sdk-extension-aws-1: {[testenv]test_deps} sdk-extension-aws-1: -r {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws/test-requirements-1.txt lint-sdk-extension-aws: -r {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws/test-requirements-0.txt benchmark-sdk-extension-aws: -r {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws/benchmark-requirements.txt resource-detector-containerid: {[testenv]test_deps} resource-detector-containerid: -r {toxinidir}/resource/opentelemetry-resource-detector-containerid/test-requirements.txt # packages that are released individually should provide a test-requirements.txt with the lowest version of OTel API # and SDK supported to test we are honoring it resource-detector-azure-0: -r {toxinidir}/resource/opentelemetry-resource-detector-azure/test-requirements-0.txt # and the latest version of OTel API and SDK resource-detector-azure-1: {[testenv]test_deps} resource-detector-azure-1: -r {toxinidir}/resource/opentelemetry-resource-detector-azure/test-requirements-1.txt lint-resource-detector-azure: -r {toxinidir}/resource/opentelemetry-resource-detector-azure/test-requirements-0.txt # packages that are released individually should provide a test-requirements.txt with the lowest version of OTel API # and SDK supported to test we are honoring it resourcedetector-gcp-0: -r {toxinidir}/resource/opentelemetry-resourcedetector-gcp/requirements.oldest.txt # and the latest version of OTel API and SDK resourcedetector-gcp-1: {[testenv]test_deps} resourcedetector-gcp-1: -r {toxinidir}/resource/opentelemetry-resourcedetector-gcp/requirements.latest.txt lint-resourcedetector-gcp: -r {toxinidir}/resource/opentelemetry-resourcedetector-gcp/requirements.oldest.txt propagator-ot-trace: {[testenv]test_deps} propagator-ot-trace: -r {toxinidir}/propagator/opentelemetry-propagator-ot-trace/test-requirements.txt # packages that are released individually should provide a test-requirements.txt with the lowest version of OTel API # and SDK supported to test we are honoring it propagator-aws-xray-0: -r {toxinidir}/propagator/opentelemetry-propagator-aws-xray/test-requirements-0.txt # and the latest version of OTel API and SDK propagator-aws-xray-1: {[testenv]test_deps} propagator-aws-xray-1: -r {toxinidir}/propagator/opentelemetry-propagator-aws-xray/test-requirements-1.txt lint-propagator-aws-xray: -r {toxinidir}/propagator/opentelemetry-propagator-aws-xray/test-requirements-0.txt benchmark-propagator-aws-xray: -r {toxinidir}/propagator/opentelemetry-propagator-aws-xray/benchmark-requirements.txt processor-baggage: {[testenv]test_deps} processor-baggage: -r {toxinidir}/processor/opentelemetry-processor-baggage/test-requirements.txt opamp-client: {[testenv]test_deps} opamp-client-lowest: -r {toxinidir}/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt opamp-client-latest: -r {toxinidir}/opamp/opentelemetry-opamp-client/test-requirements.latest.txt lint-opamp-client: -r {toxinidir}/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt util-http: {[testenv]test_deps} util-http: -r {toxinidir}/util/opentelemetry-util-http/test-requirements.txt util-http: {toxinidir}/util/opentelemetry-util-http exporter-credential-provider-gcp: -r {toxinidir}/exporter/opentelemetry-exporter-credential-provider-gcp/test-requirements.txt exporter-credential-provider-gcp: {toxinidir}/exporter/opentelemetry-exporter-credential-provider-gcp util-genai: {[testenv]test_deps} util-genai: -r {toxinidir}/util/opentelemetry-util-genai/test-requirements.txt util-genai: {toxinidir}/util/opentelemetry-util-genai ; FIXME: add coverage testing allowlist_externals = sh pytest setenv = ; override CORE_REPO_SHA via env variable when testing other branches/commits than main ; i.e: CORE_REPO_SHA=dde62cebffe519c35875af6d06fae053b3be65ec tox -e CORE_REPO_SHA={env:CORE_REPO_SHA:main} CORE_REPO=git+https://github.com/open-telemetry/opentelemetry-python.git@{env:CORE_REPO_SHA} CORE_REPO_API={env:CORE_REPO_API:{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api} CORE_REPO_SDK={env:CORE_REPO_SDK:{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk} CORE_REPO_SEMCONV={env:CORE_REPO_SEMCONV:{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions} CORE_REPO_TEST_UTILS={env:CORE_REPO_TEST_UTILS:{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils} UV_CONFIG_FILE={toxinidir}/tox-uv.toml PIP_CONSTRAINTS={toxinidir}/test-constraints.txt UV_BUILD_CONSTRAINT={toxinidir}/test-constraints.txt commands_pre = ; In order to get a health coverage report, ; we have to install packages in editable mode. coverage: python {toxinidir}/scripts/eachdist.py install --editable commands = test-distro: pytest {toxinidir}/opentelemetry-distro/tests {posargs} lint-distro: pylint {toxinidir}/opentelemetry-distro test-opentelemetry-instrumentation: pytest {toxinidir}/opentelemetry-instrumentation/tests {posargs} lint-opentelemetry-instrumentation: pylint {toxinidir}/opentelemetry-instrumentation test-instrumentation-aiohttp-client: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests {posargs} lint-instrumentation-aiohttp-client: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-aiohttp-client" test-instrumentation-aiohttp-server: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests {posargs} lint-instrumentation-aiohttp-server: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-aiohttp-server" test-instrumentation-aiopg: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg/tests {posargs} lint-instrumentation-aiopg: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-aiopg" test-instrumentation-asgi: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi/tests {posargs} lint-instrumentation-asgi: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-asgi" test-instrumentation-asyncclick: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncclick/tests {posargs} lint-instrumentation-asyncclick: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-asyncclick" test-instrumentation-asyncpg: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg/tests {posargs} lint-instrumentation-asyncpg: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-asyncpg" test-instrumentation-aws-lambda: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda/tests {posargs} lint-instrumentation-aws-lambda: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-aws-lambda" test-instrumentation-botocore: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/tests {posargs} lint-instrumentation-botocore: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-botocore" test-instrumentation-boto3sqs: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs/tests {posargs} lint-instrumentation-boto3sqs: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-boto3sqs" test-instrumentation-cassandra: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra/tests {posargs} lint-instrumentation-cassandra: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-cassandra" test-instrumentation-celery: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/tests {posargs} lint-instrumentation-celery: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-celery" test-instrumentation-click: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-click/tests {posargs} lint-instrumentation-click: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-click" test-instrumentation-dbapi: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi/tests {posargs} lint-instrumentation-dbapi: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-dbapi" test-instrumentation-django: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-django/tests {posargs} lint-instrumentation-django: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-django" test-instrumentation-falcon: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon/tests {posargs} lint-instrumentation-falcon: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-falcon" test-instrumentation-fastapi: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi/tests {posargs} lint-instrumentation-fastapi: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-fastapi" test-instrumentation-flask: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-flask/tests {posargs} lint-instrumentation-flask: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-flask" test-instrumentation-urllib: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib/tests {posargs} lint-instrumentation-urllib: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-urllib" test-instrumentation-urllib3: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3/tests {posargs} lint-instrumentation-urllib3: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-urllib3" benchmark-instrumentation-urllib3: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3/benchmarks {posargs} --benchmark-json=instrumentation-urllib3-benchmark.json test-instrumentation-grpc: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc/tests {posargs} lint-instrumentation-grpc: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-grpc" test-instrumentation-jinja2: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2/tests {posargs} lint-instrumentation-jinja2: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-jinja2" test-instrumentation-aiokafka: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-aiokafka/tests {posargs} lint-instrumentation-aiokafka: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-aiokafka" test-instrumentation-kafka-python: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python/tests {posargs} lint-instrumentation-kafka-python: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-kafka-python" test-instrumentation-confluent-kafka: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests {posargs} lint-instrumentation-confluent-kafka: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-confluent-kafka" test-instrumentation-logging: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-logging/tests {posargs} lint-instrumentation-logging: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-logging" benchmark-instrumentation-logging: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-logging/benchmarks {posargs} --benchmark-json=instrumentation-logging-benchmark.json test-instrumentation-exceptions: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-exceptions/tests {posargs} lint-instrumentation-exceptions: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-exceptions" test-instrumentation-mysql: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql/tests {posargs} lint-instrumentation-mysql: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-mysql" test-instrumentation-mysqlclient: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-mysqlclient/tests {posargs} lint-instrumentation-mysqlclient: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-mysqlclient" test-instrumentation-openai-v2: pytest {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests {posargs} lint-instrumentation-openai-v2: sh -c "cd instrumentation-genai && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-openai-v2" test-instrumentation-openai_agents-v2: pytest {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests {posargs} lint-instrumentation-openai_agents-v2: sh -c "cd instrumentation-genai && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-openai-agents-v2" test-instrumentation-vertexai: pytest {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests --vcr-record=none {posargs} lint-instrumentation-vertexai: sh -c "cd instrumentation-genai && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-vertexai" test-instrumentation-sio-pika: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-pika/tests {posargs} lint-instrumentation-sio-pika: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-pika" test-instrumentation-aio-pika: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika/tests {posargs} lint-instrumentation-aio-pika: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-aio-pika" test-instrumentation-psycopg: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg/tests {posargs} lint-instrumentation-psycopg: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-psycopg" test-instrumentation-psycopg2: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2/tests {posargs} lint-instrumentation-psycopg2: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-psycopg2" ; Test only for psycopg2-binary instrumentation as the only difference between psycopg2 and psycopg2-binary is the install method test-instrumentation-psycopg2-binary: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2/tests {posargs} test-instrumentation-pymemcache: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/tests {posargs} lint-instrumentation-pymemcache: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-pymemcache" test-instrumentation-pymongo: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo/tests {posargs} lint-instrumentation-pymongo: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-pymongo" test-instrumentation-pymysql: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql/tests {posargs} lint-instrumentation-pymysql: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-pymysql" test-instrumentation-pymssql: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-pymssql/tests {posargs} lint-instrumentation-pymssql: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-pymssql" test-instrumentation-pyramid: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid/tests {posargs} lint-instrumentation-pyramid: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-pyramid" test-instrumentation-redis: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-redis/tests {posargs} lint-instrumentation-redis: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-redis" test-instrumentation-remoulade: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade/tests {posargs} lint-instrumentation-remoulade: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-remoulade" test-instrumentation-requests: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-requests/tests {posargs} lint-instrumentation-requests: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-requests" benchmark-instrumentation-requests: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-requests/benchmarks {posargs} --benchmark-json=instrumentation-requests-benchmark.json test-instrumentation-sqlalchemy: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests {posargs} lint-instrumentation-sqlalchemy: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-sqlalchemy" test-instrumentation-sqlite3: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3/tests {posargs} lint-instrumentation-sqlite3: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-sqlite3" test-instrumentation-starlette: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette/tests {posargs} lint-instrumentation-starlette: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-starlette" test-instrumentation-structlog: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-structlog/tests {posargs} lint-instrumentation-structlog: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-structlog" test-instrumentation-system-metrics: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics/tests {posargs} lint-instrumentation-system-metrics: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-system-metrics" test-instrumentation-threading: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-threading/tests {posargs} lint-instrumentation-threading: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-threading" test-instrumentation-tornado: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado/tests {posargs} lint-instrumentation-tornado: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-tornado" test-instrumentation-tortoiseorm: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-tortoiseorm/tests {posargs} lint-instrumentation-tortoiseorm: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-tortoiseorm" test-instrumentation-wsgi: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi/tests {posargs} lint-instrumentation-wsgi: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-wsgi" test-instrumentation-httpx-{0,1}: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/tests {posargs} test-instrumentation-httpx-2: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx2_integration.py {posargs} test-instrumentation-httpx-3: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/tests {posargs} lint-instrumentation-httpx: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-httpx" test-instrumentation-asyncio: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncio/tests {posargs} lint-instrumentation-asyncio: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-asyncio" test-util-http: pytest {toxinidir}/util/opentelemetry-util-http/tests {posargs} lint-util-http: sh -c "cd util && pylint --rcfile ../.pylintrc opentelemetry-util-http" test-exporter-credential-provider-gcp: pytest {toxinidir}/exporter/opentelemetry-exporter-credential-provider-gcp/tests {posargs} lint-exporter-credential-provider-gcp: sh -c "cd exporter && pylint --rcfile ../.pylintrc opentelemetry-exporter-credential-provider-gcp" test-util-genai: pytest {toxinidir}/util/opentelemetry-util-genai/tests {posargs} lint-util-genai: sh -c "cd util && pylint --rcfile ../.pylintrc opentelemetry-util-genai" test-sdk-extension-aws: pytest {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws/tests {posargs} lint-sdk-extension-aws: sh -c "cd sdk-extension && pylint --rcfile ../.pylintrc opentelemetry-sdk-extension-aws" benchmark-sdk-extension-aws: pytest {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws/benchmarks {posargs} --benchmark-json=sdk-extension-aws-benchmark.json test-resource-detector-containerid: pytest {toxinidir}/resource/opentelemetry-resource-detector-containerid/tests {posargs} lint-resource-detector-containerid: sh -c "cd resource && pylint --rcfile ../.pylintrc opentelemetry-resource-detector-containerid" test-resource-detector-azure: pytest {toxinidir}/resource/opentelemetry-resource-detector-azure/tests {posargs} lint-resource-detector-azure: sh -c "cd resource && pylint --rcfile ../.pylintrc opentelemetry-resource-detector-azure" test-resourcedetector-gcp: pytest {toxinidir}/resource/opentelemetry-resourcedetector-gcp/tests {posargs} lint-resourcedetector-gcp: sh -c "cd resource && pylint --rcfile ../.pylintrc opentelemetry-resourcedetector-gcp" test-processor-baggage: pytest {toxinidir}/processor/opentelemetry-processor-baggage/tests {posargs} lint-processor-baggage: sh -c "cd processor && pylint --rcfile ../.pylintrc opentelemetry-processor-baggage" test-propagator-aws-xray: pytest {toxinidir}/propagator/opentelemetry-propagator-aws-xray/tests {posargs} lint-propagator-aws-xray: sh -c "cd propagator && pylint --rcfile ../.pylintrc opentelemetry-propagator-aws-xray" benchmark-propagator-aws-xray: pytest {toxinidir}/propagator/opentelemetry-propagator-aws-xray/benchmarks {posargs} --benchmark-json=propagator-aws-xray-benchmark.json test-propagator-ot-trace: pytest {toxinidir}/propagator/opentelemetry-propagator-ot-trace/tests {posargs} lint-propagator-ot-trace: sh -c "cd propagator && pylint --rcfile ../.pylintrc opentelemetry-propagator-ot-trace" test-exporter-richconsole: pytest {toxinidir}/exporter/opentelemetry-exporter-richconsole/tests {posargs} lint-exporter-richconsole: sh -c "cd exporter && pylint --rcfile ../.pylintrc opentelemetry-exporter-richconsole" test-exporter-prometheus-remote-write: pytest {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write/tests {posargs} lint-exporter-prometheus-remote-write: sh -c "cd exporter && pylint --rcfile ../.pylintrc opentelemetry-exporter-prometheus-remote-write" test-opamp-client: pytest {toxinidir}/opamp/opentelemetry-opamp-client/tests {posargs} lint-opamp-client: sh -c "cd opamp && pylint --rcfile ../.pylintrc opentelemetry-opamp-client" coverage: {toxinidir}/scripts/coverage.sh [testenv:docs] deps = -c {toxinidir}/dev-requirements.txt -r {toxinidir}/docs-requirements.txt pytest {[testenv]test_deps} {toxinidir}/opentelemetry-instrumentation {toxinidir}/util/opentelemetry-util-http {toxinidir}/util/opentelemetry-util-genai {toxinidir}/exporter/opentelemetry-exporter-credential-provider-gcp changedir = docs commands = sphinx-build -E -a -W -b html -T . _build/html [testenv:spellcheck] basepython: python3 recreate = True deps = codespell==2.2.6 commands = codespell [testenv:docker-tests] basepython: python3 deps = {[testenv]test_deps} -r {toxinidir}/tests/opentelemetry-docker-tests/tests/test-requirements.txt -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg -e {toxinidir}/instrumentation/opentelemetry-instrumentation-celery -e {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi -e {toxinidir}/instrumentation/opentelemetry-instrumentation-django -e {toxinidir}/instrumentation/opentelemetry-instrumentation-flask -e {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python -e {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql -e {toxinidir}/instrumentation/opentelemetry-instrumentation-mysqlclient -e {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg -e {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2 -e {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo -e {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql -e {toxinidir}/instrumentation/opentelemetry-instrumentation-pymssql -e {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg -e {toxinidir}/instrumentation/opentelemetry-instrumentation-redis -e {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/util/opentelemetry-util-http changedir = tests/opentelemetry-docker-tests/tests allowlist_externals = docker pytest commands_pre = docker --version || echo 'Docker with compose subcommand is required' && /bin/false python -c 'import pyodbc; print(pyodbc.drivers())' || echo 'You need to install unixODBC' && /bin/false docker compose up -d python check_availability.py commands = pytest {posargs} commands_post = docker compose down -v [testenv:generate] deps = -r {toxinidir}/gen-requirements.txt allowlist_externals = {toxinidir}/scripts/generate_instrumentation_bootstrap.py {toxinidir}/scripts/generate_instrumentation_readme.py {toxinidir}/scripts/generate_instrumentation_metapackage.py pytest commands = {toxinidir}/scripts/generate_instrumentation_bootstrap.py {toxinidir}/scripts/generate_instrumentation_readme.py {toxinidir}/scripts/generate_instrumentation_metapackage.py [testenv:lint-license-header-check] commands = python {toxinidir}/scripts/check_license_header.py [testenv:generate-workflows] recreate = True deps = {toxinidir}/.github/workflows/generate_workflows_lib commands = python {toxinidir}/.github/workflows/generate_workflows.py [testenv:shellcheck] commands_pre = sh -c "sudo apt update -y && sudo apt install --assume-yes shellcheck" commands = sh -c "find {toxinidir} -name \*.sh | xargs shellcheck --severity=warning" [testenv:{precommit,ruff}] basepython: python3 deps = -c {toxinidir}/dev-requirements.txt pre-commit commands = pre-commit run --color=always --all-files {posargs} [testenv:typecheck] deps = -c {toxinidir}/dev-requirements.txt pyright protobuf==6.31.1 {[testenv]test_deps} {toxinidir}/opentelemetry-instrumentation {toxinidir}/util/opentelemetry-util-http {toxinidir}/util/opentelemetry-util-genai[upload] {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-vertexai[instruments] {toxinidir}/instrumentation/opentelemetry-instrumentation-aiokafka[instruments] {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncclick[instruments] {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi[instruments] {toxinidir}/exporter/opentelemetry-exporter-credential-provider-gcp {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client[instruments] {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx[instruments-any] {toxinidir}/opamp/opentelemetry-opamp-client commands = pyright opentelemetry-python-contrib-0.66b2/typings/000077500000000000000000000000001527070072300212425ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/typings/fsspec/000077500000000000000000000000001527070072300225255ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/typings/fsspec/__init__.pyi000066400000000000000000000020161527070072300250060ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # # 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. """Handwritten stubs for fsspec usage in opentelemetry-util-genai""" import io from typing import Any, Literal from fsspec.spec import ( AbstractFileSystem as RealAbstractFileSystem, ) class AbstractFileSystem(RealAbstractFileSystem): def open(self, path: str, mode: Literal["w"], *args: Any, **kwargs: Any) -> io.TextIOWrapper: ... def exists(self, path: str) -> bool: ... def url_to_fs(url: str) -> tuple[AbstractFileSystem, str]: ... opentelemetry-python-contrib-0.66b2/util/000077500000000000000000000000001527070072300205225ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/000077500000000000000000000000001527070072300254525ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/.changelog/000077500000000000000000000000001527070072300274575ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/.changelog/.gitignore000066400000000000000000000000001527070072300314350ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/.changelog/4500.added000066400000000000000000000001221527070072300310250ustar00rootroot00000000000000Add shared sync and async stream wrapper base classes for GenAI instrumentations. opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/.changelog/4538.changed000066400000000000000000000002301527070072300313700ustar00rootroot00000000000000Change `InferenceInvocation` init params to only accept base params; pass in `attributes` on invocation `_start` so samplers have access to attributes. opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/.changelog/4553.changed000066400000000000000000000001071527070072300313700ustar00rootroot00000000000000Apply attribute for sampling on instantiation of all invocation types. opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/.changelog/4556.changed000066400000000000000000000001711527070072300313740ustar00rootroot00000000000000Minor code cleanup and changes in preparation of moving google's GenAI instrumentation library to use this util library. opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/AGENTS.md000066400000000000000000000124061527070072300267600ustar00rootroot00000000000000# GenAI Utils — Agent and Contributor Guidelines This package provides shared telemetry utilities for OpenTelemetry GenAI instrumentation. ## 1. Semantic Convention Compliance No new telemetry without semconv. If a signal, attribute, or operation is not in the [OpenTelemetry GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/), land the semconv change first. All attributes, operation names, and span names must match semconv. Use the semconv attribute modules — do not hardcode attribute name strings: - `gen_ai.*` attributes: `opentelemetry.semconv._incubating.attributes.gen_ai_attributes` - `server.*` attributes: `opentelemetry.semconv.attributes.server_attributes` - `error.*` attributes: `opentelemetry.semconv.attributes.error_attributes` - Other namespaces: use the corresponding module from `opentelemetry.semconv` Shared attributes should behave consistently across invocation types (same conditions, same defaults). If an attribute applies to more than one invocation per semconv, set it on all applicable ones. ## 2. Invocation Lifecycle Pattern Every new operation type must follow this pattern: ```python invocation = handler.start_inference(provider, request_model, server_address=..., server_port=...) invocation.temperature = ... try: response = client.call(...) invocation.response_model_name = response.model invocation.finish_reasons = response.finish_reasons invocation.stop() except Exception as exc: invocation.fail(exc) raise ``` Factory methods on `TelemetryHandler` (`handler.py`): - `start_inference(provider, request_model, *, server_address, server_port)` → `InferenceInvocation` - `start_embedding(provider, request_model, *, server_address, server_port)` → `EmbeddingInvocation` - `start_tool(name, *, arguments, tool_call_id, tool_type, tool_description)` → `ToolInvocation` - `start_workflow(name)` → `WorkflowInvocation` Context manager equivalents (`handler.inference()`, `handler.embedding()`, `handler.tool()`, `handler.workflow()`) are available when the span lifetime maps cleanly to a `with` block. `start_*()` factories must map 1:1 to distinct semconv operation types (inference, embeddings, tool execution, agent invocation, workflow invocation). Names must match the operation unambiguously — for example, `create_agent` and `invoke_agent` are different operations, so a single `start_agent()` would be ambiguous and is not acceptable. Add a new factory per operation instead. Factory names are Python-style singular verbs (`start_embedding`, `start_tool`); the op names they map to follow semconv (`embeddings`, `tool execution`, future operations). `start_*()` factories must accept all attributes that semconv marks as important for sampling decisions as parameters, so they are on the span at creation time. Attributes that are also marked required by semconv must be required parameters (no default value). Operation name is usually hardcoded in specific invocation and does not need to be passed. ### Anti-patterns **Never construct invocation types directly** (`InferenceInvocation(...)`, `ToolInvocation(...)`, etc.) in instrumentation or production code — direct construction skips span creation and context propagation, so all telemetry calls become no-ops. Always use `handler.start_*()`. ## 3. Exception Handling - When catching exceptions from the underlying library to record telemetry, always re-raise the original exception unmodified. - Do not raise new exceptions in telemetry code. ## 4. Performance Keep the hot path tight: - Avoid per-invocation allocations; do not accumulate state unboundedly. - Skip content capture when content capture is disabled. - Skip setting span-only attributes when the span is not recording. - Still record attributes that feed metrics — metric recording is independent of span sampling. ## 5. DRY Do not copy-paste logic across invocation types. Extract shared helpers. ## 6. Documentation - Docstrings for invocation types and span/event helpers must include a link to the corresponding operation in the semconv spec. - When adding or changing attributes, update the docstring to describe what is set and under what conditions (e.g., "set only when `server_address` is provided"). ## 7. Tests - Every new operation type or attribute change must have tests verifying the exact attribute names **and value types**, checked against the semconv spec. - Cover all paths: success (`invocation.stop()`), failure (`invocation.fail(exc)`), and any conditional attribute logic (e.g., attributes set only when optional fields are populated). - Tests live in `tests/` — follow existing patterns there. - Don't call internal API in tests when the public API is available. ## 8. Python API Conventions - Mark private modules with an underscore. Objects inside a private module should be prefixed with an underscore if they are not used outside that module. - When adding fields or methods on invocation types (or anywhere in the public surface), push back hard: does this need to be public? If instrumentations don't need it, keep it internal (`_`-prefixed). Every public addition becomes a back-compat commitment. - Before removing or renaming an object exposed publicly, deprecate it first with a note in the docstring pointing to the replacement. opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/CHANGELOG.md000066400000000000000000000140001527070072300272560ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Version 0.4b0 (2026-05-01) - Add `AgentInvocation` type with `invoke_agent` span lifecycle ([#4274](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4274)) - Add metrics support for EmbeddingInvocation ([#4377](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4377)) - Add support for workflow in genAI utils handler. ([#4366](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4366)) - Enrich ToolCall type, breaking change: usage of ToolCall class renamed to ToolCallRequest ([#4218](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4218)) - Add EmbeddingInvocation span lifecycle support ([#4219](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4219)) - Populate schema_url on metrics ([#4320](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4320)) - Add workflow invocation type to genAI utils ([#4310](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4310)) - Check if upload works at startup in initializer of the `UploadCompletionHook`, instead of repeatedly failing on every upload ([#4390](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4390)). - Refactor public API: add factory methods (`start_inference`, `start_embedding`, `start_tool`, `start_workflow`) and invocation-owned lifecycle (`invocation.stop()` / `invocation.fail(exc)`); rename `LLMInvocation` → `InferenceInvocation` and `ToolCall` → `ToolInvocation`. Existing usages remain fully functional via deprecated aliases. ([#4391](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4391)) - `TelemetryHandler` now accepts a `completion_hook` parameter and calls it after each LLM invocation, passing inputs, outputs, the active span, and the log record. Content capture is enabled automatically when a real hook is configured. ([#4315](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4315)) - Add metrics to ToolInvocations ([#4443](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4443)) - Wrap completion hooks loaded via `load_completion_hook` so exceptions raised by `on_completion` are logged and swallowed instead of propagating to instrumentation call sites. ## Version 0.3b0 (2026-02-20) - Add `gen_ai.tool_definitions` to completion hook ([#4181](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4181)) - Add support for emitting inference events and enrich message types. ([#3994](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3994)) - Add support for `server.address`, `server.port` on all signals and additional metric-only attributes ([#4069](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4069)) - Log error when `fsspec` fails to be imported instead of silently failing ([#4037](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4037)). - Minor change to check LRU cache in Completion Hook before acquiring semaphore/thread ([#3907](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3907)). - Add environment variable for genai upload hook queue size ([#3943](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3943)) - Add more Semconv attributes to LLMInvocation spans. ([#3862](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3862)) - Limit the upload hook thread pool to 64 workers ([#3944](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3944)) - Add metrics to LLMInvocation traces ([#3891](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3891)) - Add parent class genAI invocation ([#3889](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3889)) ## Version 0.2b0 (2025-10-14) - Add jsonlines support to fsspec uploader ([#3791](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3791)) - Rename "fsspec_upload" entry point and classes to more generic "upload" ([#3798](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3798)) - Record content-type and use canonical paths in fsspec genai uploader ([#3795](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3795)) - Make inputs / outputs / system instructions optional params to `on_completion`, ([#3802](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3802)). - Use a SHA256 hash of the system instructions as it's upload filename, and check if the file exists before re-uploading it, ([#3814](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3814)). ## Version 0.1b0 (2025-09-25) - Add completion hook to genai utils to implement semconv v1.37. Includes a hook implementation using [`fsspec`](https://filesystem-spec.readthedocs.io/en/latest/) to support uploading to various pluggable backends. ([#3780](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3780)) ([#3752](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3752)) ([#3759](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3759)) ([#3763](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3763)) - Add a utility to parse the `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable. Add `gen_ai_latest_experimental` as a new value to the Sem Conv stability flag ([#3716](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3716)). ### Added - Generate Spans for LLM invocations - Helper functions for starting and finishing LLM invocations opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/CLAUDE.md000077700000000000000000000000001527070072300302272AGENTS.mdustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/LICENSE000066400000000000000000000261351527070072300264660ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/README.rst000066400000000000000000000137571527070072300271560ustar00rootroot00000000000000OpenTelemetry Util for GenAI ============================ .. note:: This package is being migrated to `opentelemetry-python-genai/util/opentelemetry-util-genai `_. Future development will happen there. The GenAI Utils package provides boilerplate and helpers to standardize instrumentation for Generative AI. It offers APIs to minimize the work needed to instrument GenAI libraries, while providing standardization for generating spans, metrics, and events. Key Components -------------- - ``TelemetryHandler`` -- manages LLM invocation lifecycles (spans, metrics, events) - ``InferenceInvocation`` and message types (``Text``, ``Reasoning``, ``Blob``, etc.) -- structured data model for GenAI interactions - ``CompletionHook`` -- protocol for uploading content to external storage (built-in ``fsspec`` support) - Metrics -- ``gen_ai.client.operation.duration`` and ``gen_ai.client.token.usage`` histograms Usage ----- See the module docstring in ``opentelemetry.util.genai.handler`` for usage examples, including context manager and manual lifecycle patterns. Environment Variables --------------------- This package relies on environment variables to configure capturing of message content. By default, message content will not be captured. Set the environment variable ``OTEL_SEMCONV_STABILITY_OPT_IN`` to ``gen_ai_latest_experimental`` to enable experimental features. Set the environment variable ``OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`` to one of: - ``NO_CONTENT``: Do not capture message content (default). - ``SPAN_ONLY``: Capture message content in spans only. - ``EVENT_ONLY``: Capture message content in events only. - ``SPAN_AND_EVENT``: Capture message content in both spans and events. To control event emission, you can optionally set ``OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT`` to ``true`` or ``false`` (case-insensitive). This variable controls whether to emit ``gen_ai.client.inference.operation.details`` events. If not explicitly set, the default value is automatically determined by ``OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT``: - When ``NO_CONTENT`` or ``SPAN_ONLY`` is set: defaults to ``false`` - When ``EVENT_ONLY`` or ``SPAN_AND_EVENT`` is set: defaults to ``true`` If explicitly set, the user's value takes precedence over the default. When ``EVENT_ONLY`` or ``SPAN_AND_EVENT`` mode is enabled and a LoggerProvider is configured, the package also emits ``gen_ai.client.inference.operation.details`` events with structured message content (as dictionaries instead of JSON strings). Note that when using ``EVENT_ONLY`` or ``SPAN_AND_EVENT``, the ``OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT`` environment variable defaults to ``true``, so events will be emitted automatically unless explicitly set to ``false``. Completion Hook / Upload ^^^^^^^^^^^^^^^^^^^^^^^^ - ``OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK``: Name of the completion hook entry point to load (e.g. ``upload``). - ``OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH``: An ``fsspec``-compatible URI/path for uploading prompts and completions (e.g. ``/path/to/prompts`` or ``gs://my_bucket``). Required when using the ``upload`` hook. - ``OTEL_INSTRUMENTATION_GENAI_UPLOAD_FORMAT``: Format for uploaded data -- ``json`` (default) or ``jsonl``. - ``OTEL_INSTRUMENTATION_GENAI_UPLOAD_MAX_QUEUE_SIZE``: Maximum number of concurrent uploads to queue (default: ``20``). Span Attributes --------------- This package sets the following span attributes on LLM invocations: **Common attributes:** - ``gen_ai.operation.name``: Str(chat) - ``gen_ai.provider.name``: Str(openai) - ``gen_ai.request.model``: Str(gpt-4o) - ``server.address``: Str(api.openai.com) - ``server.port``: Int(443) **Response attributes:** - ``gen_ai.response.finish_reasons``: Slice(["stop"]) - ``gen_ai.response.model``: Str(gpt-4o-2024-05-13) - ``gen_ai.response.id``: Str(chatcmpl-Bz8yrvPnydD9pObv625n2CGBPHS13) - ``gen_ai.usage.input_tokens``: Int(24) - ``gen_ai.usage.output_tokens``: Int(7) **Request parameter attributes (when provided):** - ``gen_ai.request.temperature``: Float(0.7) - ``gen_ai.request.top_p``: Float(1.0) - ``gen_ai.request.frequency_penalty``: Float(0.0) - ``gen_ai.request.presence_penalty``: Float(0.0) - ``gen_ai.request.max_tokens``: Int(1024) - ``gen_ai.request.stop_sequences``: Slice(["\\n"]) - ``gen_ai.request.seed``: Int(42) **Content attributes (sensitive, requires content capturing enabled):** - ``gen_ai.input.messages``: Str('[{"role": "user", "parts": [{"content": "hello world", "type": "text"}]}]') - ``gen_ai.output.messages``: Str('[{"role": "assistant", "parts": [{"content": "hello back", "type": "text"}], "finish_reason": "stop"}]') - ``gen_ai.system_instructions``: Str('[{"content": "You are a helpful assistant.", "type": "text"}]') **Error attributes:** - ``error.type``: Str(TimeoutError) Embedding Span Attributes ------------------------- This package also supports embedding invocation spans via the ``embedding`` context manager. For embedding invocations, the following attributes are set: **Common attributes:** - ``gen_ai.operation.name``: Str(embeddings) - ``gen_ai.provider.name``: Str(openai) - ``server.address``: Str(api.openai.com) - ``server.port``: Int(443) **Request attributes:** - ``gen_ai.request.model``: Str(text-embedding-3-small) - ``gen_ai.embeddings.dimension.count``: Int(1536) - ``gen_ai.request.encoding_formats``: Slice(["float"]) **Response attributes:** - ``gen_ai.response.model``: Str(text-embedding-3-small) - ``gen_ai.usage.input_tokens``: Int(24) Installation ------------ :: pip install opentelemetry-util-genai For upload support (requires ``fsspec``):: pip install opentelemetry-util-genai[upload] Design Document --------------- The design document for the OpenTelemetry GenAI Utils can be found at: `Design Document `_ References ---------- * `OpenTelemetry Project `_ opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/pyproject.toml000066400000000000000000000044261527070072300303740ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-util-genai" dynamic = ["version"] description = "OpenTelemetry GenAI Utils" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "opentelemetry-instrumentation ~= 0.61b0", "opentelemetry-semantic-conventions ~= 0.61b0", "opentelemetry-api ~= 1.40", "wrapt >= 1.0.0, < 3.0.0", ] [project.entry-points.opentelemetry_genai_completion_hook] upload = "opentelemetry.util.genai._upload:upload_completion_hook" [project.optional-dependencies] test = ["pytest>=7.0.0"] upload = ["fsspec>=2025.9.0"] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/util/opentelemetry-util-genai" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/util/genai/version.py" [tool.hatch.build.targets.sdist] include = ["/src", "/tests"] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] [tool.towncrier] directory = ".changelog" filename = "CHANGELOG.md" start_string = "\n" template = "../../scripts/changelog_template.j2" issue_format = "[#{issue}](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/{issue})" wrap = true issue_pattern = "^(\\d+)" [[tool.towncrier.type]] directory = "added" name = "Added" showcontent = true [[tool.towncrier.type]] directory = "changed" name = "Changed" showcontent = true [[tool.towncrier.type]] directory = "deprecated" name = "Deprecated" showcontent = true [[tool.towncrier.type]] directory = "removed" name = "Removed" showcontent = true [[tool.towncrier.type]] directory = "fixed" name = "Fixed" showcontent = true opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/000077500000000000000000000000001527070072300262415ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/000077500000000000000000000000001527070072300311355ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/000077500000000000000000000000001527070072300321125ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai/000077500000000000000000000000001527070072300331755ustar00rootroot00000000000000__init__.py000066400000000000000000000001141527070072300352230ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _agent_invocation.py000066400000000000000000000221301527070072300371540ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from typing import Any from opentelemetry._logs import Logger from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAI, ) from opentelemetry.semconv.attributes import server_attributes from opentelemetry.trace import SpanKind, Tracer from opentelemetry.util.genai._invocation import ( Error, GenAIInvocation, get_content_attributes, ) from opentelemetry.util.genai.completion_hook import CompletionHook from opentelemetry.util.genai.metrics import InvocationMetricsRecorder from opentelemetry.util.genai.types import ( InputMessage, MessagePart, OutputMessage, ToolDefinition, ) # TODO: Migrate to GenAI constants once available in semconv package _GEN_AI_AGENT_VERSION = "gen_ai.agent.version" _GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS = "gen_ai.usage.cache_creation.input_tokens" _GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.cache_read.input_tokens" class AgentInvocation(GenAIInvocation): """Represents a single agent invocation (invoke_agent span). Use handler.start_invoke_local_agent() / handler.start_invoke_remote_agent() or the handler.invoke_local_agent() / handler.invoke_remote_agent() context managers rather than constructing this directly. Reference: Client span: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-agent-spans.md#invoke-agent-client-span Internal span: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-agent-spans.md#invoke-agent-internal-span """ def __init__( self, tracer: Tracer, metrics_recorder: InvocationMetricsRecorder, logger: Logger, completion_hook: CompletionHook, provider: str, *, span_kind: SpanKind = SpanKind.INTERNAL, request_model: str | None = None, server_address: str | None = None, server_port: int | None = None, agent_name: str | None = None, ) -> None: """Use handler.start_invoke_local_agent() or handler.start_invoke_remote_agent() instead of calling this directly.""" _operation_name = GenAI.GenAiOperationNameValues.INVOKE_AGENT.value # pyright: ignore[reportDeprecated] super().__init__( tracer, metrics_recorder, logger, completion_hook, operation_name=_operation_name, span_name=f"{_operation_name} {agent_name}" if agent_name else _operation_name, span_kind=span_kind, ) self.provider = provider self.request_model = request_model self.server_address = server_address self.server_port = server_port self.agent_name: str | None = agent_name self.agent_id: str | None = None self.agent_description: str | None = None self.agent_version: str | None = None self.conversation_id: str | None = None self.data_source_id: str | None = None self.output_type: str | None = None self.temperature: float | None = None self.top_p: float | None = None self.frequency_penalty: float | None = None self.presence_penalty: float | None = None self.max_tokens: int | None = None self.stop_sequences: list[str] | None = None self.seed: int | None = None self.choice_count: int | None = None self.finish_reasons: list[str] | None = None self.input_tokens: int | None = None self.output_tokens: int | None = None self.cache_creation_input_tokens: int | None = None self.cache_read_input_tokens: int | None = None self.input_messages: list[InputMessage] = [] self.output_messages: list[OutputMessage] = [] self.system_instruction: list[MessagePart] = [] self.tool_definitions: list[ToolDefinition] | None = None self._start(self._get_base_attributes()) def _get_base_attributes(self) -> dict[str, Any]: """Return sampling-relevant attributes available at span creation time.""" optional_attrs = ( (GenAI.GEN_AI_REQUEST_MODEL, self.request_model), (GenAI.GEN_AI_AGENT_NAME, self.agent_name), (server_attributes.SERVER_ADDRESS, self.server_address), (server_attributes.SERVER_PORT, self.server_port), ) return { GenAI.GEN_AI_OPERATION_NAME: self._operation_name, GenAI.GEN_AI_PROVIDER_NAME: self.provider, **{k: v for k, v in optional_attrs if v is not None}, } def _get_common_attributes(self) -> dict[str, Any]: optional_attrs = ( (GenAI.GEN_AI_REQUEST_MODEL, self.request_model), (server_attributes.SERVER_ADDRESS, self.server_address), (server_attributes.SERVER_PORT, self.server_port), (GenAI.GEN_AI_AGENT_NAME, self.agent_name), (GenAI.GEN_AI_AGENT_ID, self.agent_id), (GenAI.GEN_AI_AGENT_DESCRIPTION, self.agent_description), (_GEN_AI_AGENT_VERSION, self.agent_version), ) return { GenAI.GEN_AI_OPERATION_NAME: self._operation_name, GenAI.GEN_AI_PROVIDER_NAME: self.provider, **{k: v for k, v in optional_attrs if v is not None}, } def _get_request_attributes(self) -> dict[str, Any]: optional_attrs = ( (GenAI.GEN_AI_CONVERSATION_ID, self.conversation_id), (GenAI.GEN_AI_DATA_SOURCE_ID, self.data_source_id), (GenAI.GEN_AI_OUTPUT_TYPE, self.output_type), (GenAI.GEN_AI_REQUEST_TEMPERATURE, self.temperature), (GenAI.GEN_AI_REQUEST_TOP_P, self.top_p), (GenAI.GEN_AI_REQUEST_FREQUENCY_PENALTY, self.frequency_penalty), (GenAI.GEN_AI_REQUEST_PRESENCE_PENALTY, self.presence_penalty), (GenAI.GEN_AI_REQUEST_MAX_TOKENS, self.max_tokens), (GenAI.GEN_AI_REQUEST_STOP_SEQUENCES, self.stop_sequences), (GenAI.GEN_AI_REQUEST_SEED, self.seed), (GenAI.GEN_AI_REQUEST_CHOICE_COUNT, self.choice_count), ) return {k: v for k, v in optional_attrs if v is not None} def _get_response_attributes(self) -> dict[str, Any]: if self.finish_reasons: return {GenAI.GEN_AI_RESPONSE_FINISH_REASONS: self.finish_reasons} return {} def _get_usage_attributes(self) -> dict[str, Any]: optional_attrs = ( (GenAI.GEN_AI_USAGE_INPUT_TOKENS, self.input_tokens), (GenAI.GEN_AI_USAGE_OUTPUT_TOKENS, self.output_tokens), ( _GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, self.cache_creation_input_tokens, ), ( _GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, self.cache_read_input_tokens, ), ) return {k: v for k, v in optional_attrs if v is not None} def _get_content_attributes_for_span(self) -> dict[str, Any]: return get_content_attributes( input_messages=self.input_messages, output_messages=self.output_messages, system_instruction=self.system_instruction, tool_definitions=self.tool_definitions, for_span=True, ) def _get_metric_attributes(self) -> dict[str, Any]: optional_attrs = ( (GenAI.GEN_AI_PROVIDER_NAME, self.provider), (GenAI.GEN_AI_REQUEST_MODEL, self.request_model), (server_attributes.SERVER_ADDRESS, self.server_address), (server_attributes.SERVER_PORT, self.server_port), ) attrs: dict[str, Any] = { GenAI.GEN_AI_OPERATION_NAME: self._operation_name, **{k: v for k, v in optional_attrs if v is not None}, } attrs.update(self.metric_attributes) return attrs def _get_metric_token_counts(self) -> dict[str, int]: counts: dict[str, int] = {} if self.input_tokens is not None: counts[GenAI.GenAiTokenTypeValues.INPUT.value] = self.input_tokens # pyright: ignore[reportDeprecated] if self.output_tokens is not None: counts[GenAI.GenAiTokenTypeValues.OUTPUT.value] = ( # pyright: ignore[reportDeprecated] self.output_tokens ) return counts def _apply_finish(self, error: Error | None = None) -> None: if error is not None: self._apply_error_attributes(error) attributes: dict[str, Any] = {} attributes.update(self._get_common_attributes()) attributes.update(self._get_request_attributes()) attributes.update(self._get_response_attributes()) attributes.update(self._get_usage_attributes()) attributes.update(self._get_content_attributes_for_span()) attributes.update(self.attributes) self.span.set_attributes(attributes) self._call_completion_hook( inputs=self.input_messages, outputs=self.output_messages, system_instruction=self.system_instruction, tool_definitions=self.tool_definitions, ) self._metrics_recorder.record(self) _embedding_invocation.py000066400000000000000000000114241527070072300400000ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from typing import Any from opentelemetry._logs import Logger from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAI, ) from opentelemetry.semconv.attributes import server_attributes from opentelemetry.trace import SpanKind, Tracer from opentelemetry.util.genai._invocation import Error, GenAIInvocation from opentelemetry.util.genai.completion_hook import CompletionHook from opentelemetry.util.genai.metrics import InvocationMetricsRecorder from opentelemetry.util.types import AttributeValue class EmbeddingInvocation(GenAIInvocation): """Represents a single embedding model invocation. Use handler.start_embedding(provider) or the handler.embedding(provider) context manager rather than constructing this directly. """ def __init__( self, tracer: Tracer, metrics_recorder: InvocationMetricsRecorder, logger: Logger, completion_hook: CompletionHook, provider: str, *, request_model: str | None = None, server_address: str | None = None, server_port: int | None = None, ) -> None: """Use handler.start_embedding(provider) or handler.embedding(provider) instead of calling this directly.""" _operation_name = GenAI.GenAiOperationNameValues.EMBEDDINGS.value # pyright: ignore[reportDeprecated] super().__init__( tracer, metrics_recorder, logger, completion_hook, operation_name=_operation_name, span_name=f"{_operation_name} {request_model}" if request_model else _operation_name, span_kind=SpanKind.CLIENT, ) self.provider = provider # e.g., azure.ai.openai, openai, aws.bedrock self.request_model = request_model self.server_address = server_address self.server_port = server_port # encoding_formats can be multi-value -> combinational cardinality risk. # Keep on spans/events only. self.encoding_formats: list[str] | None = None self.input_tokens: int | None = None self.dimension_count: int | None = None self.response_model_name: str | None = None self._start(self._get_base_attributes()) def _get_base_attributes(self) -> dict[str, Any]: """Return sampling-relevant attributes available at span creation time.""" optional_attrs = ( (GenAI.GEN_AI_REQUEST_MODEL, self.request_model), (GenAI.GEN_AI_PROVIDER_NAME, self.provider), (server_attributes.SERVER_ADDRESS, self.server_address), (server_attributes.SERVER_PORT, self.server_port), ) return { GenAI.GEN_AI_OPERATION_NAME: self._operation_name, **{k: v for k, v in optional_attrs if v is not None}, } def _get_metric_attributes(self) -> dict[str, Any]: optional_attrs = ( (GenAI.GEN_AI_PROVIDER_NAME, self.provider), (GenAI.GEN_AI_REQUEST_MODEL, self.request_model), (GenAI.GEN_AI_RESPONSE_MODEL, self.response_model_name), (server_attributes.SERVER_ADDRESS, self.server_address), (server_attributes.SERVER_PORT, self.server_port), ) attrs: dict[str, AttributeValue] = { GenAI.GEN_AI_OPERATION_NAME: self._operation_name, **{k: v for k, v in optional_attrs if v is not None}, } attrs.update(self.metric_attributes) return attrs def _get_metric_token_counts(self) -> dict[str, int]: if self.input_tokens is not None: return {GenAI.GenAiTokenTypeValues.INPUT.value: self.input_tokens} # pyright: ignore[reportDeprecated] return {} def _apply_finish(self, error: Error | None = None) -> None: optional_attrs = ( (GenAI.GEN_AI_PROVIDER_NAME, self.provider), (server_attributes.SERVER_ADDRESS, self.server_address), (server_attributes.SERVER_PORT, self.server_port), (GenAI.GEN_AI_REQUEST_MODEL, self.request_model), (GenAI.GEN_AI_EMBEDDINGS_DIMENSION_COUNT, self.dimension_count), (GenAI.GEN_AI_REQUEST_ENCODING_FORMATS, self.encoding_formats), (GenAI.GEN_AI_RESPONSE_MODEL, self.response_model_name), (GenAI.GEN_AI_USAGE_INPUT_TOKENS, self.input_tokens), ) attributes: dict[str, Any] = { GenAI.GEN_AI_OPERATION_NAME: self._operation_name, **{key: value for key, value in optional_attrs if value is not None}, } if error is not None: self._apply_error_attributes(error) attributes.update(self.attributes) self.span.set_attributes(attributes) self._metrics_recorder.record(self) _inference_invocation.py000066400000000000000000000324441527070072300400250ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from dataclasses import dataclass, field from typing import Any from opentelemetry._logs import Logger, LogRecord from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAI, ) from opentelemetry.semconv.attributes import server_attributes from opentelemetry.trace import INVALID_SPAN, Span, SpanKind, Tracer from opentelemetry.util.genai._invocation import ( Error, GenAIInvocation, get_content_attributes, ) from opentelemetry.util.genai.completion_hook import CompletionHook from opentelemetry.util.genai.metrics import InvocationMetricsRecorder from opentelemetry.util.genai.types import ( InputMessage, MessagePart, OutputMessage, ToolDefinition, ) from opentelemetry.util.genai.utils import ( is_experimental_mode, should_emit_event, ) # TODO: Migrate to GenAI constants once available in semconv package _GEN_AI_REASONING_OUTPUT_TOKENS = "gen_ai.usage.reasoning.output_tokens" class InferenceInvocation(GenAIInvocation): """Represents a single LLM chat/completion call. Use handler.start_inference(provider) or the handler.inference(provider) context manager rather than constructing this directly. """ def __init__( self, tracer: Tracer, metrics_recorder: InvocationMetricsRecorder, logger: Logger, completion_hook: CompletionHook, provider: str, *, request_model: str | None = None, server_address: str | None = None, server_port: int | None = None, operation_name: str | None = None, ) -> None: operation_name = ( operation_name or GenAI.GenAiOperationNameValues.CHAT.value # pyright: ignore[reportDeprecated] ) """Use handler.start_inference(provider) or handler.inference(provider) instead of calling this directly.""" super().__init__( tracer, metrics_recorder, logger, completion_hook, operation_name=operation_name, span_name=f"{operation_name} {request_model}" if request_model else operation_name, span_kind=SpanKind.CLIENT, ) self.provider = provider self.request_model = request_model self.server_address = server_address self.server_port = server_port self.input_messages: list[InputMessage] = [] self.output_messages: list[OutputMessage] = [] self.system_instruction: list[MessagePart] = [] self.response_model_name: str | None = None self.response_id: str | None = None self.finish_reasons: list[str] | None = None self.input_tokens: int | None = None # Output tokens will ultimately be the sum of normal output tokens and thinking tokens. self.output_tokens: int | None = None self.thinking_tokens: int | None = None self.temperature: float | None = None self.top_p: float | None = None self.frequency_penalty: float | None = None self.presence_penalty: float | None = None self.max_tokens: int | None = None self.stop_sequences: list[str] | None = None self.seed: int | None = None self.cache_creation_input_tokens: int | None = None self.cache_read_input_tokens: int | None = None self.tool_definitions: list[ToolDefinition] | None = None self._start(self._get_base_attributes()) def _get_message_attributes(self, *, for_span: bool) -> dict[str, Any]: return get_content_attributes( input_messages=self.input_messages, output_messages=self.output_messages, system_instruction=self.system_instruction, tool_definitions=self.tool_definitions, for_span=for_span, ) def _get_finish_reasons(self) -> list[str] | None: if self.finish_reasons is not None: return self.finish_reasons or None if self.output_messages: reasons = [msg.finish_reason for msg in self.output_messages if msg.finish_reason] return reasons or None return None def _get_base_attributes(self) -> dict[str, Any]: optional_attrs = ( (GenAI.GEN_AI_REQUEST_MODEL, self.request_model), (GenAI.GEN_AI_PROVIDER_NAME, self.provider), (server_attributes.SERVER_ADDRESS, self.server_address), (server_attributes.SERVER_PORT, self.server_port), ) return { GenAI.GEN_AI_OPERATION_NAME: self._operation_name, **{k: v for k, v in optional_attrs if v is not None}, } def _get_attributes(self) -> dict[str, Any]: attrs = self._get_base_attributes() if self.output_tokens is None and self.thinking_tokens is None: output_tokens = None else: output_tokens = (self.output_tokens or 0) + (self.thinking_tokens or 0) optional_attrs = ( (GenAI.GEN_AI_REQUEST_TEMPERATURE, self.temperature), (GenAI.GEN_AI_REQUEST_TOP_P, self.top_p), (GenAI.GEN_AI_REQUEST_FREQUENCY_PENALTY, self.frequency_penalty), (GenAI.GEN_AI_REQUEST_PRESENCE_PENALTY, self.presence_penalty), (GenAI.GEN_AI_REQUEST_MAX_TOKENS, self.max_tokens), (GenAI.GEN_AI_REQUEST_STOP_SEQUENCES, self.stop_sequences), (GenAI.GEN_AI_REQUEST_SEED, self.seed), (GenAI.GEN_AI_RESPONSE_FINISH_REASONS, self._get_finish_reasons()), (GenAI.GEN_AI_RESPONSE_MODEL, self.response_model_name), (GenAI.GEN_AI_RESPONSE_ID, self.response_id), (GenAI.GEN_AI_USAGE_INPUT_TOKENS, self.input_tokens), (GenAI.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens), ( GenAI.GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, self.cache_creation_input_tokens, ), ( GenAI.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, self.cache_read_input_tokens, ), ( _GEN_AI_REASONING_OUTPUT_TOKENS, self.thinking_tokens, ), ) attrs.update({k: v for k, v in optional_attrs if v is not None}) return attrs def _get_metric_attributes(self) -> dict[str, Any]: attrs = self._get_base_attributes() if self.response_model_name is not None: attrs[GenAI.GEN_AI_RESPONSE_MODEL] = self.response_model_name attrs.update(self.metric_attributes) return attrs def _get_metric_token_counts(self) -> dict[str, int]: counts: dict[str, int] = {} if self.input_tokens is not None: counts[GenAI.GenAiTokenTypeValues.INPUT.value] = self.input_tokens # pyright: ignore[reportDeprecated] if self.output_tokens is not None: counts[GenAI.GenAiTokenTypeValues.OUTPUT.value] = ( # pyright: ignore[reportDeprecated] self.output_tokens ) return counts def _apply_finish(self, error: Error | None = None) -> None: if error is not None: self._apply_error_attributes(error) attributes = self._get_attributes() attributes.update(self._get_message_attributes(for_span=True)) attributes.update(self.attributes) self.span.set_attributes(attributes) self._metrics_recorder.record(self) log_record = self._maybe_create_event() self._call_completion_hook( inputs=self.input_messages, outputs=self.output_messages, system_instruction=self.system_instruction, tool_definitions=self.tool_definitions, log_record=log_record, ) if log_record is not None: self._logger.emit(log_record) def _maybe_create_event(self) -> LogRecord | None: """Emit a gen_ai.client.inference.operation.details event. For more details, see the semantic convention documentation: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-events.md#event-eventgen_aiclientinferenceoperationdetails """ if not is_experimental_mode() or not should_emit_event(): return None attributes = self._get_attributes() attributes.update(self._get_message_attributes(for_span=False)) attributes.update(self.attributes) return LogRecord( event_name="gen_ai.client.inference.operation.details", attributes=attributes, context=self._span_context, ) @dataclass class LLMInvocation: """Deprecated. Use InferenceInvocation instead. Data container for an LLM invocation. Pass to handler.start_llm() to start the span, then update fields and call handler.stop_llm() or handler.fail_llm(). """ request_model: str | None = None input_messages: list[InputMessage] = field(default_factory=list) # pyright: ignore[reportUnknownVariableType] output_messages: list[OutputMessage] = field(default_factory=list) # pyright: ignore[reportUnknownVariableType] system_instruction: list[MessagePart] = field(default_factory=list) # pyright: ignore[reportUnknownVariableType] provider: str | None = None response_model_name: str | None = None response_id: str | None = None finish_reasons: list[str] | None = None input_tokens: int | None = None output_tokens: int | None = None cache_creation_input_tokens: int | None = None cache_read_input_tokens: int | None = None attributes: dict[str, Any] = field(default_factory=dict) # pyright: ignore[reportUnknownVariableType] """Additional attributes to set on spans and/or events. Not set on metrics.""" metric_attributes: dict[str, Any] = field(default_factory=dict) # pyright: ignore[reportUnknownVariableType] """Additional attributes to set on metrics. Must be low cardinality. Not set on spans or events.""" temperature: float | None = None top_p: float | None = None frequency_penalty: float | None = None presence_penalty: float | None = None max_tokens: int | None = None stop_sequences: list[str] | None = None seed: int | None = None server_address: str | None = None server_port: int | None = None _inference_invocation: InferenceInvocation | None = field(default=None, init=False, repr=False) def _start_with_handler( self, tracer: Tracer, metrics_recorder: InvocationMetricsRecorder, logger: Logger, completion_hook: CompletionHook, ) -> None: """Create and start an InferenceInvocation from this data container. Called by handler.start_llm().""" inv = InferenceInvocation( tracer, metrics_recorder, logger, completion_hook, self.provider or "", request_model=self.request_model, server_address=self.server_address, server_port=self.server_port, ) inv.input_messages = self.input_messages inv.output_messages = self.output_messages inv.system_instruction = self.system_instruction inv.response_model_name = self.response_model_name inv.response_id = self.response_id inv.finish_reasons = self.finish_reasons inv.input_tokens = self.input_tokens inv.output_tokens = self.output_tokens inv.cache_creation_input_tokens = self.cache_creation_input_tokens inv.cache_read_input_tokens = self.cache_read_input_tokens inv.temperature = self.temperature inv.top_p = self.top_p inv.frequency_penalty = self.frequency_penalty inv.presence_penalty = self.presence_penalty inv.max_tokens = self.max_tokens inv.stop_sequences = self.stop_sequences inv.seed = self.seed inv.attributes.update(self.attributes) inv.metric_attributes.update(self.metric_attributes) self._inference_invocation = inv def _sync_to_invocation(self) -> None: inv = self._inference_invocation if inv is None: return inv.provider = self.provider or "" inv.request_model = self.request_model inv.input_messages = self.input_messages inv.output_messages = self.output_messages inv.system_instruction = self.system_instruction inv.response_model_name = self.response_model_name inv.response_id = self.response_id inv.finish_reasons = self.finish_reasons inv.input_tokens = self.input_tokens inv.output_tokens = self.output_tokens inv.cache_creation_input_tokens = self.cache_creation_input_tokens inv.cache_read_input_tokens = self.cache_read_input_tokens inv.temperature = self.temperature inv.top_p = self.top_p inv.frequency_penalty = self.frequency_penalty inv.presence_penalty = self.presence_penalty inv.max_tokens = self.max_tokens inv.stop_sequences = self.stop_sequences inv.seed = self.seed inv.server_address = self.server_address inv.server_port = self.server_port inv.attributes = self.attributes inv.metric_attributes = self.metric_attributes @property def span(self) -> Span: """The underlying span, for back-compat with code that checks span.is_recording().""" return self._inference_invocation.span if self._inference_invocation is not None else INVALID_SPAN _invocation.py000066400000000000000000000210661527070072300360050ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import timeit from abc import ABC, abstractmethod from collections.abc import Iterator, Sequence from contextlib import contextmanager from contextvars import Token from dataclasses import asdict from typing import TYPE_CHECKING, Any, TypeAlias from typing_extensions import Self from opentelemetry._logs import Logger, LogRecord from opentelemetry.context import Context, attach, detach from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAI, ) from opentelemetry.semconv.attributes import error_attributes from opentelemetry.trace import INVALID_SPAN as _INVALID_SPAN from opentelemetry.trace import Span, SpanKind, Tracer, set_span_in_context from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util.genai.completion_hook import CompletionHook from opentelemetry.util.genai.types import ( Error, InputMessage, MessagePart, OutputMessage, ToolDefinition, ) from opentelemetry.util.genai.utils import ( ContentCapturingMode, gen_ai_json_dumps, get_content_capturing_mode, is_experimental_mode, ) if TYPE_CHECKING: from opentelemetry.util.genai.metrics import InvocationMetricsRecorder ContextToken: TypeAlias = Token[Context] class GenAIInvocation(ABC): """ Base class for all GenAI invocation types. Manages the lifecycle of a single GenAI operation (LLM call, embedding, tool execution, workflow, etc.). Use the factory methods on TelemetryHandler (start_inference, start_embedding, start_workflow, start_tool) rather than constructing invocations directly. """ def __init__( self, # Individual components instead of TelemetryHandler to avoid a circular # import between handler.py and the invocation modules. tracer: Tracer, metrics_recorder: InvocationMetricsRecorder, logger: Logger, completion_hook: CompletionHook, operation_name: str, span_name: str, span_kind: SpanKind = SpanKind.CLIENT, attributes: dict[str, Any] | None = None, metric_attributes: dict[str, Any] | None = None, ) -> None: self._tracer = tracer self._metrics_recorder = metrics_recorder self._logger = logger self._completion_hook = completion_hook self._operation_name: str = operation_name self.attributes: dict[str, Any] = {} if attributes is None else attributes """Additional attributes to set on spans and/or events. Not set on metrics.""" self.metric_attributes: dict[str, Any] = {} if metric_attributes is None else metric_attributes """Additional attributes to set on metrics. Must be low cardinality. Not set on spans or events.""" self.span: Span = _INVALID_SPAN self._span_context: Context self._span_name: str = span_name self._span_kind: SpanKind = span_kind self._context_token: ContextToken | None = None self._monotonic_start_s: float | None = None def _start(self, attributes: dict[str, Any] | None = None) -> None: """Start the invocation span and attach it to the current context. Args: attributes: Initial span attributes available for sampling decisions. """ self.span = self._tracer.start_span( name=self._span_name, kind=self._span_kind, attributes=attributes, ) self._span_context = set_span_in_context(self.span) self._monotonic_start_s = timeit.default_timer() self._context_token = attach(self._span_context) def _get_metric_attributes(self) -> dict[str, Any]: """Return low-cardinality attributes for metric recording.""" return self.metric_attributes def _get_metric_token_counts(self) -> dict[str, int]: # pylint: disable=no-self-use """Return {token_type: count} for token histogram recording.""" return {} def _apply_error_attributes(self, error: Error) -> None: """Apply error status and error.type attribute to the span, events, and metrics.""" error_type = error.type.__qualname__ self.span.set_status(Status(StatusCode.ERROR, error.message)) self.attributes[error_attributes.ERROR_TYPE] = error_type self.metric_attributes[error_attributes.ERROR_TYPE] = error_type def _call_completion_hook( self, *, inputs: list[InputMessage] | None = None, outputs: list[OutputMessage] | None = None, system_instruction: list[MessagePart] | None = None, tool_definitions: list[ToolDefinition] | None = None, log_record: LogRecord | None = None, ) -> None: """Invoke the completion hook with the invocation's content. Subclasses pass whichever content fields they carry; the wrapper substitutes [] for unspecified list fields """ self._completion_hook.on_completion( inputs=inputs or [], outputs=outputs or [], system_instruction=system_instruction or [], tool_definitions=tool_definitions, span=self.span, log_record=log_record, ) @abstractmethod def _apply_finish(self, error: Error | None = None) -> None: """Apply finish telemetry (attributes, metrics, events).""" def _finish(self, error: Error | None = None) -> None: """Apply finish telemetry and end the span.""" if self._context_token is None: return try: self._apply_finish(error) finally: try: detach(self._context_token) except Exception: # pylint: disable=broad-except pass self.span.end() def stop(self) -> None: """Finalize the invocation successfully and end its span.""" self._finish() def fail(self, error: Error | BaseException) -> None: """Fail the invocation and end its span with error status.""" if isinstance(error, BaseException): error = Error(type=type(error), message=str(error)) self._finish(error) @contextmanager def _managed(self) -> Iterator[Self]: """Context manager that calls stop() on success or fail() on exception.""" try: yield self except Exception as exc: self.fail(exc) raise self.stop() def get_content_attributes( *, input_messages: Sequence[InputMessage], output_messages: Sequence[OutputMessage], system_instruction: Sequence[MessagePart], tool_definitions: Sequence[ToolDefinition] | None, for_span: bool, ) -> dict[str, Any]: """Serialize messages, system instructions, and tool definitions into attributes. Args: input_messages: Input messages to serialize. output_messages: Output messages to serialize. system_instruction: System instructions to serialize. tool_definitions: Tool definitions to serialize (may be None). for_span: If True, serialize for span attributes (JSON string); if False, serialize for event attributes (list of dicts). """ if not is_experimental_mode(): return {} mode = get_content_capturing_mode() allowed_modes = ( ( ContentCapturingMode.SPAN_ONLY, ContentCapturingMode.SPAN_AND_EVENT, ) if for_span else ( ContentCapturingMode.EVENT_ONLY, ContentCapturingMode.SPAN_AND_EVENT, ) ) def serialize(items: Sequence[Any]) -> Any: dicts = [asdict(item) for item in items] return gen_ai_json_dumps(dicts) if for_span else dicts # Tool definitions are always captured, the sem conv recommends adding params / description only # when the content capture mode is set.. if mode not in allowed_modes: return {GenAI.GEN_AI_TOOL_DEFINITIONS: serialize(tool_definitions)} if tool_definitions else {} optional_attrs = ( ( GenAI.GEN_AI_INPUT_MESSAGES, serialize(input_messages) if input_messages else None, ), ( GenAI.GEN_AI_OUTPUT_MESSAGES, serialize(output_messages) if output_messages else None, ), ( GenAI.GEN_AI_SYSTEM_INSTRUCTIONS, serialize(system_instruction) if system_instruction else None, ), ( GenAI.GEN_AI_TOOL_DEFINITIONS, serialize(tool_definitions) if tool_definitions else None, ), ) return {key: value for key, value in optional_attrs if value is not None} _tool_invocation.py000066400000000000000000000103231527070072300370340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from typing import Any from opentelemetry._logs import Logger from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAI, ) from opentelemetry.trace import Tracer from opentelemetry.util.genai._invocation import Error, GenAIInvocation from opentelemetry.util.genai.completion_hook import CompletionHook from opentelemetry.util.genai.metrics import InvocationMetricsRecorder class ToolInvocation(GenAIInvocation): """Represents a tool call invocation for execute_tool span tracking. Not used as a message part — use ToolCallRequest for that purpose. Use handler.start_tool(name) rather than constructing this directly. Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md#execute-tool-span Semantic convention attributes for execute_tool spans: - gen_ai.operation.name: "execute_tool" (Required) - gen_ai.tool.name: Name of the tool (Recommended) - gen_ai.tool.call.id: Tool call identifier (Recommended if available) - gen_ai.tool.type: Type classification - "function", "extension", or "datastore" (Recommended if available) - gen_ai.tool.description: Tool description (Recommended if available) - gen_ai.tool.call.arguments: Parameters passed to tool (Opt-In, may contain sensitive data) - gen_ai.tool.call.result: Result returned by tool (Opt-In, may contain sensitive data) - error.type: Error type if operation failed (Conditionally Required) """ def __init__( self, tracer: Tracer, metrics_recorder: InvocationMetricsRecorder, logger: Logger, completion_hook: CompletionHook, name: str, *, arguments: Any = None, tool_call_id: str | None = None, tool_type: str | None = None, tool_description: str | None = None, ) -> None: """Use handler.start_tool(name) or handler.tool(name) instead of calling this directly.""" _operation_name = GenAI.GenAiOperationNameValues.EXECUTE_TOOL.value # pyright: ignore[reportDeprecated] super().__init__( tracer, metrics_recorder, logger, completion_hook, operation_name=_operation_name, span_name=f"{_operation_name} {name}" if name else _operation_name, ) self.name = name self.arguments = arguments self.tool_call_id = tool_call_id self.tool_type = tool_type self.tool_description = tool_description self.tool_result: Any = None self._start(self._get_base_attributes()) def _get_base_attributes(self) -> dict[str, Any]: """Return sampling-relevant attributes available at span creation time.""" optional_attrs = ( (GenAI.GEN_AI_TOOL_NAME, self.name), (GenAI.GEN_AI_TOOL_CALL_ID, self.tool_call_id), (GenAI.GEN_AI_TOOL_TYPE, self.tool_type), (GenAI.GEN_AI_TOOL_DESCRIPTION, self.tool_description), ) return { GenAI.GEN_AI_OPERATION_NAME: self._operation_name, **{k: v for k, v in optional_attrs if v is not None}, } def _get_metric_attributes(self) -> dict[str, Any]: attrs: dict[str, Any] = { GenAI.GEN_AI_OPERATION_NAME: self._operation_name, } attrs.update(self.metric_attributes) return attrs def _apply_finish(self, error: Error | None = None) -> None: if error is not None: self._apply_error_attributes(error) optional_attrs = ( (GenAI.GEN_AI_TOOL_NAME, self.name), (GenAI.GEN_AI_TOOL_CALL_ID, self.tool_call_id), (GenAI.GEN_AI_TOOL_TYPE, self.tool_type), (GenAI.GEN_AI_TOOL_DESCRIPTION, self.tool_description), (GenAI.GEN_AI_TOOL_CALL_ARGUMENTS, self.arguments), ) attributes: dict[str, Any] = { GenAI.GEN_AI_OPERATION_NAME: self._operation_name, **{k: v for k, v in optional_attrs if v is not None}, } attributes.update(self.attributes) self.span.set_attributes(attributes) self._metrics_recorder.record(self) _upload/000077500000000000000000000000001527070072300345415ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai__init__.py000066400000000000000000000046311527070072300366560ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_upload# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging from os import environ from opentelemetry.util.genai.completion_hook import ( CompletionHook, _NoOpCompletionHook, ) from opentelemetry.util.genai.environment_variables import ( OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH, OTEL_INSTRUMENTATION_GENAI_UPLOAD_FORMAT, OTEL_INSTRUMENTATION_GENAI_UPLOAD_MAX_QUEUE_SIZE, ) _logger = logging.getLogger(__name__) def upload_completion_hook() -> CompletionHook: base_path = environ.get(OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH) if not base_path: _logger.warning( "%s is required but not set, using no-op instead", OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH, ) return _NoOpCompletionHook() # If fsspec is not installed the hook will be a no-op. try: # pylint: disable=import-outside-toplevel from opentelemetry.util.genai._upload.completion_hook import ( # noqa: PLC0415 _DEFAULT_FORMAT, _DEFAULT_MAX_QUEUE_SIZE, _FORMATS, UploadCompletionHook, ) except ImportError: _logger.exception("Failed to import `fsspec`. Falling back to `NoOpCompletionHook`.") return _NoOpCompletionHook() environ_max_queue_size = environ.get( OTEL_INSTRUMENTATION_GENAI_UPLOAD_MAX_QUEUE_SIZE, _DEFAULT_MAX_QUEUE_SIZE, ) try: environ_max_queue_size = int(environ_max_queue_size) except ValueError: _logger.warning( "%s is not a valid integer for %s. Defaulting to %s.", environ_max_queue_size, OTEL_INSTRUMENTATION_GENAI_UPLOAD_MAX_QUEUE_SIZE, _DEFAULT_MAX_QUEUE_SIZE, ) environ_max_queue_size = _DEFAULT_MAX_QUEUE_SIZE environ_format = environ.get(OTEL_INSTRUMENTATION_GENAI_UPLOAD_FORMAT, _DEFAULT_FORMAT).lower() if environ_format not in _FORMATS: _logger.warning( "%s is not a valid option for %s, should be be one of %s. Defaulting to %s.", environ_format, OTEL_INSTRUMENTATION_GENAI_UPLOAD_FORMAT, _FORMATS, _DEFAULT_FORMAT, ) environ_format = _DEFAULT_FORMAT return UploadCompletionHook( base_path=base_path, max_queue_size=environ_max_queue_size, upload_format=environ_format, ) completion_hook.py000066400000000000000000000326141527070072300403120ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_upload# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import dataclasses import hashlib import json import logging import posixpath import threading from collections import OrderedDict from collections.abc import Callable from concurrent.futures import ( Future, ThreadPoolExecutor, ) from contextlib import ExitStack from dataclasses import asdict, dataclass from functools import partial from time import time from typing import Any, Final, Literal from uuid import uuid4 import fsspec from opentelemetry._logs import LogRecord from opentelemetry.semconv._incubating.attributes import gen_ai_attributes from opentelemetry.trace import Span from opentelemetry.util.genai import types from opentelemetry.util.genai.completion_hook import CompletionHook from opentelemetry.util.genai.utils import gen_ai_json_dump GEN_AI_INPUT_MESSAGES_REF: Final = gen_ai_attributes.GEN_AI_INPUT_MESSAGES + "_ref" GEN_AI_OUTPUT_MESSAGES_REF: Final = gen_ai_attributes.GEN_AI_OUTPUT_MESSAGES + "_ref" GEN_AI_SYSTEM_INSTRUCTIONS_REF: Final = gen_ai_attributes.GEN_AI_SYSTEM_INSTRUCTIONS + "_ref" # TODO: Migrate to gen_ai_attributes constant once available in semconv package GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions" GEN_AI_TOOL_DEFINITIONS_REF: Final = GEN_AI_TOOL_DEFINITIONS + "_ref" _MESSAGE_INDEX_KEY = "index" _DEFAULT_MAX_QUEUE_SIZE = 20 _DEFAULT_FORMAT = "json" Format = Literal["json", "jsonl"] _FORMATS: tuple[Format, ...] = ("json", "jsonl") _logger = logging.getLogger(__name__) @dataclass class Completion: inputs: list[types.InputMessage] | None outputs: list[types.OutputMessage] | None system_instruction: list[types.MessagePart] | None tool_definitions: list[types.ToolDefinition] | None @dataclass class CompletionRefs: inputs_ref: str outputs_ref: str system_instruction_ref: str tool_definitions_ref: str JsonEncodeable = list[dict[str, Any]] # mapping of upload path and whether the contents were hashed to the filename to function computing upload data dict UploadData = dict[tuple[str, bool], Callable[[], JsonEncodeable]] def is_message_part_list_hashable( message_parts: list[types.MessagePart] | None, ) -> bool: return bool(message_parts) and all(isinstance(x, types.Text) for x in message_parts) def hash_tool_definitions( tool_definitions: list[types.ToolDefinition] | None, ) -> str | None: if not tool_definitions: return None try: tool_dicts = [{k: v for k, v in dataclasses.asdict(t).items() if v is not None} for t in tool_definitions] encoded_tools = json.dumps( tool_dicts, sort_keys=True, ).encode("utf-8") return hashlib.sha256( encoded_tools, usedforsecurity=False, ).hexdigest() except (TypeError, AttributeError): return None class UploadCompletionHook(CompletionHook): """An completion hook using ``fsspec`` to upload to external storage This function can be used as the :func:`~opentelemetry.util.genai.completion_hook.load_completion_hook` implementation by setting :envvar:`OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK` to ``upload``. :envvar:`OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH` must be configured to specify the base path for uploads. Both the ``fsspec`` and ``opentelemetry-sdk`` packages should be installed, or a no-op implementation will be used instead. You can use ``opentelemetry-util-genai[upload]`` as a requirement to achieve this. """ def __init__( self, *, base_path: str, max_queue_size: int = _DEFAULT_MAX_QUEUE_SIZE, upload_format: Format = _DEFAULT_FORMAT, lru_cache_max_size: int = 1024, ) -> None: self._max_queue_size = max_queue_size self._fs, base_path = fsspec.url_to_fs(base_path) self._base_path = self._fs.unstrip_protocol(base_path) self.lru_dict: OrderedDict[str, bool] = OrderedDict() self.lru_cache_max_size = lru_cache_max_size if upload_format not in _FORMATS: raise ValueError(f"Invalid {upload_format=}. Must be one of {_FORMATS}") self._format = upload_format self._content_type = "application/json" if self._format == "json" else "application/jsonl" test_path = posixpath.join( self._base_path, f".one_off_test_to_see_if_upload_works.{self._format}", ) try: with self._fs.open(test_path, "w", content_type=self._content_type) as file: file.write("\n") except Exception as exception: # pylint: disable=broad-exception-caught raise ValueError( f"Failed to write file to the following path, upload is not working: {test_path}.\n Got error: {exception}" ) # Try to delete the file.. But we don't explicitly ask people to grant the GCS delete IAM permission in our # docs, so if delete fails just leave the file.. try: self._fs.rm_file(test_path) # pyright: ignore[reportUnknownMemberType] except Exception: # pylint: disable=broad-exception-caught pass # Use a ThreadPoolExecutor for its queueing and thread management. The semaphore # limits the number of queued tasks. If the queue is full, data will be dropped. self._executor = ThreadPoolExecutor(max_workers=min(self._max_queue_size, 64)) self._semaphore = threading.BoundedSemaphore(self._max_queue_size) def _submit_all(self, upload_data: UploadData) -> None: def done(future: Future[None]) -> None: try: future.result() except Exception: # pylint: disable=broad-except _logger.exception("uploader failed") finally: self._semaphore.release() for ( path, contents_hashed_to_filename, ), json_encodeable in upload_data.items(): if contents_hashed_to_filename and path in self.lru_dict: self.lru_dict.move_to_end(path) continue # could not acquire, drop data if not self._semaphore.acquire(blocking=False): # pylint: disable=consider-using-with _logger.warning( "upload queue is full, dropping upload %s", path, ) continue try: fut = self._executor.submit( self._do_upload, path, contents_hashed_to_filename, json_encodeable, ) fut.add_done_callback(done) except RuntimeError: _logger.info("attempting to upload file after UploadCompletionHook.shutdown() was already called") self._semaphore.release() def _calculate_ref_path( self, system_instruction: list[types.MessagePart], tool_definitions: list[types.ToolDefinition] | None = None, ) -> CompletionRefs: # TODO: experimental with using the trace_id and span_id, or fetching # gen_ai.response.id from the active span. system_instruction_hash = None if is_message_part_list_hashable(system_instruction): # Get a hash of the text. system_instruction_hash = hashlib.sha256( "\n".join(x.content for x in system_instruction).encode( # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownArgumentType, reportCallIssue, reportArgumentType] "utf-8" ), usedforsecurity=False, ).hexdigest() tool_definitions_hash = hash_tool_definitions(tool_definitions) uuid_str = str(uuid4()) return CompletionRefs( inputs_ref=posixpath.join(self._base_path, f"{uuid_str}_inputs.{self._format}"), outputs_ref=posixpath.join(self._base_path, f"{uuid_str}_outputs.{self._format}"), system_instruction_ref=posixpath.join( self._base_path, f"{system_instruction_hash or uuid_str}_system_instruction.{self._format}", ), tool_definitions_ref=posixpath.join( self._base_path, f"{tool_definitions_hash or uuid_str}_tool.definitions.{self._format}", ), ) def _file_exists(self, path: str) -> bool: if path in self.lru_dict: self.lru_dict.move_to_end(path) return True # https://filesystem-spec.readthedocs.io/en/latest/api.html#fsspec.spec.AbstractFileSystem.exists file_exists = self._fs.exists(path) # don't cache this because soon the file will exist.. if not file_exists: return False self.lru_dict[path] = True if len(self.lru_dict) > self.lru_cache_max_size: self.lru_dict.popitem(last=False) return True def _do_upload( self, path: str, contents_hashed_to_filename: bool, json_encodeable: Callable[[], JsonEncodeable], ) -> None: if contents_hashed_to_filename and self._file_exists(path): return if self._format == "json": # output as a single line with the json messages array message_lines = [json_encodeable()] else: # output as one line per message in the array message_lines = json_encodeable() # add an index for streaming readers of jsonl for message_idx, line in enumerate(message_lines): line[_MESSAGE_INDEX_KEY] = message_idx with self._fs.open(path, "w", content_type=self._content_type) as file: for message in message_lines: gen_ai_json_dump(message, file) file.write("\n") if contents_hashed_to_filename: self.lru_dict[path] = True if len(self.lru_dict) > self.lru_cache_max_size: self.lru_dict.popitem(last=False) def on_completion( self, *, inputs: list[types.InputMessage], outputs: list[types.OutputMessage], system_instruction: list[types.MessagePart], tool_definitions: list[types.ToolDefinition] | None = None, span: Span | None = None, log_record: LogRecord | None = None, **kwargs: Any, ) -> None: if not any([inputs, outputs, system_instruction, tool_definitions]): return # An empty list will not be uploaded. completion = Completion( inputs=inputs or None, outputs=outputs or None, system_instruction=system_instruction or None, tool_definitions=tool_definitions or None, ) # generate the paths to upload to ref_names = self._calculate_ref_path(system_instruction, tool_definitions) def to_dict( dataclass_list: list[types.InputMessage] | list[types.OutputMessage] | list[types.MessagePart] | list[types.ToolDefinition], ) -> JsonEncodeable: return [asdict(dc) for dc in dataclass_list] references = [ (ref_name, ref, ref_attr, contents_hashed_to_filename) for ref_name, ref, ref_attr, contents_hashed_to_filename in [ ( ref_names.inputs_ref, completion.inputs, GEN_AI_INPUT_MESSAGES_REF, False, ), ( ref_names.outputs_ref, completion.outputs, GEN_AI_OUTPUT_MESSAGES_REF, False, ), ( ref_names.system_instruction_ref, completion.system_instruction, GEN_AI_SYSTEM_INSTRUCTIONS_REF, is_message_part_list_hashable(completion.system_instruction), ), ( ref_names.tool_definitions_ref, completion.tool_definitions, GEN_AI_TOOL_DEFINITIONS_REF, bool(completion.tool_definitions), ), ] if ref # Filter out empty input/output/sys instruction/tool defs ] self._submit_all( { (ref_name, contents_hashed_to_filename): partial(to_dict, ref) for ref_name, ref, _, contents_hashed_to_filename in references } ) # stamp the refs on telemetry references = {ref_attr: name for name, _, ref_attr, _ in references} if span: span.set_attributes(references) if log_record: log_record.attributes = { **(log_record.attributes or {}), **references, } def shutdown(self, *, timeout_sec: float = 10.0) -> None: deadline = time() + timeout_sec # Wait for all tasks to finish to flush the queue with ExitStack() as stack: for _ in range(self._max_queue_size): remaining = deadline - time() if not self._semaphore.acquire(timeout=remaining): # pylint: disable=consider-using-with # Couldn't finish flushing all uploads before timeout break stack.callback(self._semaphore.release) # Queue is flushed and blocked, start shutdown self._executor.shutdown(wait=False) _workflow_invocation.py000066400000000000000000000070501527070072300377340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from dataclasses import asdict from typing import Any from opentelemetry._logs import Logger from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAI, ) from opentelemetry.trace import SpanKind, Tracer from opentelemetry.util.genai._invocation import Error, GenAIInvocation from opentelemetry.util.genai.completion_hook import CompletionHook from opentelemetry.util.genai.metrics import InvocationMetricsRecorder from opentelemetry.util.genai.types import ( InputMessage, OutputMessage, ) from opentelemetry.util.genai.utils import ( ContentCapturingMode, gen_ai_json_dumps, get_content_capturing_mode, is_experimental_mode, ) class WorkflowInvocation(GenAIInvocation): """ Represents a predetermined sequence of operations (e.g. agent, LLM, tool, and retrieval invocations). A workflow groups multiple operations together, accepting input(s) and producing final output(s). Use handler.start_workflow(name) or the handler.workflow(name) context manager rather than constructing this directly. """ def __init__( self, tracer: Tracer, metrics_recorder: InvocationMetricsRecorder, logger: Logger, completion_hook: CompletionHook, name: str | None, ) -> None: """Use handler.start_workflow(name) or handler.workflow(name) instead of calling this directly.""" _operation_name = "invoke_workflow" super().__init__( tracer, metrics_recorder, logger, completion_hook, operation_name=_operation_name, span_name=f"{_operation_name} {name}" if name else _operation_name, span_kind=SpanKind.INTERNAL, ) self.name = name self.input_messages: list[InputMessage] = [] self.output_messages: list[OutputMessage] = [] self._start(self._get_base_attributes()) def _get_base_attributes(self) -> dict[str, Any]: """Return sampling-relevant attributes available at span creation time.""" attrs: dict[str, Any] = { GenAI.GEN_AI_OPERATION_NAME: self._operation_name, } return attrs def _get_messages_for_span(self) -> dict[str, Any]: if not is_experimental_mode() or get_content_capturing_mode() not in ( ContentCapturingMode.SPAN_ONLY, ContentCapturingMode.SPAN_AND_EVENT, ): return {} optional_attrs = ( ( GenAI.GEN_AI_INPUT_MESSAGES, gen_ai_json_dumps([asdict(m) for m in self.input_messages]) if self.input_messages else None, ), ( GenAI.GEN_AI_OUTPUT_MESSAGES, gen_ai_json_dumps([asdict(m) for m in self.output_messages]) if self.output_messages else None, ), ) return {key: value for key, value in optional_attrs if value is not None} def _apply_finish(self, error: Error | None = None) -> None: attributes: dict[str, Any] = {GenAI.GEN_AI_OPERATION_NAME: self._operation_name} attributes.update(self._get_messages_for_span()) if error is not None: self._apply_error_attributes(error) attributes.update(self.attributes) self.span.set_attributes(attributes) self._call_completion_hook( inputs=self.input_messages, outputs=self.output_messages, ) # TODO: Add workflow metrics when supported completion_hook.py000066400000000000000000000124131527070072300366620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """This module defines the generic hooks for GenAI content completion The hooks are specified as part of semconv in `Uploading content to external storage `__. This module defines the `CompletionHook` type that custom implementations should implement, and a `load_completion_hook` function to load it from an entry point. """ from __future__ import annotations import logging from os import environ from typing import Any, Protocol, runtime_checkable from opentelemetry._logs import LogRecord from opentelemetry.trace import Span from opentelemetry.util._importlib_metadata import ( entry_points, # pyright: ignore[reportUnknownVariableType] ) from opentelemetry.util.genai import types from opentelemetry.util.genai.environment_variables import ( OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK, ) _logger = logging.getLogger(__name__) @runtime_checkable class CompletionHook(Protocol): """A hook to be called on completion of a GenAI operation. This is the interface for a hook that can be used to capture GenAI content on completion. The hook is a callable that takes the inputs, outputs, and system instruction of a GenAI interaction, as well as the span and log record associated with it. The hook can be used to upload the content to any external storage, such as a database, a file system, or a cloud storage service. The span and log_record arguments should be provided based on the content capturing mode :func:`~opentelemetry.util.genai.utils.get_content_capturing_mode`. .. note:: Hooks returned from :func:`load_completion_hook` are wrapped so any exception raised by :meth:`on_completion` is logged and swallowed. Instrumentation code calling ``on_completion`` on a hook obtained from :func:`load_completion_hook` does not need a ``try``/``except`` around the call - exceptions never escape into the instrumented application. Args: inputs: The inputs of the GenAI interaction. outputs: The outputs of the GenAI interaction. system_instruction: The system instruction of the GenAI interaction. tool_definitions: The list of source system tool definitions available to the GenAI agent or model. span: The span associated with the GenAI interaction. log_record: The event log associated with the GenAI interaction. """ def on_completion( self, *, inputs: list[types.InputMessage], outputs: list[types.OutputMessage], system_instruction: list[types.MessagePart], tool_definitions: list[types.ToolDefinition] | None = None, span: Span | None = None, log_record: LogRecord | None = None, ) -> None: ... class _NoOpCompletionHook(CompletionHook): def on_completion(self, **kwargs: Any) -> None: return None class _SafeCompletionHook(CompletionHook): """Wraps a :class:`CompletionHook` so exceptions raised by ``on_completion`` are logged and swallowed instead of propagating to the caller. Instrumentation code calls ``on_completion`` from telemetry paths that must not surface telemetry errors to the user's application. Wrapping at the boundary keeps each call site free of repetitive ``try/except`` blocks. """ def __init__(self, wrapped: CompletionHook) -> None: self._wrapped = wrapped def on_completion(self, **kwargs: Any) -> None: try: self._wrapped.on_completion(**kwargs) except Exception as ex: # pylint: disable=broad-except _logger.warning( "CompletionHook %r raised an exception; suppressing", self._wrapped, exc_info=ex, ) def load_completion_hook() -> CompletionHook: """Load the completion hook from entry point or return a noop implementation This function loads an completion hook from the entry point group ``opentelemetry_genai_completion_hook`` with name coming from :envvar:`OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK`. If one can't be found, returns a no-op implementation. The returned hook wraps the user-provided implementation so any exception raised by ``on_completion`` is logged and swallowed. """ hook_name = environ.get(OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK, None) if not hook_name: return _NoOpCompletionHook() for entry_point in entry_points(group="opentelemetry_genai_completion_hook"): name = entry_point.name try: if hook_name != name: continue hook = entry_point.load()() if not isinstance(hook, CompletionHook): _logger.debug("%s is not a valid CompletionHook. Using noop", name) continue _logger.debug("Using CompletionHook %s", name) return _SafeCompletionHook(hook) except Exception: # pylint: disable=broad-except _logger.exception("CompletionHook %s configuration failed. Using noop", name) return _NoOpCompletionHook() __all__ = ["CompletionHook", "load_completion_hook"] environment_variables.py000066400000000000000000000035771527070072300401000ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT = "OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT" """ .. envvar:: OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT Controls whether to emit gen_ai.client.inference.operation.details events. Must be one of ``true`` or ``false`` (case-insensitive). Defaults to ``false``. """ OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK = "OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK" """ .. envvar:: OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK """ OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH = "OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH" """ .. envvar:: OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH An :func:`fsspec.open` compatible URI/path for uploading prompts and responses. Can be a local path like ``/path/to/prompts`` or a cloud storage URI such as ``gs://my_bucket``. For more information, see * `Instantiate a file-system `_ for supported values and how to install support for additional backend implementations. * `Configuration `_ for configuring a backend with environment variables. """ OTEL_INSTRUMENTATION_GENAI_UPLOAD_FORMAT = "OTEL_INSTRUMENTATION_GENAI_UPLOAD_FORMAT" """ .. envvar:: OTEL_INSTRUMENTATION_GENAI_UPLOAD_FORMAT The format to use when uploading prompt and response data. Must be one of ``json`` or ``jsonl``. Defaults to ``json``. """ OTEL_INSTRUMENTATION_GENAI_UPLOAD_MAX_QUEUE_SIZE = "OTEL_INSTRUMENTATION_GENAI_UPLOAD_MAX_QUEUE_SIZE" """ .. envvar:: OTEL_INSTRUMENTATION_GENAI_UPLOAD_MAX_QUEUE_SIZE The maximum number of concurrent uploads to queue. New uploads will be dropped if the queue is full. Defaults to 20. """ handler.py000066400000000000000000000374061527070072300351170ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Telemetry handler for GenAI invocations. This module exposes the `TelemetryHandler` class, which manages the lifecycle of GenAI (Generative AI) invocations and emits telemetry data (spans and related attributes). Classes: - TelemetryHandler: Manages GenAI invocation lifecycles and emits telemetry. Functions: - get_telemetry_handler: Returns a singleton `TelemetryHandler` instance. Usage: handler = get_telemetry_handler() # Factory method: construct and start in one call, then stop or fail. invocation = handler.start_inference("my-provider", request_model="my-model") invocation.input_messages = [...] invocation.temperature = 0.7 try: # ... call the underlying library ... invocation.output_messages = [...] invocation.stop() except Exception as exc: invocation.fail(exc) raise # Or use the context manager form — exception handling is automatic. with handler.inference("my-provider", request_model="my-model") as invocation: invocation.input_messages = [...] # ... call the underlying library ... invocation.output_messages = [...] """ from __future__ import annotations import os from contextlib import AbstractContextManager from opentelemetry._logs import ( LoggerProvider, get_logger, ) from opentelemetry.metrics import MeterProvider, get_meter from opentelemetry.semconv.schemas import Schemas from opentelemetry.trace import ( SpanKind, TracerProvider, get_tracer, ) from opentelemetry.util.genai._agent_invocation import AgentInvocation from opentelemetry.util.genai._inference_invocation import LLMInvocation from opentelemetry.util.genai._invocation import Error from opentelemetry.util.genai.completion_hook import ( CompletionHook, _NoOpCompletionHook, ) from opentelemetry.util.genai.environment_variables import ( OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, ) from opentelemetry.util.genai.invocation import ( EmbeddingInvocation, InferenceInvocation, ToolInvocation, WorkflowInvocation, ) from opentelemetry.util.genai.metrics import InvocationMetricsRecorder from opentelemetry.util.genai.types import ContentCapturingMode from opentelemetry.util.genai.utils import ( get_content_capturing_mode, is_experimental_mode, ) from opentelemetry.util.genai.version import __version__ class TelemetryHandler: """ High-level handler managing GenAI invocation lifecycles and emitting them as spans, metrics, and events. """ def __init__( self, tracer_provider: TracerProvider | None = None, meter_provider: MeterProvider | None = None, logger_provider: LoggerProvider | None = None, completion_hook: CompletionHook | None = None, ): schema_url = Schemas.V1_37_0.value self._tracer = get_tracer( __name__, __version__, tracer_provider, schema_url=schema_url, ) meter = get_meter(__name__, meter_provider=meter_provider, schema_url=schema_url) self._metrics_recorder = InvocationMetricsRecorder(meter) self._logger = get_logger( __name__, __version__, logger_provider, schema_url=schema_url, ) self._completion_hook = completion_hook or _NoOpCompletionHook() if is_experimental_mode(): content_enabled = get_content_capturing_mode() != ContentCapturingMode.NO_CONTENT else: content_enabled = os.environ.get(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "").lower() in ( "true", "span_only", "event_only", "span_and_event", ) self._capture_content = content_enabled or not isinstance(self._completion_hook, _NoOpCompletionHook) def should_capture_content(self) -> bool: """Returns True if content should be captured. Content is captured when the content capturing mode requires it, or when a real completion hook is configured (not a no-op). """ return self._capture_content # New-style factory methods: construct + start in one call, handler stored on invocation def start_inference( self, provider: str, *, request_model: str | None = None, server_address: str | None = None, server_port: int | None = None, operation_name: str | None = None, ) -> InferenceInvocation: """Create and start an LLM inference invocation. Set remaining attributes (input_messages, temperature, etc.) on the returned invocation, then call invocation.stop() or invocation.fail(). """ return InferenceInvocation( self._tracer, self._metrics_recorder, self._logger, self._completion_hook, provider, request_model=request_model, server_address=server_address, server_port=server_port, operation_name=operation_name, ) def start_llm(self, invocation: LLMInvocation) -> LLMInvocation: """Start an LLM invocation. .. deprecated:: Use ``handler.start_inference()`` instead. """ invocation._start_with_handler( self._tracer, self._metrics_recorder, self._logger, self._completion_hook, ) return invocation def start_embedding( self, provider: str, *, request_model: str | None = None, server_address: str | None = None, server_port: int | None = None, ) -> EmbeddingInvocation: """Create and start an Embedding invocation. Set remaining attributes (encoding_formats, etc.) on the returned invocation, then call invocation.stop() or invocation.fail(). """ return EmbeddingInvocation( self._tracer, self._metrics_recorder, self._logger, self._completion_hook, provider, request_model=request_model, server_address=server_address, server_port=server_port, ) def start_tool( self, name: str, *, arguments: object = None, tool_call_id: str | None = None, tool_type: str | None = None, tool_description: str | None = None, ) -> ToolInvocation: """Create and start a tool invocation. Set tool_result on the returned invocation when done, then call invocation.stop() or invocation.fail(). """ return ToolInvocation( self._tracer, self._metrics_recorder, self._logger, self._completion_hook, name, arguments=arguments, tool_call_id=tool_call_id, tool_type=tool_type, tool_description=tool_description, ) def start_workflow( self, *, name: str | None = None, ) -> WorkflowInvocation: """Create and start a workflow invocation. Set remaining attributes on the returned invocation, then call invocation.stop() or invocation.fail(). """ return WorkflowInvocation( self._tracer, self._metrics_recorder, self._logger, self._completion_hook, name, ) def stop_llm(self, invocation: LLMInvocation) -> LLMInvocation: # pylint: disable=no-self-use """Finalize an LLM invocation successfully and end its span. .. deprecated:: Use ``handler.start_inference()`` and then ``inference.stop()`` instead. """ invocation._sync_to_invocation() if invocation._inference_invocation is not None: invocation._inference_invocation.stop() return invocation def fail_llm( # pylint: disable=no-self-use self, invocation: LLMInvocation, error: Error, ) -> LLMInvocation: """Fail an LLM invocation and end its span with error status. .. deprecated:: Use ``handler.start_inference()`` and then ``inference.fail()`` instead. """ invocation._sync_to_invocation() if invocation._inference_invocation is not None: invocation._inference_invocation.fail(error) return invocation def inference( self, provider: str, *, request_model: str | None = None, server_address: str | None = None, server_port: int | None = None, ) -> AbstractContextManager[InferenceInvocation]: """Context manager for LLM inference invocations. Only set data attributes on the invocation object, do not modify the span or context. Starts the span on entry. On normal exit, finalizes the invocation and ends the span. If an exception occurs inside the context, marks the span as error, ends it, and re-raises the original exception. """ return self.start_inference( provider=provider, request_model=request_model, server_address=server_address, server_port=server_port, )._managed() def embedding( self, provider: str, *, request_model: str | None = None, server_address: str | None = None, server_port: int | None = None, ) -> AbstractContextManager[EmbeddingInvocation]: """Context manager for Embedding invocations. Only set data attributes on the invocation object, do not modify the span or context. Starts the span on entry. On normal exit, finalizes the invocation and ends the span. If an exception occurs inside the context, marks the span as error, ends it, and re-raises the original exception. """ return self.start_embedding( provider=provider, request_model=request_model, server_address=server_address, server_port=server_port, )._managed() def tool( self, name: str, *, arguments: object = None, tool_call_id: str | None = None, tool_type: str | None = None, tool_description: str | None = None, ) -> AbstractContextManager[ToolInvocation]: """Context manager for Tool invocations. Only set data attributes on the invocation object, do not modify the span or context. Starts the span on entry. On normal exit, finalizes the invocation and ends the span. If an exception occurs inside the context, marks the span as error, ends it, and re-raises the original exception. """ return self.start_tool( name, arguments=arguments, tool_call_id=tool_call_id, tool_type=tool_type, tool_description=tool_description, )._managed() def start_invoke_local_agent( self, provider: str, *, request_model: str | None = None, agent_name: str | None = None, ) -> AgentInvocation: """Create and start a local agent invocation (INTERNAL span kind). Use for agents running within the same process (e.g. LangChain, CrewAI). Set remaining attributes (agent_name, etc.) on the returned invocation, then call invocation.stop() or invocation.fail(). """ return AgentInvocation( self._tracer, self._metrics_recorder, self._logger, self._completion_hook, provider, span_kind=SpanKind.INTERNAL, request_model=request_model, agent_name=agent_name, ) def start_invoke_remote_agent( self, provider: str, *, request_model: str | None = None, server_address: str | None = None, server_port: int | None = None, agent_name: str | None = None, ) -> AgentInvocation: """Create and start a remote agent invocation (CLIENT span kind). Use for agents invoked over a remote service (e.g. OpenAI Assistants, AWS Bedrock). Set remaining attributes (agent_name, etc.) on the returned invocation, then call invocation.stop() or invocation.fail(). """ return AgentInvocation( self._tracer, self._metrics_recorder, self._logger, self._completion_hook, provider, span_kind=SpanKind.CLIENT, request_model=request_model, agent_name=agent_name, server_address=server_address, server_port=server_port, ) def invoke_local_agent( self, provider: str, *, request_model: str | None = None, agent_name: str | None = None, ) -> AbstractContextManager[AgentInvocation]: """Context manager for local agent invocations (INTERNAL span kind). Use for agents running within the same process (e.g. LangChain, CrewAI). Only set data attributes on the invocation object, do not modify the span or context. Starts the span on entry. On normal exit, finalizes the invocation and ends the span. If an exception occurs inside the context, marks the span as error, ends it, and re-raises the original exception. """ return self.start_invoke_local_agent( provider, request_model=request_model, agent_name=agent_name, )._managed() def invoke_remote_agent( self, provider: str, *, request_model: str | None = None, server_address: str | None = None, server_port: int | None = None, agent_name: str | None = None, ) -> AbstractContextManager[AgentInvocation]: """Context manager for remote agent invocations (CLIENT span kind). Use for agents invoked over a remote service (e.g. OpenAI Assistants, AWS Bedrock). Only set data attributes on the invocation object, do not modify the span or context. Starts the span on entry. On normal exit, finalizes the invocation and ends the span. If an exception occurs inside the context, marks the span as error, ends it, and re-raises the original exception. """ return self.start_invoke_remote_agent( provider, request_model=request_model, agent_name=agent_name, server_address=server_address, server_port=server_port, )._managed() def workflow( self, name: str | None = None, ) -> AbstractContextManager[WorkflowInvocation]: """Context manager for Workflow invocations. Only set data attributes on the invocation object, do not modify the span or context. Starts the span on entry. On normal exit, finalizes the invocation and ends the span. If an exception occurs inside the context, marks the span as error, ends it, and re-raises the original exception. """ return self.start_workflow(name=name)._managed() def get_telemetry_handler( tracer_provider: TracerProvider | None = None, meter_provider: MeterProvider | None = None, logger_provider: LoggerProvider | None = None, completion_hook: CompletionHook | None = None, ) -> TelemetryHandler: """ Returns a singleton TelemetryHandler instance. """ handler: TelemetryHandler | None = getattr(get_telemetry_handler, "_default_handler", None) if handler is None: handler = TelemetryHandler( tracer_provider=tracer_provider, meter_provider=meter_provider, logger_provider=logger_provider, completion_hook=completion_hook, ) setattr(get_telemetry_handler, "_default_handler", handler) return handler instruments.py000066400000000000000000000023311527070072300360620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.metrics import Histogram, Meter from opentelemetry.semconv._incubating.metrics import gen_ai_metrics _GEN_AI_CLIENT_OPERATION_DURATION_BUCKETS = [ 0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.56, 5.12, 10.24, 20.48, 40.96, 81.92, ] _GEN_AI_CLIENT_TOKEN_USAGE_BUCKETS = [ 1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864, ] def create_duration_histogram(meter: Meter) -> Histogram: return meter.create_histogram( name=gen_ai_metrics.GEN_AI_CLIENT_OPERATION_DURATION, description="Duration of GenAI client operation", unit="s", explicit_bucket_boundaries_advisory=_GEN_AI_CLIENT_OPERATION_DURATION_BUCKETS, ) def create_token_histogram(meter: Meter) -> Histogram: return meter.create_histogram( name=gen_ai_metrics.GEN_AI_CLIENT_TOKEN_USAGE, description="Number of input and output tokens used by GenAI clients", unit="{token}", explicit_bucket_boundaries_advisory=_GEN_AI_CLIENT_TOKEN_USAGE_BUCKETS, ) invocation.py000066400000000000000000000020601527070072300356370ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Public re-export of all GenAI invocation types. Users can import everything from this single module: from opentelemetry.util.genai.invocation import ( Error, GenAIInvocation, InferenceInvocation, EmbeddingInvocation, ToolInvocation, WorkflowInvocation, ) """ from opentelemetry.util.genai._agent_invocation import AgentInvocation from opentelemetry.util.genai._embedding_invocation import EmbeddingInvocation from opentelemetry.util.genai._inference_invocation import InferenceInvocation from opentelemetry.util.genai._invocation import ( ContextToken, Error, GenAIInvocation, ) from opentelemetry.util.genai._tool_invocation import ToolInvocation from opentelemetry.util.genai._workflow_invocation import WorkflowInvocation __all__ = [ "AgentInvocation", "ContextToken", "EmbeddingInvocation", "Error", "GenAIInvocation", "InferenceInvocation", "ToolInvocation", "WorkflowInvocation", ] metrics.py000066400000000000000000000034541527070072300351440ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Helpers for emitting GenAI metrics from LLM invocations.""" from __future__ import annotations import timeit from opentelemetry.metrics import Histogram, Meter from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAI, ) from opentelemetry.util.genai.instruments import ( create_duration_histogram, create_token_histogram, ) from ._invocation import GenAIInvocation class InvocationMetricsRecorder: """Records duration and token usage histograms for GenAI invocations.""" def __init__(self, meter: Meter): self._duration_histogram: Histogram = create_duration_histogram(meter) self._token_histogram: Histogram = create_token_histogram(meter) def record(self, invocation: GenAIInvocation) -> None: """Record duration and token metrics for an invocation if possible.""" attributes = invocation._get_metric_attributes() token_counts = invocation._get_metric_token_counts() duration_seconds: float | None = None if invocation._monotonic_start_s is not None: duration_seconds = max( timeit.default_timer() - invocation._monotonic_start_s, 0.0, ) if duration_seconds is not None: self._duration_histogram.record( duration_seconds, attributes=attributes, context=invocation._span_context, ) for token_type, token_count in token_counts.items(): self._token_histogram.record( token_count, attributes=attributes | {GenAI.GEN_AI_TOKEN_TYPE: token_type}, context=invocation._span_context, ) __all__ = ["InvocationMetricsRecorder"] stream.py000066400000000000000000000231301527070072300347620ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging from abc import ABCMeta, abstractmethod from collections.abc import AsyncIterable, Iterable from types import TracebackType from typing import ( TYPE_CHECKING, Generic, Literal, Protocol, TypeVar, ) if TYPE_CHECKING: class _ObjectProxy: def __init__(self, wrapped: object) -> None: ... else: from wrapt import ObjectProxy as _ObjectProxy ChunkT = TypeVar("ChunkT") _ChunkT_co = TypeVar("_ChunkT_co", covariant=True) _logger = logging.getLogger(__name__) class _StreamWrapperMeta(ABCMeta, type(_ObjectProxy)): """Metaclass compatible with wrapt's proxy type and ABC hooks.""" class _SyncStream(Iterable[_ChunkT_co], Protocol[_ChunkT_co]): """Structural type for streams accepted by ``SyncStreamWrapper``.""" def close(self) -> None: ... class _AsyncStream(AsyncIterable[_ChunkT_co], Protocol[_ChunkT_co]): """Structural type for streams accepted by ``AsyncStreamWrapper``.""" async def close(self) -> None: ... class SyncStreamWrapper( _ObjectProxy, Generic[ChunkT], metaclass=_StreamWrapperMeta, ): """Base class for synchronous instrumented stream wrappers. Subclass this when wrapping a provider SDK stream that is consumed with normal iteration. The subclass should pass the SDK stream to ``super().__init__(stream)`` and implement the three telemetry hooks: ``_process_chunk`` for per-chunk state, ``_on_stream_end`` for successful finalization, and ``_on_stream_error`` for failure finalization. Users should consume subclasses as normal streams, for example with ``for chunk in wrapper`` or ``with wrapper``. The hook methods are called internally by the wrapper lifecycle and are not part of the public API. """ def __init__(self, stream: _SyncStream[ChunkT]): super().__init__(stream) self._self_stream = stream self._self_iterator = iter(stream) self._self_finalized = False def __enter__(self): return self def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> Literal[False]: if exc_val is not None: self._safe_finalize_failure(exc_val) try: self._self_stream.close() except Exception: # pylint: disable=broad-exception-caught _logger.debug( "GenAI stream close error after user exception", exc_info=True, ) return False self.close() return False def close(self) -> None: try: self._self_stream.close() except Exception as error: self._safe_finalize_failure(error) raise self._safe_finalize_success() def __iter__(self): # Override ``ObjectProxy.__iter__`` so iteration drives ``__next__`` # below and runs ``_process_chunk`` per chunk; otherwise iteration # would be forwarded to the wrapped stream and bypass instrumentation. return self def __next__(self) -> ChunkT: try: chunk = next(self._self_iterator) except StopIteration: self._safe_finalize_success() raise except Exception as error: self._safe_finalize_failure(error) raise try: self._process_chunk(chunk) except Exception as error: # pylint: disable=broad-exception-caught self._handle_process_chunk_error(error) return chunk def _finalize_success(self) -> None: if self._self_finalized: return self._self_finalized = True self._on_stream_end() def _finalize_failure(self, error: BaseException) -> None: if self._self_finalized: return self._self_finalized = True self._on_stream_error(error) def _safe_finalize_success(self) -> None: try: self._finalize_success() except Exception: # pylint: disable=broad-exception-caught _logger.debug( "GenAI stream instrumentation error during finalization", exc_info=True, ) def _safe_finalize_failure(self, error: BaseException) -> None: try: self._finalize_failure(error) except Exception: # pylint: disable=broad-exception-caught _logger.debug( "GenAI stream instrumentation error during failure finalization", exc_info=True, ) @abstractmethod def _process_chunk(self, chunk: ChunkT) -> None: """Process one stream chunk for telemetry.""" @abstractmethod def _on_stream_end(self) -> None: """Finalize the stream successfully.""" @abstractmethod def _on_stream_error(self, error: BaseException) -> None: """Finalize the stream with failure.""" @staticmethod def _handle_process_chunk_error(_error: Exception) -> None: _logger.debug( "GenAI stream instrumentation error during chunk processing", exc_info=True, ) class AsyncStreamWrapper( _ObjectProxy, Generic[ChunkT], metaclass=_StreamWrapperMeta, ): """Base class for asynchronous instrumented stream wrappers. Subclass this when wrapping a provider SDK stream that is consumed with async iteration. The subclass should pass the SDK stream to ``super().__init__(stream)`` and implement the three telemetry hooks: ``_process_chunk`` for per-chunk state, ``_on_stream_end`` for successful finalization, and ``_on_stream_error`` for failure finalization. Users should consume subclasses as normal async streams, for example with ``async for chunk in wrapper`` or ``async with wrapper``. The hook methods remain synchronous telemetry hooks; async stream reads and close handling are owned by this base class. """ def __init__(self, stream: _AsyncStream[ChunkT]): super().__init__(stream) self._self_stream = stream self._self_aiter = aiter(stream) self._self_finalized = False async def __aenter__(self): return self async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> Literal[False]: if exc_val is not None: self._safe_finalize_failure(exc_val) try: await self._self_stream.close() except Exception: # pylint: disable=broad-exception-caught _logger.debug( "GenAI stream close error after user exception", exc_info=True, ) return False await self.close() return False async def close(self) -> None: # Named ``close`` (not ``aclose``) to match OpenAI's ``AsyncStream``. # Revisit when migrating SDKs that expose ``aclose`` instead. try: await self._self_stream.close() except Exception as error: self._safe_finalize_failure(error) raise self._safe_finalize_success() def __aiter__(self): # Override ``ObjectProxy.__aiter__`` so iteration drives ``__anext__`` # below and runs ``_process_chunk`` per chunk; otherwise iteration # would be forwarded to the wrapped stream and bypass instrumentation. return self async def __anext__(self) -> ChunkT: try: chunk = await anext(self._self_aiter) except StopAsyncIteration: self._safe_finalize_success() raise except Exception as error: self._safe_finalize_failure(error) raise try: self._process_chunk(chunk) except Exception as error: # pylint: disable=broad-exception-caught self._handle_process_chunk_error(error) return chunk def _finalize_success(self) -> None: if self._self_finalized: return self._self_finalized = True self._on_stream_end() def _finalize_failure(self, error: BaseException) -> None: if self._self_finalized: return self._self_finalized = True self._on_stream_error(error) def _safe_finalize_success(self) -> None: try: self._finalize_success() except Exception: # pylint: disable=broad-exception-caught _logger.debug( "GenAI stream instrumentation error during finalization", exc_info=True, ) def _safe_finalize_failure(self, error: BaseException) -> None: try: self._finalize_failure(error) except Exception: # pylint: disable=broad-exception-caught _logger.debug( "GenAI stream instrumentation error during failure finalization", exc_info=True, ) @abstractmethod def _process_chunk(self, chunk: ChunkT) -> None: """Process one stream chunk for telemetry.""" @abstractmethod def _on_stream_end(self) -> None: """Finalize the stream successfully.""" @abstractmethod def _on_stream_error(self, error: BaseException) -> None: """Finalize the stream with failure.""" @staticmethod def _handle_process_chunk_error(_error: Exception) -> None: _logger.debug( "GenAI stream instrumentation error during chunk processing", exc_info=True, ) __all__ = [ "AsyncStreamWrapper", "SyncStreamWrapper", ] types.py000066400000000000000000000161061527070072300346400ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from dataclasses import dataclass from enum import Enum from typing import TYPE_CHECKING, Any, Literal if TYPE_CHECKING: from opentelemetry.util.genai._inference_invocation import ( # pylint: disable=useless-import-alias LLMInvocation as LLMInvocation, # noqa: PLC0414 ) from opentelemetry.util.genai._invocation import ( # pylint: disable=useless-import-alias GenAIInvocation as GenAIInvocation, # noqa: PLC0414 ) class ContentCapturingMode(Enum): # Do not capture content (default). NO_CONTENT = 0 # Only capture content in spans. SPAN_ONLY = 1 # Only capture content in events. EVENT_ONLY = 2 # Capture content in both spans and events. SPAN_AND_EVENT = 3 @dataclass() class GenericPart: """Used for provider-specific message part types that don't match the standard MessagePart types defined in semantic conventions. Wrap custom types with GenericPart(value=...) to explicitly opt-in to non-standard types. This will be removed in a future version when all instrumentations use core types.""" value: Any type: Literal["generic"] = "generic" @dataclass() class ToolCallRequest: """Represents a tool call requested by the model (message part only). Use this for tool calls in message history. For execution tracking with spans and metrics, use ToolInvocation instead. This model is specified as part of semconv in `GenAI messages Python models - ToolCallRequestPart `__. """ arguments: Any name: str id: str | None type: Literal["tool_call"] = "tool_call" @dataclass() class ToolCallResponse: """Represents a tool call result sent to the model or a built-in tool call outcome and details This model is specified as part of semconv in `GenAI messages Python models - ToolCallResponsePart `__. """ response: Any id: str | None type: Literal["tool_call_response"] = "tool_call_response" @dataclass() class ServerToolCall: """Represents a server-side tool call. Server tool calls are executed by the model provider on the server side rather than by the client application. Provider-specific tools (e.g., code_interpreter, web_search) can have well-defined schemas defined by the respective providers. This model is specified as part of semconv in `GenAI messages Python models - ServerToolCallPart `__. """ name: str server_tool_call: Any id: str | None = None type: Literal["server_tool_call"] = "server_tool_call" @dataclass() class ServerToolCallResponse: """Represents a server-side tool call response. Contains the outcome and details of a server tool execution. Provider-specific tools (e.g., code_interpreter, web_search) can have well-defined response schemas defined by the respective providers. This model is specified as part of semconv in `GenAI messages Python models - ServerToolCallResponsePart `__. """ server_tool_call_response: Any id: str | None = None type: Literal["server_tool_call_response"] = "server_tool_call_response" @dataclass() class Text: """Represents text content sent to or received from the model This model is specified as part of semconv in `GenAI messages Python models - TextPart `__. """ content: str type: Literal["text"] = "text" @dataclass() class Reasoning: """Represents reasoning/thinking content received from the model This model is specified as part of semconv in `GenAI messages Python models - ReasoningPart `__. """ content: str type: Literal["reasoning"] = "reasoning" Modality = Literal["image", "video", "audio"] @dataclass() class Blob: """Represents blob binary data sent inline to the model This model is specified as part of semconv in `GenAI messages Python models - BlobPart `__. """ mime_type: str | None modality: Modality | str content: bytes type: Literal["blob"] = "blob" @dataclass() class File: """Represents an external referenced file sent to the model by file id This model is specified as part of semconv in `GenAI messages Python models - FilePart `__. """ mime_type: str | None modality: Modality | str file_id: str type: Literal["file"] = "file" @dataclass() class Uri: """Represents an external referenced file sent to the model by URI This model is specified as part of semconv in `GenAI messages Python models - UriPart `__. """ mime_type: str | None modality: Modality | str uri: str type: Literal["uri"] = "uri" @dataclass() class FunctionToolDefinition: """Represents a function tool definition sent to the model""" name: str description: str | None parameters: Any type: Literal["function"] = "function" @dataclass() class GenericToolDefinition: """Represents a generic tool definition sent to the model""" name: str type: str ToolDefinition = FunctionToolDefinition | GenericToolDefinition MessagePart = ( Text | ToolCallRequest | ToolCallResponse | ServerToolCall | ServerToolCallResponse | Blob | File | Uri | Reasoning | GenericPart # For provider-specific types; prefer standard types above ) FinishReason = Literal["content_filter", "error", "length", "stop", "tool_calls"] @dataclass() class InputMessage: role: str parts: list[MessagePart] @dataclass() class OutputMessage: role: str parts: list[MessagePart] finish_reason: str | FinishReason @dataclass class Error: message: str type: type[BaseException] def __getattr__(name: str) -> object: if name == "GenAIInvocation": import opentelemetry.util.genai.invocation as _inv # pylint: disable=import-outside-toplevel # noqa: PLC0415 return _inv.GenAIInvocation if name == "LLMInvocation": from opentelemetry.util.genai._inference_invocation import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415 LLMInvocation, ) return LLMInvocation raise AttributeError(f"module {__name__!r} has no attribute {name!r}") utils.py000066400000000000000000000113361527070072300346340ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import logging import os from base64 import b64encode from functools import partial from typing import Any from opentelemetry.instrumentation._semconv import ( _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, _StabilityMode, ) from opentelemetry.util.genai.environment_variables import ( OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT, ) from opentelemetry.util.genai.types import ContentCapturingMode logger = logging.getLogger(__name__) def is_experimental_mode() -> bool: return ( _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( _OpenTelemetryStabilitySignalType.GEN_AI, ) is _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL ) def get_content_capturing_mode() -> ContentCapturingMode: """This function should not be called when GEN_AI stability mode is set to DEFAULT. When the GEN_AI stability mode is DEFAULT this function will raise a ValueError -- see the code below.""" envvar = os.environ.get(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT) if not is_experimental_mode(): raise ValueError("This function should never be called when StabilityMode is not experimental.") if not envvar: return ContentCapturingMode.NO_CONTENT try: return ContentCapturingMode[envvar.upper()] except KeyError: logger.warning( "%s is not a valid option for `%s` environment variable. Must be one of %s. Defaulting to `NO_CONTENT`.", envvar, OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, ", ".join(e.name for e in ContentCapturingMode), ) return ContentCapturingMode.NO_CONTENT def should_emit_event() -> bool: """Check if event emission is enabled. Returns True if event emission is enabled, False otherwise. If the environment variable OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT is explicitly set, its value takes precedence. Otherwise, the default value is determined by OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: - NO_CONTENT or SPAN_ONLY: defaults to False - EVENT_ONLY or SPAN_AND_EVENT: defaults to True """ envvar = os.environ.get(OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT) # If explicitly set (and not empty), use the user's value (highest priority) if envvar and envvar.strip(): envvar_lower = envvar.lower().strip() if envvar_lower == "true": return True if envvar_lower == "false": return False logger.warning( "%s is not a valid option for `%s` environment variable. Must be one of true or false (case-insensitive). Defaulting based on content capturing mode.", envvar, OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT, ) # Invalid value falls through to default logic below # If not explicitly set (or invalid), determine default based on content capturing mode try: if not is_experimental_mode(): # Not in experimental mode, default to False return False content_mode = get_content_capturing_mode() # EVENT_ONLY and SPAN_AND_EVENT require events, so default to True if content_mode in ( ContentCapturingMode.EVENT_ONLY, ContentCapturingMode.SPAN_AND_EVENT, ): return True # NO_CONTENT and SPAN_ONLY don't require events, so default to False return False except ValueError: # If get_content_capturing_mode raises ValueError (not in experimental mode), # default to False return False def should_capture_content_on_spans_in_experimental_mode() -> bool: """Return True when content conversion should be performed.""" if not is_experimental_mode(): return False mode = get_content_capturing_mode() if mode == ContentCapturingMode.NO_CONTENT: return False if mode == ContentCapturingMode.EVENT_ONLY and not should_emit_event(): return False return True class _GenAiJsonEncoder(json.JSONEncoder): def default(self, o: Any) -> Any: if isinstance(o, bytes): return b64encode(o).decode() return super().default(o) gen_ai_json_dump = partial(json.dump, separators=(",", ":"), cls=_GenAiJsonEncoder) """Should be used by GenAI instrumentations when serializing objects that may contain bytes, datetimes, etc. for GenAI observability.""" gen_ai_json_dumps = partial(json.dumps, separators=(",", ":"), cls=_GenAiJsonEncoder) """Should be used by GenAI instrumentations when serializing objects that may contain bytes, datetimes, etc. for GenAI observability.""" version.py000066400000000000000000000001471527070072300351570ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/src/opentelemetry/util/genai# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.5b0.dev" opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/test-requirements.txt000066400000000000000000000001001527070072300317020ustar00rootroot00000000000000pytest==7.4.4 fsspec==2025.9.0 -e opentelemetry-instrumentation opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/tests/000077500000000000000000000000001527070072300266145ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/tests/__init__.py000066400000000000000000000000001527070072300307130ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/tests/test_completion_hook.py000066400000000000000000000111171527070072300334170ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging from collections.abc import Callable from dataclasses import dataclass from typing import Any from unittest import TestCase from unittest.mock import Mock, patch from opentelemetry.util.genai.completion_hook import ( CompletionHook, _NoOpCompletionHook, _SafeCompletionHook, load_completion_hook, ) from opentelemetry.util.genai.environment_variables import ( OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK, ) class FakeCompletionHook(CompletionHook): def on_completion(self, **kwargs: Any): pass class InvalidCompletionHook: pass @dataclass class FakeEntryPoint: name: str load: Callable[[], type[CompletionHook]] class TestCompletionHook(TestCase): @patch.dict("os.environ", {}) def test_load_completion_hook_noop(self): self.assertIsInstance(load_completion_hook(), _NoOpCompletionHook) @patch( "opentelemetry.util.genai.completion_hook.entry_points", ) @patch.dict("os.environ", {OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK: "my-hook"}) def test_load_completion_hook_custom(self, mock_entry_points: Mock): mock_entry_points.return_value = [FakeEntryPoint("my-hook", lambda: FakeCompletionHook)] hook = load_completion_hook() self.assertIsInstance(hook, _SafeCompletionHook) self.assertIsInstance(hook._wrapped, FakeCompletionHook) @patch("opentelemetry.util.genai.completion_hook.entry_points") @patch.dict("os.environ", {OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK: "my-hook"}) def test_load_completion_hook_invalid(self, mock_entry_points: Mock): mock_entry_points.return_value = [FakeEntryPoint("my-hook", lambda: InvalidCompletionHook)] with self.assertLogs(level=logging.DEBUG) as logs: self.assertIsInstance(load_completion_hook(), _NoOpCompletionHook) self.assertEqual(len(logs.output), 1) self.assertIn("is not a valid CompletionHook. Using noop", logs.output[0]) @patch("opentelemetry.util.genai.completion_hook.entry_points") @patch.dict("os.environ", {OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK: "my-hook"}) def test_load_completion_hook_error(self, mock_entry_points: Mock): def load(): raise RuntimeError("error") mock_entry_points.return_value = [FakeEntryPoint("my-hook", load)] self.assertIsInstance(load_completion_hook(), _NoOpCompletionHook) @patch("opentelemetry.util.genai.completion_hook.entry_points") @patch.dict("os.environ", {OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK: "my-hook"}) def test_load_completion_hook_not_found(self, mock_entry_points: Mock): mock_entry_points.return_value = [FakeEntryPoint("other-hook", lambda: FakeCompletionHook)] self.assertIsInstance(load_completion_hook(), _NoOpCompletionHook) class TestSafeCompletionHook(TestCase): def test_passes_arguments_to_wrapped_hook(self): wrapped = Mock(spec=CompletionHook) safe = _SafeCompletionHook(wrapped) kwargs = { "inputs": [], "outputs": [], "system_instruction": [], "span": None, "log_record": None, } safe.on_completion(**kwargs) self.assertEqual(wrapped.on_completion.call_count, 1) self.assertEqual(wrapped.on_completion.call_args.kwargs, kwargs) def test_swallows_exception_from_wrapped_hook(self): class RaisingCompletionHook(CompletionHook): def on_completion(self, **kwargs: Any) -> None: raise RuntimeError("boom") safe = _SafeCompletionHook(RaisingCompletionHook()) with self.assertLogs("opentelemetry.util.genai.completion_hook", level=logging.WARNING) as logs: safe.on_completion( inputs=[], outputs=[], system_instruction=[], ) self.assertEqual(len(logs.records), 1) self.assertEqual(logs.records[0].levelno, logging.WARNING) self.assertIn("raised an exception", logs.records[0].getMessage()) self.assertIsInstance(logs.records[0].exc_info[1], RuntimeError) def test_does_not_swallow_keyboard_interrupt(self): class InterruptingCompletionHook(CompletionHook): def on_completion(self, **kwargs: Any) -> None: raise KeyboardInterrupt safe = _SafeCompletionHook(InterruptingCompletionHook()) with self.assertRaises(KeyboardInterrupt): safe.on_completion( inputs=[], outputs=[], system_instruction=[], ) opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/tests/test_events_options.py000066400000000000000000000202561527070072300333110ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import unittest from unittest.mock import patch from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.util.genai.environment_variables import ( OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT, ) from opentelemetry.util.genai.utils import ( should_capture_content_on_spans_in_experimental_mode, should_emit_event, ) def patch_env_vars(stability_mode, content_capturing, emit_event): def decorator(test_case): @patch.dict( os.environ, { OTEL_SEMCONV_STABILITY_OPT_IN: stability_mode, OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: content_capturing, OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT: emit_event, }, ) def wrapper(*args, **kwargs): # Reset state. _OpenTelemetrySemanticConventionStability._initialized = False _OpenTelemetrySemanticConventionStability._initialize() return test_case(*args, **kwargs) return wrapper return decorator class TestShouldEmitEvent(unittest.TestCase): @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="EVENT_ONLY", emit_event="true", ) def test_should_emit_event_returns_true_when_set_to_true( self, ): # pylint: disable=no-self-use assert should_emit_event() is True @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="EVENT_ONLY", emit_event="True", ) def test_should_emit_event_case_insensitive_true( self, ): # pylint: disable=no-self-use assert should_emit_event() is True @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="EVENT_ONLY", emit_event="false", ) def test_should_emit_event_returns_false_when_set_to_false( self, ): # pylint: disable=no-self-use assert should_emit_event() is False @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="EVENT_ONLY", emit_event="False", ) def test_should_emit_event_case_insensitive_false( self, ): # pylint: disable=no-self-use assert should_emit_event() is False @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="EVENT_ONLY", emit_event="", ) def test_should_emit_event_defaults_to_true_for_event_only( self, ): # pylint: disable=no-self-use # When EVENT_ONLY is set, should_emit_event should default to True assert should_emit_event() is True @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="SPAN_AND_EVENT", emit_event="", ) def test_should_emit_event_defaults_to_true_for_span_and_event( self, ): # pylint: disable=no-self-use # When SPAN_AND_EVENT is set, should_emit_event should default to True assert should_emit_event() is True @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="NO_CONTENT", emit_event="", ) def test_should_emit_event_defaults_to_false_for_no_content( self, ): # pylint: disable=no-self-use # When NO_CONTENT is set, should_emit_event should default to False assert should_emit_event() is False @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="SPAN_ONLY", emit_event="", ) def test_should_emit_event_defaults_to_false_for_span_only( self, ): # pylint: disable=no-self-use # When SPAN_ONLY is set, should_emit_event should default to False assert should_emit_event() is False @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="", emit_event="", ) def test_should_emit_event_defaults_to_false_when_no_content_capturing_set( self, ): # pylint: disable=no-self-use # When content_capturing is not set (defaults to NO_CONTENT), # should_emit_event should default to False assert should_emit_event() is False @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="EVENT_ONLY", emit_event="INVALID_VALUE", ) def test_should_emit_event_with_invalid_value_falls_back_to_default( self, ): # pylint: disable=no-self-use # When invalid value is set, should fall back to default based on content_capturing_mode # EVENT_ONLY should default to True with self.assertLogs(level="WARNING") as cm: result = should_emit_event() assert result is True, f"Expected True but got {result} (EVENT_ONLY should default to True)" self.assertEqual(len(cm.output), 1) self.assertIn("INVALID_VALUE is not a valid option for", cm.output[0]) self.assertIn("Must be one of true or false (case-insensitive)", cm.output[0]) @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="SPAN_ONLY", emit_event="INVALID_VALUE", ) def test_should_emit_event_with_invalid_value_falls_back_to_false_for_span_only( self, ): # pylint: disable=no-self-use # When invalid value is set with SPAN_ONLY, should default to False with self.assertLogs(level="WARNING") as cm: result = should_emit_event() assert result is False, f"Expected False but got {result} (SPAN_ONLY should default to False)" self.assertEqual(len(cm.output), 1) self.assertIn("INVALID_VALUE is not a valid option for", cm.output[0]) @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="EVENT_ONLY", emit_event="false", ) def test_should_emit_event_user_setting_overrides_default( self, ): # pylint: disable=no-self-use # User explicitly setting emit_event="false" should override the default (True for EVENT_ONLY) assert should_emit_event() is False @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="NO_CONTENT", emit_event="true", ) def test_should_emit_event_user_setting_overrides_default_for_no_content( self, ): # pylint: disable=no-self-use # User explicitly setting emit_event="true" should override the default (False for NO_CONTENT) assert should_emit_event() is True class TestShouldCaptureContent(unittest.TestCase): @patch_env_vars( stability_mode="default", content_capturing="SPAN_AND_EVENT", emit_event="true", ) def test_should_capture_content_on_spans_in_experimental_mode_false_when_not_experimental( self, ): # pylint: disable=no-self-use assert should_capture_content_on_spans_in_experimental_mode() is False @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="NO_CONTENT", emit_event="true", ) def test_should_capture_content_on_spans_in_experimental_mode_false_when_no_content( self, ): # pylint: disable=no-self-use assert should_capture_content_on_spans_in_experimental_mode() is False @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="EVENT_ONLY", emit_event="false", ) def test_should_capture_content_on_spans_in_experimental_mode_false_when_event_only_but_event_disabled( self, ): # pylint: disable=no-self-use assert should_capture_content_on_spans_in_experimental_mode() is False @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="SPAN_ONLY", emit_event="", ) def test_should_capture_content_on_spans_in_experimental_mode_true_for_span_only( self, ): # pylint: disable=no-self-use assert should_capture_content_on_spans_in_experimental_mode() is True opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/tests/test_handler_agent.py000066400000000000000000000605221527070072300330250ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import unittest from unittest.mock import patch from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) from opentelemetry.sdk.trace.sampling import Decision, SamplingResult from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAI, ) from opentelemetry.semconv.attributes import server_attributes from opentelemetry.test.test_base import TestBase from opentelemetry.trace import INVALID_SPAN, SpanKind from opentelemetry.util.genai.handler import TelemetryHandler from opentelemetry.util.genai.types import ( ContentCapturingMode, Error, FunctionToolDefinition, InputMessage, OutputMessage, Text, ) class TestLocalAgentInvocation(unittest.TestCase): # pylint: disable=too-many-public-methods def setUp(self): self.span_exporter = InMemorySpanExporter() tracer_provider = TracerProvider() tracer_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter)) self.handler = TelemetryHandler(tracer_provider=tracer_provider) def test_start_stop_creates_span(self): invocation = self.handler.start_invoke_local_agent( "openai", request_model="gpt-4", agent_name="Math Tutor", ) invocation.stop() spans = self.span_exporter.get_finished_spans() assert len(spans) == 1 span = spans[0] assert span.name == "invoke_agent Math Tutor" assert span.attributes[GenAI.GEN_AI_OPERATION_NAME] == "invoke_agent" assert span.attributes[GenAI.GEN_AI_AGENT_NAME] == "Math Tutor" assert span.attributes[GenAI.GEN_AI_PROVIDER_NAME] == "openai" assert span.attributes[GenAI.GEN_AI_REQUEST_MODEL] == "gpt-4" def test_span_kind_internal(self): invocation = self.handler.start_invoke_local_agent("openai") invocation.stop() assert self.span_exporter.get_finished_spans()[0].kind == SpanKind.INTERNAL def test_no_server_attributes(self): invocation = self.handler.start_invoke_local_agent("openai") invocation.stop() attrs = self.span_exporter.get_finished_spans()[0].attributes assert server_attributes.SERVER_ADDRESS not in attrs assert server_attributes.SERVER_PORT not in attrs def test_all_attributes(self): invocation = self.handler.start_invoke_local_agent( "openai", request_model="gpt-4", ) invocation.agent_name = "Full Agent" invocation.agent_id = "agent-123" invocation.agent_description = "A test agent" invocation.agent_version = "1.0.0" invocation.conversation_id = "conv-456" invocation.data_source_id = "ds-789" invocation.output_type = "text" invocation.temperature = 0.7 invocation.top_p = 0.9 invocation.frequency_penalty = 0.5 invocation.presence_penalty = 0.3 invocation.max_tokens = 1000 invocation.stop_sequences = ["END", "STOP"] invocation.seed = 42 invocation.choice_count = 3 invocation.finish_reasons = ["stop"] invocation.input_tokens = 100 invocation.output_tokens = 200 invocation.stop() attrs = self.span_exporter.get_finished_spans()[0].attributes assert attrs[GenAI.GEN_AI_AGENT_NAME] == "Full Agent" assert attrs[GenAI.GEN_AI_AGENT_ID] == "agent-123" assert attrs[GenAI.GEN_AI_AGENT_DESCRIPTION] == "A test agent" assert attrs[GenAI.GEN_AI_AGENT_VERSION] == "1.0.0" assert attrs[GenAI.GEN_AI_USAGE_INPUT_TOKENS] == 100 assert attrs[GenAI.GEN_AI_USAGE_OUTPUT_TOKENS] == 200 assert attrs[GenAI.GEN_AI_CONVERSATION_ID] == "conv-456" assert attrs[GenAI.GEN_AI_DATA_SOURCE_ID] == "ds-789" assert attrs[GenAI.GEN_AI_OUTPUT_TYPE] == "text" assert attrs[GenAI.GEN_AI_REQUEST_TEMPERATURE] == 0.7 assert attrs[GenAI.GEN_AI_REQUEST_TOP_P] == 0.9 assert attrs[GenAI.GEN_AI_REQUEST_FREQUENCY_PENALTY] == 0.5 assert attrs[GenAI.GEN_AI_REQUEST_PRESENCE_PENALTY] == 0.3 assert attrs[GenAI.GEN_AI_REQUEST_MAX_TOKENS] == 1000 assert attrs[GenAI.GEN_AI_REQUEST_STOP_SEQUENCES] == ("END", "STOP") assert attrs[GenAI.GEN_AI_REQUEST_SEED] == 42 assert attrs[GenAI.GEN_AI_REQUEST_CHOICE_COUNT] == 3 assert attrs[GenAI.GEN_AI_RESPONSE_FINISH_REASONS] == ("stop",) def test_finish_reasons_multiple(self): invocation = self.handler.start_invoke_local_agent("openai") invocation.finish_reasons = ["stop", "length"] invocation.stop() attrs = self.span_exporter.get_finished_spans()[0].attributes assert attrs[GenAI.GEN_AI_RESPONSE_FINISH_REASONS] == ( "stop", "length", ) def test_finish_reasons_empty_list_omitted(self): invocation = self.handler.start_invoke_local_agent("openai") invocation.finish_reasons = [] invocation.stop() attrs = self.span_exporter.get_finished_spans()[0].attributes assert GenAI.GEN_AI_RESPONSE_MODEL not in attrs assert GenAI.GEN_AI_RESPONSE_FINISH_REASONS not in attrs def test_cache_token_attributes(self): invocation = self.handler.start_invoke_local_agent("openai") invocation.input_tokens = 100 invocation.cache_creation_input_tokens = 25 invocation.cache_read_input_tokens = 50 invocation.stop() attrs = self.span_exporter.get_finished_spans()[0].attributes assert attrs[GenAI.GEN_AI_USAGE_INPUT_TOKENS] == 100 assert attrs[GenAI.GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] == 25 assert attrs[GenAI.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] == 50 def test_fail_sets_error_status(self): invocation = self.handler.start_invoke_local_agent("openai") invocation.fail(RuntimeError("agent crashed")) span = self.span_exporter.get_finished_spans()[0] assert span.status.description == "agent crashed" assert span.attributes.get("error.type") == "RuntimeError" def test_context_manager_success(self): with self.handler.invoke_local_agent("openai", request_model="gpt-4", agent_name="CM Agent") as inv: inv.input_tokens = 10 inv.output_tokens = 20 assert self.span_exporter.get_finished_spans()[0].name == "invoke_agent CM Agent" def test_context_manager_error(self): with self.assertRaises(ValueError): with self.handler.invoke_local_agent("openai"): raise ValueError("test error") assert self.span_exporter.get_finished_spans()[0].attributes.get("error.type") == "ValueError" def test_context_manager_default_invocation(self): with self.handler.invoke_local_agent("openai") as inv: inv.agent_name = "Dynamic Agent" assert len(self.span_exporter.get_finished_spans()) == 1 def test_default_values(self): invocation = self.handler.start_invoke_local_agent("openai") invocation.stop() assert invocation._operation_name == "invoke_agent" assert invocation.agent_name is None assert invocation.provider == "openai" assert invocation.request_model is None assert not invocation.input_messages assert not invocation.output_messages assert invocation.tool_definitions is None assert invocation.cache_creation_input_tokens is None assert invocation.cache_read_input_tokens is None assert invocation.span is not INVALID_SPAN assert not invocation.attributes def test_with_messages(self): invocation = self.handler.start_invoke_local_agent("openai") invocation.input_messages = [InputMessage(role="user", parts=[Text(content="Hello")])] invocation.output_messages = [ OutputMessage( role="assistant", parts=[Text(content="Hi there!")], finish_reason="stop", ) ] invocation.stop() assert len(invocation.input_messages) == 1 assert invocation.input_messages[0].role == "user" def test_custom_attributes(self): invocation = self.handler.start_invoke_local_agent("openai") invocation.attributes["custom.key"] = "custom_value" invocation.stop() spans = self.span_exporter.get_finished_spans() assert spans[0].attributes["custom.key"] == "custom_value" def test_tool_definitions_type(self): tool = FunctionToolDefinition( name="get_weather", description="Get the weather", parameters={"type": "object", "properties": {}}, ) invocation = self.handler.start_invoke_local_agent("openai") invocation.tool_definitions = [tool] invocation.stop() assert len(invocation.tool_definitions) == 1 assert invocation.tool_definitions[0].name == "get_weather" assert invocation.tool_definitions[0].type == "function" def test_default_lists_are_independent(self): inv1 = self.handler.start_invoke_local_agent("openai") inv2 = self.handler.start_invoke_local_agent("openai") inv1.input_messages.append(InputMessage(role="user", parts=[])) assert len(inv2.input_messages) == 0 inv2.stop() inv1.stop() def test_default_attributes_are_independent(self): inv1 = self.handler.start_invoke_local_agent("openai") inv2 = self.handler.start_invoke_local_agent("openai") inv1.attributes["foo"] = "bar" assert "foo" not in inv2.attributes inv2.stop() inv1.stop() def test_agent_name_set_after_construction(self): invocation = self.handler.start_invoke_local_agent("openai") invocation.agent_name = "Named Agent" invocation.stop() span = self.span_exporter.get_finished_spans()[0] # Span name is not updated after construction — agent_name should be # passed at construction time for correct span naming and sampling. assert span.name == "invoke_agent" assert span.attributes[GenAI.GEN_AI_AGENT_NAME] == "Named Agent" def test_agent_name_passed_at_construction(self): invocation = self.handler.start_invoke_local_agent("openai", agent_name="Constructor Agent") invocation.stop() span = self.span_exporter.get_finished_spans()[0] assert span.name == "invoke_agent Constructor Agent" assert span.attributes[GenAI.GEN_AI_AGENT_NAME] == "Constructor Agent" def test_agent_name_at_construction_available_to_sampler(self): captured_attributes = {} class AttributeCapturingSampler: # pylint: disable=no-self-use def should_sample( self, parent_context, trace_id, name, kind=None, attributes=None, links=None, ): captured_attributes.update(attributes or {}) return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes) def get_description(self): return "AttributeCapturingSampler" sampler_provider = TracerProvider(sampler=AttributeCapturingSampler()) sampler_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter)) handler = TelemetryHandler(tracer_provider=sampler_provider) invocation = handler.start_invoke_local_agent("openai", agent_name="Sampler Agent") invocation.stop() assert captured_attributes[GenAI.GEN_AI_AGENT_NAME] == "Sampler Agent" def test_agent_name_omitted_not_in_sampler_attributes(self): captured_attributes = {} class AttributeCapturingSampler: # pylint: disable=no-self-use def should_sample( self, parent_context, trace_id, name, kind=None, attributes=None, links=None, ): captured_attributes.update(attributes or {}) return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes) def get_description(self): return "AttributeCapturingSampler" sampler_provider = TracerProvider(sampler=AttributeCapturingSampler()) sampler_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter)) handler = TelemetryHandler(tracer_provider=sampler_provider) invocation = handler.start_invoke_local_agent("openai") invocation.stop() assert GenAI.GEN_AI_AGENT_NAME not in captured_attributes class TestAgentInvocationContent(unittest.TestCase): def setUp(self): self.span_exporter = InMemorySpanExporter() tracer_provider = TracerProvider() tracer_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter)) self.handler = TelemetryHandler(tracer_provider=tracer_provider) @patch( "opentelemetry.util.genai._invocation.get_content_capturing_mode", return_value=ContentCapturingMode.SPAN_AND_EVENT, ) @patch( "opentelemetry.util.genai._invocation.is_experimental_mode", return_value=True, ) def test_system_instruction_on_span(self, _mock_exp, _mock_cap): invocation = self.handler.start_invoke_local_agent("openai") invocation.system_instruction = [ Text(content="You are a helpful assistant."), ] invocation.stop() attrs = self.span_exporter.get_finished_spans()[0].attributes assert GenAI.GEN_AI_SYSTEM_INSTRUCTIONS in attrs @patch( "opentelemetry.util.genai._invocation.get_content_capturing_mode", return_value=ContentCapturingMode.SPAN_AND_EVENT, ) @patch( "opentelemetry.util.genai._invocation.is_experimental_mode", return_value=True, ) def test_tool_definitions_on_span(self, _mock_exp, _mock_cap): tool = FunctionToolDefinition( name="get_weather", description="Get the weather", parameters={"type": "object", "properties": {}}, ) invocation = self.handler.start_invoke_local_agent("openai") invocation.tool_definitions = [tool] invocation.stop() attrs = self.span_exporter.get_finished_spans()[0].attributes assert GenAI.GEN_AI_TOOL_DEFINITIONS in attrs @patch( "opentelemetry.util.genai._invocation.get_content_capturing_mode", return_value=ContentCapturingMode.SPAN_AND_EVENT, ) @patch( "opentelemetry.util.genai._invocation.is_experimental_mode", return_value=True, ) def test_messages_on_span(self, _mock_exp, _mock_cap): invocation = self.handler.start_invoke_local_agent("openai") invocation.input_messages = [InputMessage(role="user", parts=[Text(content="Hello")])] invocation.output_messages = [ OutputMessage( role="assistant", parts=[Text(content="Hi!")], finish_reason="stop", ) ] invocation.stop() attrs = self.span_exporter.get_finished_spans()[0].attributes assert GenAI.GEN_AI_INPUT_MESSAGES in attrs assert GenAI.GEN_AI_OUTPUT_MESSAGES in attrs def test_content_not_on_span_by_default(self): invocation = self.handler.start_invoke_local_agent("openai") invocation.system_instruction = [ Text(content="You are a helpful assistant."), ] invocation.input_messages = [InputMessage(role="user", parts=[Text(content="Hello")])] invocation.stop() attrs = self.span_exporter.get_finished_spans()[0].attributes assert GenAI.GEN_AI_SYSTEM_INSTRUCTIONS not in attrs assert GenAI.GEN_AI_INPUT_MESSAGES not in attrs class TestRemoteAgentInvocation(unittest.TestCase): def setUp(self): self.span_exporter = InMemorySpanExporter() tracer_provider = TracerProvider() tracer_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter)) self.handler = TelemetryHandler(tracer_provider=tracer_provider) def test_span_kind_client(self): invocation = self.handler.start_invoke_remote_agent("openai") invocation.stop() assert self.span_exporter.get_finished_spans()[0].kind == SpanKind.CLIENT def test_server_attributes(self): invocation = self.handler.start_invoke_remote_agent( "openai", server_address="api.openai.com", server_port=443, ) invocation.stop() attrs = self.span_exporter.get_finished_spans()[0].attributes assert attrs[server_attributes.SERVER_ADDRESS] == "api.openai.com" assert attrs[server_attributes.SERVER_PORT] == 443 def test_all_attributes(self): invocation = self.handler.start_invoke_remote_agent( "openai", request_model="gpt-4", server_address="api.openai.com", server_port=443, ) invocation.agent_name = "Remote Agent" invocation.agent_id = "agent-123" invocation.agent_description = "A remote test agent" invocation.agent_version = "1.0.0" invocation.input_tokens = 100 invocation.output_tokens = 200 invocation.stop() attrs = self.span_exporter.get_finished_spans()[0].attributes assert attrs[GenAI.GEN_AI_AGENT_NAME] == "Remote Agent" assert attrs[GenAI.GEN_AI_AGENT_ID] == "agent-123" assert attrs[GenAI.GEN_AI_AGENT_DESCRIPTION] == "A remote test agent" assert attrs[GenAI.GEN_AI_AGENT_VERSION] == "1.0.0" assert attrs[GenAI.GEN_AI_USAGE_INPUT_TOKENS] == 100 assert attrs[GenAI.GEN_AI_USAGE_OUTPUT_TOKENS] == 200 assert attrs[GenAI.GEN_AI_REQUEST_MODEL] == "gpt-4" def test_fail_sets_error_status(self): invocation = self.handler.start_invoke_remote_agent("openai") invocation.fail(RuntimeError("remote agent crashed")) span = self.span_exporter.get_finished_spans()[0] assert span.status.description == "remote agent crashed" assert span.attributes.get("error.type") == "RuntimeError" def test_context_manager_success(self): with self.handler.invoke_remote_agent( "openai", request_model="gpt-4", server_address="api.openai.com", agent_name="CM Remote Agent", ) as _inv: pass span = self.span_exporter.get_finished_spans()[0] assert span.name == "invoke_agent CM Remote Agent" assert span.kind == SpanKind.CLIENT def test_context_manager_error(self): with self.assertRaises(ValueError): with self.handler.invoke_remote_agent("openai"): raise ValueError("remote error") assert self.span_exporter.get_finished_spans()[0].attributes.get("error.type") == "ValueError" def test_start_invoke_agent_passes_sampling_attributes_at_span_creation( self, ): """Verify that sampling-relevant attributes are available at start_span() time for agent invocations.""" captured_attributes = {} class AttributeCapturingSampler: # pylint: disable=no-self-use def should_sample( self, parent_context, trace_id, name, kind=None, attributes=None, links=None, ): captured_attributes.update(attributes or {}) return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes) def get_description(self): return "AttributeCapturingSampler" sampler_provider = TracerProvider(sampler=AttributeCapturingSampler()) sampler_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter)) handler = TelemetryHandler(tracer_provider=sampler_provider) invocation = handler.start_invoke_remote_agent( "test-provider", request_model="agent-model", agent_name="Math Tutor", server_address="agent.example.com", server_port=8080, ) invocation.stop() assert captured_attributes[GenAI.GEN_AI_OPERATION_NAME] == "invoke_agent" assert captured_attributes[GenAI.GEN_AI_PROVIDER_NAME] == "test-provider" assert captured_attributes[GenAI.GEN_AI_REQUEST_MODEL] == "agent-model" assert captured_attributes[GenAI.GEN_AI_AGENT_NAME] == "Math Tutor" assert captured_attributes[server_attributes.SERVER_ADDRESS] == "agent.example.com" assert captured_attributes[server_attributes.SERVER_PORT] == 8080 class TestAgentInvocationMetrics(TestBase): def test_local_agent_records_duration_and_tokens(self) -> None: handler = TelemetryHandler( tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) with patch("timeit.default_timer", return_value=1000.0): invocation = handler.start_invoke_local_agent("prov", request_model="model") invocation.input_tokens = 5 invocation.output_tokens = 7 with patch("timeit.default_timer", return_value=1002.0): invocation.stop() metrics = self._harvest_metrics() self.assertIn("gen_ai.client.operation.duration", metrics) duration_points = metrics["gen_ai.client.operation.duration"] self.assertEqual(len(duration_points), 1) duration_point = duration_points[0] self.assertEqual( duration_point.attributes[GenAI.GEN_AI_OPERATION_NAME], GenAI.GenAiOperationNameValues.INVOKE_AGENT.value, ) self.assertEqual(duration_point.attributes[GenAI.GEN_AI_REQUEST_MODEL], "model") self.assertEqual(duration_point.attributes[GenAI.GEN_AI_PROVIDER_NAME], "prov") self.assertAlmostEqual(duration_point.sum, 2.0, places=3) self.assertIn("gen_ai.client.token.usage", metrics) token_points = metrics["gen_ai.client.token.usage"] token_by_type = {point.attributes[GenAI.GEN_AI_TOKEN_TYPE]: point for point in token_points} self.assertEqual(len(token_by_type), 2) self.assertAlmostEqual( token_by_type[GenAI.GenAiTokenTypeValues.INPUT.value].sum, 5.0, places=3, ) self.assertAlmostEqual( token_by_type[GenAI.GenAiTokenTypeValues.OUTPUT.value].sum, 7.0, places=3, ) def test_remote_agent_records_duration_with_server_attrs(self) -> None: handler = TelemetryHandler( tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) invocation = handler.start_invoke_remote_agent( "prov", request_model="model", server_address="agent.example.com", server_port=443, ) invocation.input_tokens = 10 invocation.stop() metrics = self._harvest_metrics() self.assertIn("gen_ai.client.operation.duration", metrics) duration_point = metrics["gen_ai.client.operation.duration"][0] self.assertEqual(duration_point.attributes["server.address"], "agent.example.com") self.assertEqual(duration_point.attributes["server.port"], 443) def test_fail_agent_records_error_metric(self) -> None: handler = TelemetryHandler( tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) with patch("timeit.default_timer", return_value=2000.0): invocation = handler.start_invoke_local_agent("", request_model="err-model") invocation.input_tokens = 11 error = Error(message="boom", type=ValueError) with patch("timeit.default_timer", return_value=2001.0): invocation.fail(error) metrics = self._harvest_metrics() self.assertIn("gen_ai.client.operation.duration", metrics) duration_point = metrics["gen_ai.client.operation.duration"][0] self.assertEqual(duration_point.attributes.get("error.type"), "ValueError") self.assertAlmostEqual(duration_point.sum, 1.0, places=3) def _harvest_metrics(self): metrics = self.get_sorted_metrics() metrics_by_name = {} for metric in metrics or []: points = metric.data.data_points or [] metrics_by_name.setdefault(metric.name, []).extend(points) return metrics_by_name test_handler_completion_hook.py000066400000000000000000000401771527070072300350450ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os from unittest import TestCase from unittest.mock import MagicMock, patch from tests.test_utils import patch_env_vars from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) from opentelemetry.util.genai.completion_hook import _NoOpCompletionHook from opentelemetry.util.genai.environment_variables import ( OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT, ) from opentelemetry.util.genai.handler import TelemetryHandler from opentelemetry.util.genai.types import ( FunctionToolDefinition, InputMessage, OutputMessage, Text, ) _EXPERIMENTAL_ENV = { OTEL_SEMCONV_STABILITY_OPT_IN: "gen_ai_latest_experimental", OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "EVENT_ONLY", OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT: "true", } class TestHandlerCompletionHook(TestCase): # pylint: disable=too-many-public-methods def setUp(self) -> None: self.span_exporter = InMemorySpanExporter() self.tracer_provider = TracerProvider() self.tracer_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter)) _OpenTelemetrySemanticConventionStability._initialized = False _OpenTelemetrySemanticConventionStability._initialize() def tearDown(self) -> None: # Reset semconv stability state between tests _OpenTelemetrySemanticConventionStability._initialized = False def _make_handler(self, hook=None): return TelemetryHandler( tracer_provider=self.tracer_provider, completion_hook=hook, ) def test_hook_called_on_stop(self): hook = MagicMock() handler = self._make_handler(hook) input_messages = [InputMessage(role="user", parts=[Text(content="hello")])] output_messages = [ OutputMessage( role="assistant", parts=[Text(content="hi")], finish_reason="stop", ) ] system_instruction = [Text(content="be helpful")] tool_definitions = [ FunctionToolDefinition( name="get_weather", description="Get the weather", parameters={"type": "object", "properties": {}}, ) ] invocation = handler.start_inference("openai", request_model="gpt-4o") invocation.input_messages = input_messages invocation.output_messages = output_messages invocation.system_instruction = system_instruction invocation.tool_definitions = tool_definitions invocation.stop() hook.on_completion.assert_called_once() kwargs = hook.on_completion.call_args.kwargs self.assertEqual(kwargs["inputs"], input_messages) self.assertEqual(kwargs["outputs"], output_messages) self.assertEqual(kwargs["system_instruction"], system_instruction) self.assertEqual(kwargs["tool_definitions"], tool_definitions) self.assertIsNotNone(kwargs["span"]) def test_hook_called_on_fail(self): hook = MagicMock() handler = self._make_handler(hook) input_messages = [InputMessage(role="user", parts=[Text(content="hello")])] invocation = handler.start_inference("openai", request_model="gpt-4o") invocation.input_messages = input_messages invocation.fail(ValueError("boom")) hook.on_completion.assert_called_once() kwargs = hook.on_completion.call_args.kwargs self.assertEqual(kwargs["inputs"], input_messages) self.assertIsNotNone(kwargs["span"]) def test_hook_not_called_when_not_set(self): # No hook — stop should not raise handler = self._make_handler() handler.start_inference("openai", request_model="gpt-4o").stop() def test_log_record_is_none_when_events_disabled(self): # Default env: no experimental mode, so log_record should be None. # Also pins that tool_definitions defaults to None when unset # (the upload hook hashes off None vs []). hook = MagicMock() handler = self._make_handler(hook) handler.start_inference("openai", request_model="gpt-4o").stop() kwargs = hook.on_completion.call_args.kwargs self.assertIsNone(kwargs["log_record"]) self.assertIsNone(kwargs["tool_definitions"]) @patch.dict(os.environ, _EXPERIMENTAL_ENV) def test_log_record_passed_when_events_enabled(self): _OpenTelemetrySemanticConventionStability._initialized = False _OpenTelemetrySemanticConventionStability._initialize() hook = MagicMock() handler = self._make_handler(hook) handler.start_inference("openai", request_model="gpt-4o").stop() kwargs = hook.on_completion.call_args.kwargs self.assertIsNotNone(kwargs["log_record"]) @patch.dict(os.environ, _EXPERIMENTAL_ENV) def test_hook_can_stamp_attrs_on_log_record(self): # Verify that attrs stamped by the hook are on the same log_record that gets emitted _OpenTelemetrySemanticConventionStability._initialized = False _OpenTelemetrySemanticConventionStability._initialize() stamped_record = None def stamp_ref(*, log_record, **kwargs): nonlocal stamped_record stamped_record = log_record if log_record is not None: log_record.attributes = { **(log_record.attributes or {}), "gen_ai.input_messages_ref": "s3://bucket/inputs.json", } hook = MagicMock(on_completion=stamp_ref) handler = self._make_handler(hook) handler.start_inference("openai", request_model="gpt-4o").stop() # The record the hook stamped is the same one that would be emitted self.assertIsNotNone(stamped_record) self.assertEqual( stamped_record.attributes.get("gen_ai.input_messages_ref"), "s3://bucket/inputs.json", ) @patch.dict(os.environ, {OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: ""}) def test_should_capture_content_false_by_default(self): handler = self._make_handler() self.assertFalse(handler.should_capture_content()) def test_should_capture_content_true_when_real_hook_set(self): # A real (non-noop) hook forces content capture regardless of env vars hook = MagicMock() handler = self._make_handler(hook) self.assertTrue(handler.should_capture_content()) @patch.dict(os.environ, {OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: ""}) def test_should_capture_content_false_when_noop_hook(self): handler = self._make_handler(_NoOpCompletionHook()) self.assertFalse(handler.should_capture_content()) @patch.dict( os.environ, {OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "true"}, ) def test_should_capture_content_true_in_legacy_mode_when_content_env_true( self, ): handler = self._make_handler() self.assertTrue(handler.should_capture_content()) @patch.dict( os.environ, {OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "false"}, ) def test_should_capture_content_false_in_legacy_mode_when_content_env_false( self, ): handler = self._make_handler() self.assertFalse(handler.should_capture_content()) @patch.dict( os.environ, {OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "span_only"}, ) def test_should_capture_content_true_in_legacy_mode_when_content_env_span_only( self, ): handler = self._make_handler() self.assertTrue(handler.should_capture_content()) @patch.dict( os.environ, {OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "event_only"}, ) def test_should_capture_content_true_in_legacy_mode_when_content_env_event_only( self, ): handler = self._make_handler() self.assertTrue(handler.should_capture_content()) @patch_env_vars("gen_ai_latest_experimental", "span_only", "false") def test_should_capture_content_true_in_experimental_mode_with_content( self, ): handler = self._make_handler() self.assertTrue(handler.should_capture_content()) @patch_env_vars("gen_ai_latest_experimental", "no_content", "false") def test_should_capture_content_false_in_experimental_mode_with_no_content( self, ): handler = self._make_handler() self.assertFalse(handler.should_capture_content()) @patch_env_vars("gen_ai_latest_experimental", "no_content", "false") def test_should_capture_content_true_in_experimental_mode_no_content_but_hook_set( self, ): # Hook overrides no_content mode hook = MagicMock() handler = self._make_handler(hook) self.assertTrue(handler.should_capture_content()) @patch_env_vars("gen_ai_latest_experimental", "no_content", "false") @patch.dict( os.environ, {OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "true"}, ) def test_should_capture_content_false_in_experimental_mode_ignores_legacy_env( self, ): # Legacy CAPTURE_MESSAGE_CONTENT=true should NOT override NO_CONTENT in experimental mode handler = self._make_handler() self.assertFalse(handler.should_capture_content()) def test_workflow_hook_called_on_stop_with_messages(self): hook = MagicMock() handler = self._make_handler(hook) input_messages = [InputMessage(role="user", parts=[Text(content="what is 2+2?")])] output_messages = [ OutputMessage( role="assistant", parts=[Text(content="4")], finish_reason="stop", ) ] invocation = handler.start_workflow(name="my-workflow") invocation.input_messages = input_messages invocation.output_messages = output_messages invocation.stop() hook.on_completion.assert_called_once() kwargs = hook.on_completion.call_args.kwargs self.assertEqual(kwargs["inputs"], input_messages) self.assertEqual(kwargs["outputs"], output_messages) self.assertEqual(kwargs["system_instruction"], []) # Workflows don't carry tool_definitions — must be None, not []. self.assertIsNone(kwargs["tool_definitions"]) self.assertIsNotNone(kwargs["span"]) self.assertIsNone(kwargs["log_record"]) def test_workflow_hook_called_on_fail(self): hook = MagicMock() handler = self._make_handler(hook) invocation = handler.start_workflow(name="my-workflow") invocation.input_messages = [InputMessage(role="user", parts=[Text(content="hello")])] invocation.fail(RuntimeError("workflow failed")) hook.on_completion.assert_called_once() kwargs = hook.on_completion.call_args.kwargs self.assertIsNotNone(kwargs["span"]) def test_workflow_hook_called_with_empty_messages_when_none_set(self): hook = MagicMock() handler = self._make_handler(hook) handler.start_workflow(name="my-workflow").stop() hook.on_completion.assert_called_once() kwargs = hook.on_completion.call_args.kwargs self.assertEqual(kwargs["inputs"], []) self.assertEqual(kwargs["outputs"], []) def test_local_agent_hook_called_on_stop_with_messages(self): hook = MagicMock() handler = self._make_handler(hook) input_messages = [InputMessage(role="user", parts=[Text(content="what is 2+2?")])] output_messages = [ OutputMessage( role="assistant", parts=[Text(content="4")], finish_reason="stop", ) ] system_instruction = [Text(content="be helpful")] tool_definitions = [ FunctionToolDefinition( name="get_weather", description="Get the weather", parameters={"type": "object", "properties": {}}, ) ] invocation = handler.start_invoke_local_agent("openai", request_model="gpt-4") invocation.agent_name = "Math Tutor" invocation.input_messages = input_messages invocation.output_messages = output_messages invocation.system_instruction = system_instruction invocation.tool_definitions = tool_definitions invocation.stop() hook.on_completion.assert_called_once() kwargs = hook.on_completion.call_args.kwargs self.assertEqual(kwargs["inputs"], input_messages) self.assertEqual(kwargs["outputs"], output_messages) self.assertEqual(kwargs["system_instruction"], system_instruction) self.assertEqual(kwargs["tool_definitions"], tool_definitions) self.assertIsNotNone(kwargs["span"]) self.assertIsNone(kwargs.get("log_record")) def test_local_agent_hook_called_on_fail(self): hook = MagicMock() handler = self._make_handler(hook) invocation = handler.start_invoke_local_agent("openai", request_model="gpt-4") invocation.input_messages = [InputMessage(role="user", parts=[Text(content="hello")])] invocation.fail(RuntimeError("agent failed")) hook.on_completion.assert_called_once() kwargs = hook.on_completion.call_args.kwargs self.assertIsNotNone(kwargs["span"]) def test_remote_agent_hook_called_on_stop_with_messages(self): hook = MagicMock() handler = self._make_handler(hook) input_messages = [InputMessage(role="user", parts=[Text(content="hi")])] output_messages = [ OutputMessage( role="assistant", parts=[Text(content="hello")], finish_reason="stop", ) ] tool_definitions = [ FunctionToolDefinition( name="get_weather", description="Get the weather", parameters={"type": "object", "properties": {}}, ) ] invocation = handler.start_invoke_remote_agent( "openai", request_model="gpt-4", server_address="api.openai.com", server_port=443, ) invocation.input_messages = input_messages invocation.output_messages = output_messages invocation.tool_definitions = tool_definitions invocation.stop() hook.on_completion.assert_called_once() kwargs = hook.on_completion.call_args.kwargs self.assertEqual(kwargs["inputs"], input_messages) self.assertEqual(kwargs["outputs"], output_messages) self.assertEqual(kwargs["tool_definitions"], tool_definitions) self.assertIsNotNone(kwargs["span"]) self.assertIsNone(kwargs.get("log_record")) def test_remote_agent_hook_called_on_fail(self): hook = MagicMock() handler = self._make_handler(hook) invocation = handler.start_invoke_remote_agent("openai") invocation.fail(RuntimeError("remote agent crashed")) hook.on_completion.assert_called_once() kwargs = hook.on_completion.call_args.kwargs self.assertIsNotNone(kwargs["span"]) def test_agent_hook_called_with_empty_messages_when_none_set(self): hook = MagicMock() handler = self._make_handler(hook) handler.start_invoke_local_agent("openai").stop() handler.start_invoke_remote_agent("openai").stop() for call in hook.on_completion.call_args_list: self.assertEqual(call.kwargs["inputs"], []) self.assertEqual(call.kwargs["outputs"], []) self.assertEqual(call.kwargs["system_instruction"], []) self.assertIsNone(call.kwargs["tool_definitions"]) def test_agent_hook_not_called_when_not_set(self): # No hook — stop should not raise handler = self._make_handler() handler.start_invoke_local_agent("openai").stop() handler.start_invoke_remote_agent("openai").stop() opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/tests/test_handler_metrics.py000066400000000000000000000352741527070072300334030ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from typing import Any from unittest.mock import patch from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAI, ) from opentelemetry.semconv.schemas import Schemas from opentelemetry.test.test_base import TestBase from opentelemetry.util.genai.handler import TelemetryHandler from opentelemetry.util.genai.types import Error _DEFAULT_SCHEMA_URL = Schemas.V1_37_0.value SCOPE = "opentelemetry.util.genai.handler" class TelemetryHandlerMetricsTest(TestBase): def test_stop_llm_records_duration_and_tokens(self) -> None: handler = TelemetryHandler( tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) # Patch default_timer during start to ensure monotonic_start_s with patch("timeit.default_timer", return_value=1000.0): invocation = handler.start_inference("prov", request_model="model") invocation.input_tokens = 5 invocation.output_tokens = 7 # Simulate 2 seconds of elapsed monotonic time (seconds) with patch( "timeit.default_timer", return_value=1002.0, ): invocation.stop() self._assert_metric_scope_schema_urls(_DEFAULT_SCHEMA_URL) metrics = self._harvest_metrics() self.assertIn("gen_ai.client.operation.duration", metrics) duration_points = metrics["gen_ai.client.operation.duration"] self.assertEqual(len(duration_points), 1) duration_point = duration_points[0] self.assertEqual( duration_point.attributes[GenAI.GEN_AI_OPERATION_NAME], GenAI.GenAiOperationNameValues.CHAT.value, ) self.assertEqual(duration_point.attributes[GenAI.GEN_AI_REQUEST_MODEL], "model") self.assertEqual(duration_point.attributes[GenAI.GEN_AI_PROVIDER_NAME], "prov") self.assertAlmostEqual(duration_point.sum, 2.0, places=3) self.assertIn("gen_ai.client.token.usage", metrics) token_points = metrics["gen_ai.client.token.usage"] token_by_type = {point.attributes[GenAI.GEN_AI_TOKEN_TYPE]: point for point in token_points} self.assertEqual(len(token_by_type), 2) self.assertAlmostEqual( token_by_type[GenAI.GenAiTokenTypeValues.INPUT.value].sum, 5.0, places=3, ) self.assertAlmostEqual( token_by_type[GenAI.GenAiTokenTypeValues.COMPLETION.value].sum, 7.0, places=3, ) def test_stop_llm_records_duration_and_tokens_with_additional_attributes( self, ) -> None: handler = TelemetryHandler( tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) invocation = handler.start_inference( "prov", request_model="model", server_address="custom.server.com", server_port=42, ) invocation.input_tokens = 5 invocation.output_tokens = 7 invocation.metric_attributes = { "custom.attribute": "custom_value", } invocation.attributes = {"should not be on metrics": "value"} invocation.stop() self._assert_metric_scope_schema_urls(_DEFAULT_SCHEMA_URL) metrics = self._harvest_metrics() self.assertIn("gen_ai.client.operation.duration", metrics) duration_points = metrics["gen_ai.client.operation.duration"] self.assertIn("gen_ai.client.token.usage", metrics) token_points = metrics["gen_ai.client.token.usage"] points = duration_points + token_points for point in points: self.assertEqual(point.attributes["server.address"], "custom.server.com") self.assertEqual(point.attributes["server.port"], 42) self.assertEqual(point.attributes["custom.attribute"], "custom_value") self.assertIsNone(point.attributes.get("should not be on metrics")) def test_fail_llm_records_error_and_available_tokens(self) -> None: handler = TelemetryHandler( tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) # Patch default_timer during start to ensure monotonic_start_s with patch("timeit.default_timer", return_value=2000.0): invocation = handler.start_inference("", request_model="err-model") invocation.input_tokens = 11 error = Error(message="boom", type=ValueError) with patch( "timeit.default_timer", return_value=2001.0, ): invocation.fail(error) self._assert_metric_scope_schema_urls(_DEFAULT_SCHEMA_URL) metrics = self._harvest_metrics() self.assertIn("gen_ai.client.operation.duration", metrics) duration_points = metrics["gen_ai.client.operation.duration"] self.assertEqual(len(duration_points), 1) duration_point = duration_points[0] self.assertEqual(duration_point.attributes.get("error.type"), "ValueError") self.assertEqual( duration_point.attributes.get(GenAI.GEN_AI_REQUEST_MODEL), "err-model", ) self.assertAlmostEqual(duration_point.sum, 1.0, places=3) self.assertIn("gen_ai.client.token.usage", metrics) token_points = metrics["gen_ai.client.token.usage"] self.assertEqual(len(token_points), 1) token_point = token_points[0] self.assertEqual( token_point.attributes[GenAI.GEN_AI_TOKEN_TYPE], GenAI.GenAiTokenTypeValues.INPUT.value, ) self.assertAlmostEqual(token_point.sum, 11.0, places=3) def _harvest_metrics( self, ) -> dict[str, list[Any]]: """Returns (metrics_by_name, resource_metrics). metrics_by_name maps metric name to list of data points. resource_metrics is the raw ResourceMetrics list for scope-level assertions (e.g. schema_url). """ metrics = self.get_sorted_metrics() metrics_by_name: dict[str, list[Any]] = {} for metric in metrics or []: points = metric.data.data_points or [] metrics_by_name.setdefault(metric.name, []).extend(points) return metrics_by_name def _assert_metric_scope_schema_urls(self, expected_schema_url: str) -> None: for resource_metric in self.memory_metrics_reader.get_metrics_data().resource_metrics: for scope_metric in resource_metric.scope_metrics: if scope_metric.scope.name != SCOPE: continue self.assertEqual(scope_metric.scope.schema_url, expected_schema_url) def test_stop_embedding_records_duration_and_tokens(self) -> None: """Verify embedding invocations record duration and input token metrics.""" handler = TelemetryHandler( tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) # Patch default_timer during start to ensure monotonic_start_s with patch("timeit.default_timer", return_value=1000.0): invocation = handler.start_embedding("embed-prov", request_model="embed-model") invocation.input_tokens = 100 # Simulate 1.5 seconds of elapsed monotonic time with patch("timeit.default_timer", return_value=1001.5): invocation.stop() self._assert_metric_scope_schema_urls(_DEFAULT_SCHEMA_URL) metrics = self._harvest_metrics() # Duration should be recorded self.assertIn("gen_ai.client.operation.duration", metrics) duration_points = metrics["gen_ai.client.operation.duration"] self.assertEqual(len(duration_points), 1) duration_point = duration_points[0] self.assertEqual( duration_point.attributes[GenAI.GEN_AI_OPERATION_NAME], GenAI.GenAiOperationNameValues.EMBEDDINGS.value, ) self.assertEqual( duration_point.attributes[GenAI.GEN_AI_REQUEST_MODEL], "embed-model", ) self.assertEqual(duration_point.attributes[GenAI.GEN_AI_PROVIDER_NAME], "embed-prov") self.assertAlmostEqual(duration_point.sum, 1.5, places=3) # Token metrics should be recorded for embedding (input only) self.assertIn("gen_ai.client.token.usage", metrics) token_points = metrics["gen_ai.client.token.usage"] self.assertEqual(len(token_points), 1) # Only input tokens token_point = token_points[0] self.assertEqual( token_point.attributes[GenAI.GEN_AI_TOKEN_TYPE], GenAI.GenAiTokenTypeValues.INPUT.value, ) self.assertAlmostEqual(token_point.sum, 100.0, places=3) def test_stop_embedding_records_duration_with_additional_attributes( self, ) -> None: """Verify embedding metrics include server and custom attributes.""" handler = TelemetryHandler( tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) invocation = handler.start_embedding( "embed-prov", request_model="embed-model", server_address="embed.server.com", server_port=8080, ) invocation.metric_attributes = {"custom.embed.attr": "embed_value"} invocation.response_model_name = "embed-response-model" invocation.stop() self._assert_metric_scope_schema_urls(_DEFAULT_SCHEMA_URL) metrics = self._harvest_metrics() self.assertIn("gen_ai.client.operation.duration", metrics) duration_points = metrics["gen_ai.client.operation.duration"] self.assertEqual(len(duration_points), 1) duration_point = duration_points[0] self.assertEqual(duration_point.attributes["server.address"], "embed.server.com") self.assertEqual(duration_point.attributes["server.port"], 8080) self.assertEqual(duration_point.attributes["custom.embed.attr"], "embed_value") self.assertEqual( duration_point.attributes[GenAI.GEN_AI_RESPONSE_MODEL], "embed-response-model", ) def test_fail_embedding_records_error_and_duration(self) -> None: """Verify embedding failure records error type and duration.""" handler = TelemetryHandler( tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) with patch("timeit.default_timer", return_value=3000.0): invocation = handler.start_embedding("embed-prov", request_model="embed-err-model") error = Error(message="embedding failed", type=RuntimeError) with patch("timeit.default_timer", return_value=3002.5): invocation.fail(error) self._assert_metric_scope_schema_urls(_DEFAULT_SCHEMA_URL) metrics = self._harvest_metrics() self.assertIn("gen_ai.client.operation.duration", metrics) duration_points = metrics["gen_ai.client.operation.duration"] self.assertEqual(len(duration_points), 1) duration_point = duration_points[0] self.assertEqual(duration_point.attributes.get("error.type"), "RuntimeError") self.assertEqual( duration_point.attributes.get(GenAI.GEN_AI_REQUEST_MODEL), "embed-err-model", ) self.assertAlmostEqual(duration_point.sum, 2.5, places=3) # Token metrics should NOT be recorded when input_tokens is not set self.assertNotIn("gen_ai.client.token.usage", metrics) def test_stop_embedding_without_tokens(self) -> None: """Verify embedding without input_tokens does not record token metrics.""" handler = TelemetryHandler( tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) invocation = handler.start_embedding("embed-prov", request_model="embed-model") # input_tokens is not set invocation.stop() metrics = self._harvest_metrics() # Duration should be recorded self.assertIn("gen_ai.client.operation.duration", metrics) # Token metrics should NOT be recorded when input_tokens is not set self.assertNotIn("gen_ai.client.token.usage", metrics) class TelemetryHandlerToolMetricsTest(TestBase): def _harvest_metrics(self) -> dict[str, list[Any]]: metrics = self.get_sorted_metrics() metrics_by_name: dict[str, list[Any]] = {} for metric in metrics or []: points = metric.data.data_points or [] metrics_by_name.setdefault(metric.name, []).extend(points) return metrics_by_name def test_stop_tool_records_duration(self) -> None: handler = TelemetryHandler( tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) with patch("timeit.default_timer", return_value=1000.0): invocation = handler.start_tool("get_weather") invocation.metric_attributes = {"custom.key": "custom_value"} with patch("timeit.default_timer", return_value=1002.5): invocation.stop() metrics = self._harvest_metrics() self.assertIn("gen_ai.client.operation.duration", metrics) duration_points = metrics["gen_ai.client.operation.duration"] self.assertEqual(len(duration_points), 1) duration_point = duration_points[0] self.assertEqual( duration_point.attributes[GenAI.GEN_AI_OPERATION_NAME], "execute_tool", ) self.assertEqual(duration_point.attributes["custom.key"], "custom_value") self.assertAlmostEqual(duration_point.sum, 2.5, places=3) self.assertNotIn("gen_ai.client.token.usage", metrics) def test_fail_tool_records_duration_with_error(self) -> None: handler = TelemetryHandler( tracer_provider=self.tracer_provider, meter_provider=self.meter_provider, ) with patch("timeit.default_timer", return_value=500.0): invocation = handler.start_tool("failing_tool") error = Error(message="Tool execution failed", type=RuntimeError) with patch("timeit.default_timer", return_value=501.5): invocation.fail(error) metrics = self._harvest_metrics() self.assertIn("gen_ai.client.operation.duration", metrics) duration_points = metrics["gen_ai.client.operation.duration"] self.assertEqual(len(duration_points), 1) duration_point = duration_points[0] self.assertEqual(duration_point.attributes["error.type"], "RuntimeError") self.assertEqual( duration_point.attributes[GenAI.GEN_AI_OPERATION_NAME], "execute_tool", ) self.assertAlmostEqual(duration_point.sum, 1.5, places=3) self.assertNotIn("gen_ai.client.token.usage", metrics) opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/tests/test_handler_workflow.py000066400000000000000000000234631527070072300336040ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from unittest import TestCase from unittest.mock import patch import pytest from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) from opentelemetry.sdk.trace.sampling import Decision, SamplingResult from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAI, ) from opentelemetry.trace import INVALID_SPAN, SpanKind from opentelemetry.trace.status import StatusCode from opentelemetry.util.genai.handler import TelemetryHandler from opentelemetry.util.genai.invocation import WorkflowInvocation from opentelemetry.util.genai.types import ( Error, InputMessage, OutputMessage, Text, ) class _WorkflowTestBase(TestCase): """Shared setUp for workflow handler tests.""" def setUp(self) -> None: self.span_exporter = InMemorySpanExporter() self.tracer_provider = TracerProvider() self.tracer_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter)) self.handler = TelemetryHandler( tracer_provider=self.tracer_provider, ) def _get_finished_spans(self): return self.span_exporter.get_finished_spans() class TelemetryHandlerWorkflowTest(_WorkflowTestBase): # ------------------------------------------------------------------ # start_workflow # ------------------------------------------------------------------ def test_start_workflow_creates_span(self) -> None: invocation = self.handler.start_workflow(name="my_workflow") self.assertIsNot(invocation.span, INVALID_SPAN) invocation.stop() def test_start_workflow_span_name(self) -> None: invocation = self.handler.start_workflow(name="my_pipeline") invocation.stop() spans = self._get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].name, "invoke_workflow my_pipeline") def test_start_workflow_span_name_without_name(self) -> None: invocation = self.handler.start_workflow(name=None) invocation.stop() spans = self._get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].name, "invoke_workflow") def test_start_workflow_span_kind_is_internal(self) -> None: invocation = self.handler.start_workflow(name="wf") invocation.stop() spans = self._get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].kind, SpanKind.INTERNAL) def test_start_workflow_records_monotonic_start(self) -> None: with patch("timeit.default_timer", return_value=500.0): invocation = self.handler.start_workflow(name="wf") self.assertEqual(invocation._monotonic_start_s, 500.0) invocation.stop() # ------------------------------------------------------------------ # stop_workflow # ------------------------------------------------------------------ def test_stop_workflow_ends_span(self) -> None: invocation = self.handler.start_workflow(name="wf") invocation.stop() spans = self._get_finished_spans() self.assertEqual(len(spans), 1) def test_stop_workflow_sets_operation_name_attribute(self) -> None: invocation = self.handler.start_workflow(name="wf") invocation.stop() spans = self._get_finished_spans() self.assertEqual( spans[0].attributes[GenAI.GEN_AI_OPERATION_NAME], "invoke_workflow", ) def test_stop_workflow_sets_custom_attributes(self) -> None: invocation = self.handler.start_workflow(name="wf") invocation.attributes["custom.key"] = "custom_value" invocation.stop() spans = self._get_finished_spans() self.assertEqual(spans[0].attributes["custom.key"], "custom_value") def test_stop_workflow_returns_invocation(self) -> None: invocation = self.handler.start_workflow(name="wf") invocation.stop() spans = self._get_finished_spans() self.assertEqual(len(spans), 1) # ------------------------------------------------------------------ # fail_workflow # ------------------------------------------------------------------ def test_fail_workflow_sets_error_status(self) -> None: invocation = self.handler.start_workflow(name="wf") error = Error(message="something broke", type=RuntimeError) invocation.fail(error) spans = self._get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].status.status_code, StatusCode.ERROR) self.assertEqual(spans[0].status.description, "something broke") def test_fail_workflow_sets_error_type_attribute(self) -> None: invocation = self.handler.start_workflow(name="wf") error = Error(message="bad", type=ValueError) invocation.fail(error) spans = self._get_finished_spans() self.assertEqual(spans[0].attributes["error.type"], "ValueError") def test_fail_workflow_sets_operation_name_attribute(self) -> None: invocation = self.handler.start_workflow(name="wf") error = Error(message="fail", type=TypeError) invocation.fail(error) spans = self._get_finished_spans() self.assertEqual( spans[0].attributes[GenAI.GEN_AI_OPERATION_NAME], "invoke_workflow", ) def test_fail_workflow_ends_span(self) -> None: invocation = self.handler.start_workflow(name="wf") invocation.fail(Error(message="err", type=RuntimeError)) spans = self._get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].status.status_code, StatusCode.ERROR) class TelemetryHandlerWorkflowContextManagerTest(_WorkflowTestBase): # ------------------------------------------------------------------ # workflow context manager # ------------------------------------------------------------------ def test_workflow_context_manager_creates_and_ends_span(self) -> None: with self.handler.workflow(name="ctx_wf") as inv: self.assertIsNot(inv.span, INVALID_SPAN) spans = self._get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].name, "invoke_workflow ctx_wf") def test_workflow_context_manager_default_invocation(self) -> None: with self.handler.workflow() as inv: self.assertIsInstance(inv, WorkflowInvocation) self.assertIsNone(inv.name) self.assertEqual(inv._operation_name, "invoke_workflow") class TelemetryHandlerWorkflowSamplingTest(_WorkflowTestBase): def test_start_workflow_passes_sampling_attributes_at_span_creation( self, ) -> None: """Verify that sampling-relevant attributes are available at start_span() time for workflows.""" captured_attributes = {} class AttributeCapturingSampler: # pylint: disable=no-self-use def should_sample( self, parent_context, trace_id, name, kind=None, attributes=None, links=None, ): captured_attributes.update(attributes or {}) return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes) def get_description(self): return "AttributeCapturingSampler" sampler_provider = TracerProvider(sampler=AttributeCapturingSampler()) sampler_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter)) handler = TelemetryHandler(tracer_provider=sampler_provider) invocation = handler.start_workflow(name="my-workflow") invocation.stop() self.assertEqual(captured_attributes[GenAI.GEN_AI_OPERATION_NAME], "invoke_workflow") spans = self._get_finished_spans() self.assertEqual(len(spans), 1) def test_workflow_context_manager_sets_attributes_on_span(self) -> None: with self.handler.workflow("wf") as inv: inv.attributes["my.attr"] = "hello" spans = self._get_finished_spans() self.assertEqual(spans[0].attributes["my.attr"], "hello") def test_workflow_context_manager_reraises_exception(self) -> None: with pytest.raises(ValueError, match="test error"): with self.handler.workflow("wf"): raise ValueError("test error") def test_workflow_context_manager_marks_error_on_exception(self) -> None: with pytest.raises(ValueError): with self.handler.workflow("wf"): raise ValueError("boom") spans = self._get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].status.status_code, StatusCode.ERROR) self.assertEqual(spans[0].status.description, "boom") self.assertEqual(spans[0].attributes["error.type"], "ValueError") def test_workflow_context_manager_success_has_unset_status(self) -> None: with self.handler.workflow("wf"): pass spans = self._get_finished_spans() self.assertEqual(spans[0].status.status_code, StatusCode.UNSET) def test_workflow_context_manager_with_messages(self) -> None: inp = InputMessage(role="user", parts=[Text(content="hello")]) out = OutputMessage(role="assistant", parts=[Text(content="hi")], finish_reason="stop") with self.handler.workflow("msg_wf") as inv: inv.input_messages = [inp] inv.output_messages = [out] spans = self._get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual( spans[0].attributes[GenAI.GEN_AI_OPERATION_NAME], "invoke_workflow", ) opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/tests/test_stream.py000066400000000000000000000347741527070072300315370ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=abstract-class-instantiated import asyncio import inspect import pytest from opentelemetry.util.genai.stream import ( AsyncStreamWrapper, SyncStreamWrapper, ) def test_stream_wrapper_abstract_method_signatures_match(): method_names = ( "_process_chunk", "_on_stream_end", "_on_stream_error", "_handle_process_chunk_error", ) for method_name in method_names: assert inspect.signature(getattr(SyncStreamWrapper, method_name)) == inspect.signature( getattr(AsyncStreamWrapper, method_name) ) class _FakeSyncStream: def __init__(self, chunks=None, error=None, close_error=None): self._chunks = list(chunks or []) self._error = error self._close_error = close_error self.close_count = 0 self.extra_attribute = "passthrough" def __iter__(self): return self def __next__(self): if self._chunks: return self._chunks.pop(0) if self._error: raise self._error raise StopIteration def close(self): self.close_count += 1 if self._close_error: raise self._close_error def __len__(self): return 42 class _FakeSyncIterable: def __init__(self, chunks=None): self.iterator = iter(chunks or []) self.close_count = 0 def __iter__(self): return self.iterator def close(self): self.close_count += 1 class _TestSyncStreamWrapper(SyncStreamWrapper): def __init__(self, stream): super().__init__(stream) self._self_processed = [] self._self_stop_count = 0 self._self_failures = [] def _process_chunk(self, chunk): self._self_processed.append(chunk) def _on_stream_end(self): self._self_stop_count += 1 def _on_stream_error(self, error): self._self_failures.append(error) class _FailingSyncProcessStreamWrapper(_TestSyncStreamWrapper): def _process_chunk(self, chunk): raise ValueError("instrumentation failed") class _FailingSyncStopStreamWrapper(_TestSyncStreamWrapper): def _on_stream_end(self): self._self_stop_count += 1 raise ValueError("instrumentation failed") class _FailingSyncFailStreamWrapper(_TestSyncStreamWrapper): def _on_stream_error(self, error): self._self_failures.append(error) raise ValueError("instrumentation failed") def test_sync_stream_wrapper_processes_chunks_and_stops(): stream = _FakeSyncStream(chunks=["chunk"]) wrapper = _TestSyncStreamWrapper(stream) assert next(wrapper) == "chunk" assert wrapper._self_processed == ["chunk"] try: next(wrapper) except StopIteration: pass assert wrapper._self_stop_count == 1 def test_sync_stream_wrapper_processes_iterables(): stream = _FakeSyncIterable(chunks=["chunk"]) wrapper = _TestSyncStreamWrapper(stream) assert next(wrapper) == "chunk" assert wrapper._self_processed == ["chunk"] with pytest.raises(StopIteration): next(wrapper) assert wrapper._self_stop_count == 1 def test_sync_stream_wrapper_fails_stream_errors(): error = ValueError("boom") wrapper = _TestSyncStreamWrapper(_FakeSyncStream(error=error)) try: next(wrapper) except ValueError: pass assert wrapper._self_failures == [error] def test_sync_stream_wrapper_close_stops_once(): stream = _FakeSyncStream(chunks=["chunk"]) wrapper = _TestSyncStreamWrapper(stream) wrapper.close() wrapper.close() assert stream.close_count == 2 assert wrapper._self_stop_count == 1 assert not wrapper._self_failures def test_sync_stream_wrapper_close_fails_with_close_error(): error = RuntimeError("close failure") wrapper = _TestSyncStreamWrapper(_FakeSyncStream(chunks=["chunk"], close_error=error)) with pytest.raises(RuntimeError, match="close failure"): wrapper.close() assert wrapper._self_failures == [error] assert wrapper._self_stop_count == 0 def test_sync_stream_wrapper_exit_closes_and_propagates_user_errors(): stream = _FakeSyncStream(chunks=["chunk"]) wrapper = _TestSyncStreamWrapper(stream) error = RuntimeError("user failure") assert wrapper.__exit__(RuntimeError, error, None) is False assert stream.close_count == 1 assert wrapper._self_stop_count == 0 assert wrapper._self_failures == [error] def test_sync_stream_wrapper_exit_keeps_user_error_when_close_fails(): close_error = RuntimeError("close failure") stream = _FakeSyncStream(chunks=["chunk"], close_error=close_error) wrapper = _TestSyncStreamWrapper(stream) error = RuntimeError("user failure") assert wrapper.__exit__(RuntimeError, error, None) is False assert stream.close_count == 1 assert wrapper._self_failures == [error] assert wrapper._self_stop_count == 0 def test_sync_stream_wrapper_swallows_finalize_errors(): wrapper = _FailingSyncStopStreamWrapper(_FakeSyncStream()) wrapper.close() wrapper.close() assert wrapper._self_stop_count == 1 def test_sync_stream_wrapper_swallows_failure_finalize_errors(): close_error = RuntimeError("close failure") stream = _FakeSyncStream(close_error=close_error) wrapper = _FailingSyncFailStreamWrapper(stream) with pytest.raises(RuntimeError, match="close failure"): wrapper.close() stream._close_error = None wrapper.close() assert wrapper._self_failures == [close_error] def test_sync_stream_wrapper_swallows_stop_iteration_finalize_errors(): wrapper = _FailingSyncStopStreamWrapper(_FakeSyncStream()) with pytest.raises(StopIteration): next(wrapper) def test_sync_stream_wrapper_preserves_stream_error_when_finalize_fails(): error = RuntimeError("stream failure") wrapper = _FailingSyncFailStreamWrapper(_FakeSyncStream(error=error)) with pytest.raises(RuntimeError, match="stream failure"): next(wrapper) def test_sync_stream_wrapper_getattr_passthrough(): wrapper = _TestSyncStreamWrapper(_FakeSyncStream()) assert wrapper.extra_attribute == "passthrough" def test_sync_stream_wrapper_exposes_wrapped_stream(): stream = _FakeSyncStream() wrapper = _TestSyncStreamWrapper(stream) assert wrapper.__wrapped__ is stream # pylint: disable=no-member def test_sync_stream_wrapper_magic_method_passthrough(): wrapper = _TestSyncStreamWrapper(_FakeSyncStream()) assert len(wrapper) == 42 def test_sync_stream_wrapper_stop_iteration_does_not_double_finalize(): wrapper = _TestSyncStreamWrapper(_FakeSyncStream()) with pytest.raises(StopIteration): next(wrapper) wrapper.close() assert wrapper._self_stop_count == 1 assert not wrapper._self_failures def test_sync_stream_wrapper_swallows_process_chunk_errors(): wrapper = _FailingSyncProcessStreamWrapper(_FakeSyncStream(chunks=["chunk"])) assert next(wrapper) == "chunk" assert not wrapper._self_failures class _FakeAsyncStream: def __init__(self, chunks=None, error=None, close_error=None): self._chunks = list(chunks or []) self._error = error self._close_error = close_error self.close_count = 0 self.extra_attribute = "passthrough" def __aiter__(self): return self async def __anext__(self): if self._chunks: return self._chunks.pop(0) if self._error: raise self._error raise StopAsyncIteration async def close(self): self.close_count += 1 if self._close_error: raise self._close_error def __len__(self): return 42 class _FakeAsyncIterable: def __init__(self, chunks=None): self.iterator = _FakeAsyncStream(chunks=chunks) self.close_count = 0 def __aiter__(self): return self.iterator async def close(self): self.close_count += 1 class _TestAsyncStreamWrapper(AsyncStreamWrapper): def __init__(self, stream): super().__init__(stream) self._self_processed = [] self._self_stop_count = 0 self._self_failures = [] def _process_chunk(self, chunk): self._self_processed.append(chunk) def _on_stream_end(self): self._self_stop_count += 1 def _on_stream_error(self, error): self._self_failures.append(error) class _FailingAsyncProcessStreamWrapper(_TestAsyncStreamWrapper): def _process_chunk(self, chunk): raise ValueError("instrumentation failed") class _FailingAsyncStopStreamWrapper(_TestAsyncStreamWrapper): def _on_stream_end(self): self._self_stop_count += 1 raise ValueError("instrumentation failed") class _FailingAsyncFailStreamWrapper(_TestAsyncStreamWrapper): def _on_stream_error(self, error): self._self_failures.append(error) raise ValueError("instrumentation failed") def test_async_stream_wrapper_processes_chunks_and_stops(): async def exercise(): wrapper = _TestAsyncStreamWrapper(_FakeAsyncStream(chunks=["chunk"])) assert await anext(wrapper) == "chunk" assert wrapper._self_processed == ["chunk"] try: await anext(wrapper) except StopAsyncIteration: pass assert wrapper._self_stop_count == 1 asyncio.run(exercise()) def test_async_stream_wrapper_processes_async_iterables(): async def exercise(): stream = _FakeAsyncIterable(chunks=["chunk"]) wrapper = _TestAsyncStreamWrapper(stream) assert await anext(wrapper) == "chunk" assert wrapper._self_processed == ["chunk"] with pytest.raises(StopAsyncIteration): await anext(wrapper) assert wrapper._self_stop_count == 1 asyncio.run(exercise()) def test_async_stream_wrapper_fails_stream_errors(): async def exercise(): error = ValueError("boom") wrapper = _TestAsyncStreamWrapper(_FakeAsyncStream(error=error)) with pytest.raises(ValueError): await anext(wrapper) assert wrapper._self_failures == [error] asyncio.run(exercise()) def test_async_stream_wrapper_close_stops_once(): async def exercise(): stream = _FakeAsyncStream(chunks=["chunk"]) wrapper = _TestAsyncStreamWrapper(stream) await wrapper.close() await wrapper.close() assert stream.close_count == 2 assert wrapper._self_stop_count == 1 assert not wrapper._self_failures asyncio.run(exercise()) def test_async_stream_wrapper_close_fails_with_close_error(): async def exercise(): error = RuntimeError("close failure") wrapper = _TestAsyncStreamWrapper(_FakeAsyncStream(chunks=["chunk"], close_error=error)) with pytest.raises(RuntimeError, match="close failure"): await wrapper.close() assert wrapper._self_failures == [error] assert wrapper._self_stop_count == 0 asyncio.run(exercise()) def test_async_stream_wrapper_exit_closes_and_propagates_user_errors(): async def exercise(): stream = _FakeAsyncStream(chunks=["chunk"]) wrapper = _TestAsyncStreamWrapper(stream) error = RuntimeError("user failure") assert await wrapper.__aexit__(RuntimeError, error, None) is False assert stream.close_count == 1 assert wrapper._self_stop_count == 0 assert wrapper._self_failures == [error] asyncio.run(exercise()) def test_async_stream_wrapper_exit_keeps_user_error_when_close_fails(): async def exercise(): close_error = RuntimeError("close failure") stream = _FakeAsyncStream(chunks=["chunk"], close_error=close_error) wrapper = _TestAsyncStreamWrapper(stream) error = RuntimeError("user failure") assert await wrapper.__aexit__(RuntimeError, error, None) is False assert stream.close_count == 1 assert wrapper._self_failures == [error] assert wrapper._self_stop_count == 0 asyncio.run(exercise()) def test_async_stream_wrapper_swallows_finalize_errors(): async def exercise(): wrapper = _FailingAsyncStopStreamWrapper(_FakeAsyncStream()) await wrapper.close() await wrapper.close() assert wrapper._self_stop_count == 1 asyncio.run(exercise()) def test_async_stream_wrapper_swallows_failure_finalize_errors(): async def exercise(): close_error = RuntimeError("close failure") stream = _FakeAsyncStream(close_error=close_error) wrapper = _FailingAsyncFailStreamWrapper(stream) with pytest.raises(RuntimeError, match="close failure"): await wrapper.close() stream._close_error = None await wrapper.close() assert wrapper._self_failures == [close_error] asyncio.run(exercise()) def test_async_stream_wrapper_swallows_stop_iteration_finalize_errors(): async def exercise(): wrapper = _FailingAsyncStopStreamWrapper(_FakeAsyncStream()) with pytest.raises(StopAsyncIteration): await anext(wrapper) asyncio.run(exercise()) def test_async_stream_wrapper_preserves_stream_error_when_finalize_fails(): async def exercise(): error = RuntimeError("stream failure") wrapper = _FailingAsyncFailStreamWrapper(_FakeAsyncStream(error=error)) with pytest.raises(RuntimeError, match="stream failure"): await anext(wrapper) asyncio.run(exercise()) def test_async_stream_wrapper_getattr_passthrough(): wrapper = _TestAsyncStreamWrapper(_FakeAsyncStream()) assert wrapper.extra_attribute == "passthrough" def test_async_stream_wrapper_exposes_wrapped_stream(): stream = _FakeAsyncStream() wrapper = _TestAsyncStreamWrapper(stream) assert wrapper.__wrapped__ is stream # pylint: disable=no-member def test_async_stream_wrapper_magic_method_passthrough(): wrapper = _TestAsyncStreamWrapper(_FakeAsyncStream()) assert len(wrapper) == 42 def test_async_stream_wrapper_stop_iteration_does_not_double_finalize(): async def exercise(): wrapper = _TestAsyncStreamWrapper(_FakeAsyncStream()) with pytest.raises(StopAsyncIteration): await anext(wrapper) await wrapper.close() assert wrapper._self_stop_count == 1 assert not wrapper._self_failures asyncio.run(exercise()) def test_async_stream_wrapper_swallows_process_chunk_errors(): async def exercise(): wrapper = _FailingAsyncProcessStreamWrapper(_FakeAsyncStream(chunks=["chunk"])) assert await anext(wrapper) == "chunk" assert not wrapper._self_failures asyncio.run(exercise()) opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/tests/test_toolcall.py000066400000000000000000000133151527070072300320410ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Tests for ToolCallRequest and ToolInvocation inheritance structure""" import pytest from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) from opentelemetry.sdk.trace.sampling import Decision, SamplingResult from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAI, ) from opentelemetry.util.genai.handler import TelemetryHandler from opentelemetry.util.genai.invocation import GenAIInvocation from opentelemetry.util.genai.types import ( InputMessage, ServerToolCall, ServerToolCallResponse, ToolCallRequest, ) def _make_handler() -> TelemetryHandler: return TelemetryHandler(tracer_provider=TracerProvider()) def test_toolcallrequest_is_message_part(): """ToolCallRequest is for message parts only""" tcr = ToolCallRequest(arguments={"location": "Paris"}, name="get_weather", id="call_123") msg = InputMessage(role="user", parts=[tcr]) assert len(msg.parts) == 1 def test_toolcall_inherits_from_genaiinvocation(): """ToolInvocation inherits from GenAIInvocation for lifecycle management""" handler = _make_handler() tc = handler.start_tool("get_weather", arguments={"city": "Paris"}) assert isinstance(tc, GenAIInvocation) assert not isinstance(tc, ToolCallRequest) tc.stop() def test_toolcall_has_attributes_dict(): """ToolInvocation inherits attributes dict from GenAIInvocation""" handler = _make_handler() tc = handler.start_tool("test") tc.attributes["custom.key"] = "value" assert tc.attributes["custom.key"] == "value" tc.stop() def test_toolcallrequest_in_message_part_union(): """ToolCallRequest (not ToolInvocation) is the correct type for message parts""" tc = ToolCallRequest(name="get_weather", arguments={"city": "Paris"}, id="call_123") msg = InputMessage(role="assistant", parts=[tc]) assert len(msg.parts) == 1 assert isinstance(msg.parts[0], ToolCallRequest) assert not isinstance(msg.parts[0], GenAIInvocation) def test_toolcall_operation_name(): """ToolInvocation operation_name is fixed to execute_tool""" handler = _make_handler() tc = handler.start_tool("my_tool") assert tc._operation_name == "execute_tool" tc.stop() def test_server_tool_call_basic(): """ServerToolCall can be created with required fields""" stc = ServerToolCall( name="code_interpreter", server_tool_call={"type": "code_interpreter", "code": "print(1)"}, ) assert stc.name == "code_interpreter" assert stc.server_tool_call == { "type": "code_interpreter", "code": "print(1)", } assert stc.id is None assert stc.type == "server_tool_call" def test_server_tool_call_with_id(): """ServerToolCall can have an optional id""" stc = ServerToolCall( name="web_search", server_tool_call={"type": "web_search", "query": "weather"}, id="stc_001", ) assert stc.id == "stc_001" def test_server_tool_call_response_basic(): """ServerToolCallResponse can be created with required fields""" stcr = ServerToolCallResponse( server_tool_call_response={ "type": "code_interpreter", "output": "1\n", }, ) assert stcr.server_tool_call_response == { "type": "code_interpreter", "output": "1\n", } assert stcr.id is None assert stcr.type == "server_tool_call_response" def test_server_tool_call_in_message(): """ServerToolCall and ServerToolCallResponse work as MessageParts""" stc = ServerToolCall( name="code_interpreter", server_tool_call={"type": "code_interpreter", "code": "x = 1"}, ) stcr = ServerToolCallResponse( server_tool_call_response={"type": "code_interpreter", "output": ""}, id="stc_001", ) msg = InputMessage(role="assistant", parts=[stc, stcr]) assert len(msg.parts) == 2 assert isinstance(msg.parts[0], ServerToolCall) assert isinstance(msg.parts[1], ServerToolCallResponse) if __name__ == "__main__": pytest.main([__file__, "-v"]) def test_start_tool_passes_sampling_attributes_at_span_creation(): """Verify that sampling-relevant attributes are available at start_span() time for tools.""" captured_attributes = {} class AttributeCapturingSampler: # pylint: disable=no-self-use def should_sample( self, parent_context, trace_id, name, kind=None, attributes=None, links=None, ): captured_attributes.update(attributes or {}) return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes) def get_description(self): return "AttributeCapturingSampler" span_exporter = InMemorySpanExporter() sampler_provider = TracerProvider(sampler=AttributeCapturingSampler()) sampler_provider.add_span_processor(SimpleSpanProcessor(span_exporter)) handler = TelemetryHandler(tracer_provider=sampler_provider) invocation = handler.start_tool( "get_weather", tool_call_id="call_123", tool_type="function", tool_description="Gets weather for a location", ) invocation.stop() assert captured_attributes[GenAI.GEN_AI_OPERATION_NAME] == "execute_tool" assert captured_attributes[GenAI.GEN_AI_TOOL_NAME] == "get_weather" assert captured_attributes[GenAI.GEN_AI_TOOL_CALL_ID] == "call_123" assert captured_attributes[GenAI.GEN_AI_TOOL_TYPE] == "function" assert captured_attributes[GenAI.GEN_AI_TOOL_DESCRIPTION] == "Gets weather for a location" opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/tests/test_upload.py000066400000000000000000000471231527070072300315200ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=no-name-in-module import logging import threading import time from contextlib import contextmanager from platform import python_implementation from typing import Any from unittest import TestCase, skipIf from unittest.mock import ANY, MagicMock, patch import fsspec from opentelemetry._logs import LogRecord from opentelemetry.test.test_base import TestBase from opentelemetry.util.genai import types from opentelemetry.util.genai._upload.completion_hook import ( UploadCompletionHook, ) # Use MemoryFileSystem for testing # https://filesystem-spec.readthedocs.io/en/latest/api.html#fsspec.implementations.memory.MemoryFileSystem BASE_PATH = "memory://" MAXSIZE = 5 FAKE_INPUTS = [ types.InputMessage( role="user", parts=[types.Text(content="What is the capital of France?")], ), types.InputMessage( role="assistant", parts=[ types.ToolCallRequest( id="get_capital_0", name="get_capital", arguments={"city": "Paris"}, ) ], ), types.InputMessage( role="user", parts=[types.ToolCallResponse(id="get_capital_0", response={"capital": "Paris"})], ), ] FAKE_OUTPUTS = [ types.OutputMessage( role="assistant", parts=[types.Text(content="Paris")], finish_reason="stop", ), ] FAKE_SYSTEM_INSTRUCTION = [types.Text(content="You are a helpful assistant.")] FAKE_TOOL_DEFINITIONS: list[types.ToolDefinition] = [ types.FunctionToolDefinition( name="test_tool", description="does something", parameters=None, type="function", ), ] class ThreadSafeMagicMock(MagicMock): def __init__(self, *args, **kwargs) -> None: self.__dict__["_lock"] = threading.Lock() super().__init__(*args, **kwargs) def _increment_mock_call(self, /, *args, **kwargs): with self.__dict__["_lock"]: super()._increment_mock_call(*args, **kwargs) class TestUploadCompletionHook(TestCase): def setUp(self): self._fsspec_patcher = patch("opentelemetry.util.genai._upload.completion_hook.fsspec") mock_fsspec = self._fsspec_patcher.start() self.mock_fs = ThreadSafeMagicMock() mock_fsspec.url_to_fs.return_value = self.mock_fs, "" self.mock_fs.exists.return_value = False self.hook = UploadCompletionHook(base_path=BASE_PATH, max_queue_size=MAXSIZE, lru_cache_max_size=5) # 1 upload is done when creating the UploadHook to ensure upload works. Reset mock. self.mock_fs.reset_mock() def tearDown(self) -> None: self.hook.shutdown() self._fsspec_patcher.stop() @contextmanager def block_upload(self): unblock_upload = threading.Event() def blocked_upload(*args: Any, **kwargs: Any): unblock_upload.wait() return MagicMock() try: self.mock_fs.open.side_effect = blocked_upload yield finally: unblock_upload.set() def test_shutdown_no_items(self): self.hook.shutdown() @skipIf( python_implementation().lower() == "pypy", "fails randomly on pypy: https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3812", ) def test_upload_then_shutdown(self): self.hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, tool_definitions=FAKE_TOOL_DEFINITIONS, ) # all items should be consumed self.hook.shutdown() # TODO: https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3812 fix flaky test that requires sleep. time.sleep(0.5) self.assertEqual( self.mock_fs.open.call_count, 4, "should have uploaded 4 files", ) def test_failed_upload_causes_initializer_to_throw(self): self.mock_fs.open.side_effect = ValueError("Failed for some reason!") with self.assertRaisesRegex( ValueError, "Failed to write file to the following path, upload is not working:", ): UploadCompletionHook( base_path=BASE_PATH, max_queue_size=MAXSIZE, lru_cache_max_size=5, ) def test_lru_cache_works(self): record = LogRecord() self.hook.on_completion( inputs=[], outputs=[], system_instruction=FAKE_SYSTEM_INSTRUCTION, tool_definitions=[], log_record=record, ) # Wait a bit for file upload to finish.. time.sleep(0.5) self.assertIsNotNone(record.attributes) self.assertTrue(self.hook._file_exists(record.attributes["gen_ai.system_instructions_ref"])) # LRU cache has a size of 5. So only AFTER 5 uploads should the original file be removed from the cache. for iteration in range(5): self.assertTrue(record.attributes["gen_ai.system_instructions_ref"] in self.hook.lru_dict) self.hook.on_completion( inputs=[], outputs=[], system_instruction=[types.Text(content=str(iteration))], tool_definitions=[], ) self.hook.shutdown() self.assertFalse(record.attributes["gen_ai.system_instructions_ref"] in self.hook.lru_dict) def test_upload_when_inputs_outputs_empty(self): record = LogRecord() self.hook.on_completion( inputs=[], outputs=[], system_instruction=FAKE_SYSTEM_INSTRUCTION, tool_definitions=[], log_record=record, ) # all items should be consumed self.hook.shutdown() self.assertEqual( self.mock_fs.open.call_count, 1, "should have uploaded 1 file", ) self.assertIsNotNone(record.attributes) for ref_key in [ "gen_ai.input.messages_ref", "gen_ai.output.messages_ref", "gen_ai.system_instructions_ref", ]: if ref_key == "gen_ai.system_instructions_ref": self.assertIn(ref_key, record.attributes) else: self.assertNotIn(ref_key, record.attributes) def test_upload_blocked(self): with self.block_upload(): # fill the queue for _ in range(MAXSIZE): self.hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, tool_definitions=FAKE_TOOL_DEFINITIONS, ) self.assertLessEqual( self.mock_fs.open.call_count, MAXSIZE, f"uploader should only be called {MAXSIZE=} times", ) with self.assertLogs(level=logging.WARNING) as logs: self.hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, tool_definitions=FAKE_TOOL_DEFINITIONS, ) self.assertIn("upload queue is full, dropping upload", logs.output[0]) def test_shutdown_timeout(self): with self.block_upload(): self.hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, tool_definitions=FAKE_TOOL_DEFINITIONS, ) # shutdown should timeout and return even though there are still items in the queue self.hook.shutdown(timeout_sec=0.01) def test_failed_upload_logs(self): self.mock_fs.open.side_effect = RuntimeError("failed to upload") with self.assertLogs(level=logging.ERROR) as logs: self.hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, tool_definitions=FAKE_TOOL_DEFINITIONS, ) self.hook.shutdown() self.assertIn("uploader failed", logs.output[0]) def test_invalid_upload_format(self): with self.assertRaisesRegex(ValueError, "Invalid upload_format"): UploadCompletionHook(base_path=BASE_PATH, upload_format="invalid") def test_upload_format_sets_content_type(self): for upload_format, expect_content_type in ( ("json", "application/json"), ("jsonl", "application/jsonl"), ): hook = UploadCompletionHook(base_path=BASE_PATH, upload_format=upload_format) self.addCleanup(hook.shutdown) hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, tool_definitions=FAKE_TOOL_DEFINITIONS, ) hook.shutdown() self.mock_fs.open.assert_called_with(ANY, "w", content_type=expect_content_type) def test_upload_after_shutdown_logs(self): self.hook.shutdown() with self.assertLogs(level=logging.INFO) as logs: self.hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, tool_definitions=FAKE_TOOL_DEFINITIONS, ) self.assertEqual(len(logs.output), 4) self.assertIn( "attempting to upload file after UploadCompletionHook.shutdown() was already called", logs.output[0], ) def test_threadpool_max_workers(self): for max_queue_size, expect_threadpool_workers in ((10, 10), (100, 64)): with patch("opentelemetry.util.genai._upload.completion_hook.ThreadPoolExecutor") as mock: hook = UploadCompletionHook(base_path=BASE_PATH, max_queue_size=max_queue_size) self.addCleanup(hook.shutdown) mock.assert_called_once_with(max_workers=expect_threadpool_workers) class TestUploadCompletionHookIntegration(TestBase): def setUp(self): super().setUp() self.hook = UploadCompletionHook(base_path=BASE_PATH) def create_hook(self) -> UploadCompletionHook: self.hook = UploadCompletionHook(base_path=BASE_PATH) return self.hook def tearDown(self): super().tearDown() self.hook.shutdown() def assert_fsspec_equal(self, path: str, value: str) -> None: with fsspec.open(path, "r") as file: self.assertEqual(file.read(), value) def test_system_insruction_is_hashed_to_avoid_reupload(self): expected_hash = "7e35acac4feca03ab47929d4cc6cfef1df2190ae1ee1752196a05ffc2a6cb360" # Create the file before upload.. expected_file_name = f"memory://{expected_hash}_system_instruction.json" with fsspec.open(expected_file_name, "wb") as file: file.write(b"asg") # FIle should exist. self.assertTrue(self.hook._file_exists(expected_file_name)) system_instructions = [ types.Text(content="You are a helpful assistant."), types.Text(content="You will do your best."), ] record = LogRecord() self.hook.on_completion( inputs=[], outputs=[], system_instruction=system_instructions, tool_definitions=[], log_record=record, ) self.hook.shutdown() self.assertIsNotNone(record.attributes) self.assertEqual( record.attributes["gen_ai.system_instructions_ref"], expected_file_name, ) # Content should not have been overwritten. self.assert_fsspec_equal(expected_file_name, "asg") def test_tool_definitions_is_hashed_to_avoid_reupload(self): expected_hash = "1f559d0102f8c440a667fd5ed587beeed488ec9f3ce0828d39c424bed6546cf5" # Create the file before upload.. expected_file_name = f"memory://{expected_hash}_tool.definitions.json" with fsspec.open(expected_file_name, "wb") as file: file.write(b"asg") # FIle should exist. self.assertTrue(self.hook._file_exists(expected_file_name)) tool_definitions = [ types.FunctionToolDefinition( name="some_tool", description="does something", parameters=None, type="function", ), ] record = LogRecord() self.hook.on_completion( inputs=[], outputs=[], system_instruction=[], tool_definitions=tool_definitions, log_record=record, ) self.hook.shutdown() self.assertIsNotNone(record.attributes) self.assertEqual( record.attributes["gen_ai.tool.definitions_ref"], expected_file_name, ) # Content should not have been overwritten. self.assert_fsspec_equal(expected_file_name, "asg") def test_upload_completions(self): tracer = self.tracer_provider.get_tracer(__name__) log_record = LogRecord() with tracer.start_as_current_span("chat mymodel") as span: self.hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, tool_definitions=FAKE_TOOL_DEFINITIONS, span=span, log_record=log_record, ) self.hook.shutdown() finished_spans = self.get_finished_spans() self.assertEqual(len(finished_spans), 1) span = finished_spans[0] # span attributes, log attributes, and log body have refs for attributes in [ span.attributes, log_record.attributes, ]: for ref_key in [ "gen_ai.input.messages_ref", "gen_ai.output.messages_ref", "gen_ai.system_instructions_ref", "gen_ai.tool.definitions_ref", ]: self.assertIn(ref_key, attributes) self.assert_fsspec_equal( span.attributes["gen_ai.input.messages_ref"], '[{"role":"user","parts":[{"content":"What is the capital of France?","type":"text"}]},{"role":"assistant","parts":[{"arguments":{"city":"Paris"},"name":"get_capital","id":"get_capital_0","type":"tool_call"}]},{"role":"user","parts":[{"response":{"capital":"Paris"},"id":"get_capital_0","type":"tool_call_response"}]}]\n', ) self.assert_fsspec_equal( span.attributes["gen_ai.output.messages_ref"], '[{"role":"assistant","parts":[{"content":"Paris","type":"text"}],"finish_reason":"stop"}]\n', ) self.assert_fsspec_equal( span.attributes["gen_ai.system_instructions_ref"], '[{"content":"You are a helpful assistant.","type":"text"}]\n', ) self.assert_fsspec_equal( span.attributes["gen_ai.tool.definitions_ref"], '[{"name":"test_tool","description":"does something","parameters":null,"type":"function"}]\n', ) def test_stamps_empty_log(self): log_record = LogRecord() self.hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, tool_definitions=FAKE_TOOL_DEFINITIONS, log_record=log_record, ) # stamp on both body and attributes self.assertIn("gen_ai.input.messages_ref", log_record.attributes) self.assertIn("gen_ai.output.messages_ref", log_record.attributes) self.assertIn("gen_ai.system_instructions_ref", log_record.attributes) self.assertIn("gen_ai.tool.definitions_ref", log_record.attributes) def test_upload_bytes(self) -> None: log_record = LogRecord() self.hook.on_completion( inputs=[ types.InputMessage( role="user", parts=[ types.Text(content="What is the capital of France?"), {"type": "generic_bytes", "bytes": b"hello"}, ], ) ], outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, tool_definitions=FAKE_TOOL_DEFINITIONS, log_record=log_record, ) self.hook.shutdown() self.assert_fsspec_equal( log_record.attributes["gen_ai.input.messages_ref"], '[{"role":"user","parts":[{"content":"What is the capital of France?","type":"text"},{"type":"generic_bytes","bytes":"aGVsbG8="}]}]\n', ) def test_upload_json(self) -> None: hook = UploadCompletionHook(base_path=BASE_PATH, upload_format="json") self.addCleanup(hook.shutdown) log_record = LogRecord() hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, tool_definitions=FAKE_TOOL_DEFINITIONS, log_record=log_record, ) hook.shutdown() ref_uri: str = log_record.attributes["gen_ai.input.messages_ref"] self.assertTrue(ref_uri.endswith(".json"), f"{ref_uri=} does not end with .json") self.assert_fsspec_equal( ref_uri, '[{"role":"user","parts":[{"content":"What is the capital of France?","type":"text"}]},{"role":"assistant","parts":[{"arguments":{"city":"Paris"},"name":"get_capital","id":"get_capital_0","type":"tool_call"}]},{"role":"user","parts":[{"response":{"capital":"Paris"},"id":"get_capital_0","type":"tool_call_response"}]}]\n', ) def test_upload_jsonlines(self) -> None: hook = UploadCompletionHook(base_path=BASE_PATH, upload_format="jsonl") self.addCleanup(hook.shutdown) log_record = LogRecord() hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, tool_definitions=FAKE_TOOL_DEFINITIONS, log_record=log_record, ) hook.shutdown() ref_uri: str = log_record.attributes["gen_ai.input.messages_ref"] self.assertTrue(ref_uri.endswith(".jsonl"), f"{ref_uri=} does not end with .jsonl") self.assert_fsspec_equal( ref_uri, """\ {"role":"user","parts":[{"content":"What is the capital of France?","type":"text"}],"index":0} {"role":"assistant","parts":[{"arguments":{"city":"Paris"},"name":"get_capital","id":"get_capital_0","type":"tool_call"}],"index":1} {"role":"user","parts":[{"response":{"capital":"Paris"},"id":"get_capital_0","type":"tool_call_response"}],"index":2} """, ) def test_upload_chained_filesystem_ref(self) -> None: """Using a chained filesystem like simplecache should refer to the final remote destination""" hook = UploadCompletionHook( base_path="simplecache::memory", upload_format="jsonl", ) self.addCleanup(hook.shutdown) log_record = LogRecord() hook.on_completion( inputs=FAKE_INPUTS, outputs=FAKE_OUTPUTS, system_instruction=FAKE_SYSTEM_INSTRUCTION, tool_definitions=FAKE_TOOL_DEFINITIONS, log_record=log_record, ) hook.shutdown() ref_uri: str = log_record.attributes["gen_ai.input.messages_ref"] self.assertTrue( ref_uri.startswith("memory://"), f"{ref_uri=} does not start with final destination uri memory://", ) opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/tests/test_upload_entrypoint.py000066400000000000000000000113331527070072300340050ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=import-outside-toplevel,no-name-in-module import importlib import logging import sys from contextlib import ExitStack from unittest.mock import patch import pytest from opentelemetry.util.genai._upload.completion_hook import ( UploadCompletionHook, ) from opentelemetry.util.genai.completion_hook import ( _NoOpCompletionHook, _SafeCompletionHook, ) from opentelemetry.util.genai.completion_hook import ( load_completion_hook as real_load_completion_hook, ) # Use MemoryFileSystem for testing # https://filesystem-spec.readthedocs.io/en/latest/api.html#fsspec.implementations.memory.MemoryFileSystem BASE_PATH = "memory://" @pytest.fixture(name="load_completion_hook") def fixture_load_completion_hook(): with ExitStack() as stack: def load_completion_hook(): hook = real_load_completion_hook() # load_completion_hook wraps user-provided hooks in _SafeCompletionHook; # unwrap here so test bodies can introspect the underlying upload hook. if isinstance(hook, _SafeCompletionHook): hook = hook._wrapped if isinstance(hook, UploadCompletionHook): stack.callback(hook.shutdown) return hook yield load_completion_hook @pytest.fixture(name="upload_environ", autouse=True) def fixture_upload_environ(): with patch.dict( "os.environ", { "OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK": "upload", "OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH": BASE_PATH, }, clear=True, ): yield def test_upload_entry_point(load_completion_hook): assert isinstance(load_completion_hook(), UploadCompletionHook) def test_upload_entry_point_no_fsspec(load_completion_hook): """Tests that the a no-op uploader is used when fsspec is not installed""" from opentelemetry.util.genai import _upload # noqa: PLC0415 # Simulate fsspec imports failing with patch.dict( sys.modules, {"opentelemetry.util.genai._upload.completion_hook": None}, ): importlib.reload(_upload) assert isinstance(load_completion_hook(), _NoOpCompletionHook) def test_upload_no_base_path(load_completion_hook, caplog): with patch.dict("os.environ", {"OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH": ""}): assert isinstance(load_completion_hook(), _NoOpCompletionHook) assert caplog.records[0].levelno == logging.WARNING assert ( caplog.records[0].message == "OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH is required but not set, using no-op instead" ) @pytest.mark.parametrize( "envvar_value,expect_format,expect_warn", ( (None, "json", False), ("", "json", False), ("json", "json", False), ("invalid", "json", True), ("jsonl", "jsonl", False), ("jSoNl", "jsonl", False), ), ) def test_parse_upload_format_envvar(envvar_value, expect_format, expect_warn, load_completion_hook, caplog): with patch.dict( "os.environ", {"OTEL_INSTRUMENTATION_GENAI_UPLOAD_FORMAT": envvar_value} if envvar_value is not None else {}, ): assert isinstance(hook := load_completion_hook(), UploadCompletionHook) assert hook._format == expect_format, ( f"expected upload format {expect_format=} with {envvar_value=} got {hook._format}" ) if expect_warn: assert caplog.records[0].levelno == logging.WARNING assert ( "is not a valid option for OTEL_INSTRUMENTATION_GENAI_UPLOAD_FORMAT, should be be one of ('json', 'jsonl'). Defaulting to json." in caplog.records[0].message ) @pytest.mark.parametrize( "envvar_value,expect_size,expect_warn", ( ("", 20, False), ("foobar", 20, True), ("1", 1, False), ("100", 100, False), ), ) def test_parse_max_queue_size_envvar(envvar_value, expect_size, expect_warn, load_completion_hook, caplog): with patch.dict( "os.environ", {"OTEL_INSTRUMENTATION_GENAI_UPLOAD_MAX_QUEUE_SIZE": envvar_value} if envvar_value is not None else {}, ): assert isinstance(hook := load_completion_hook(), UploadCompletionHook) assert hook._max_queue_size == expect_size, ( f"expected max_queue_size {expect_size=} with {envvar_value=} got {hook._format}" ) if expect_warn: assert caplog.records assert caplog.records[0].levelno == logging.WARNING assert ( "is not a valid integer for OTEL_INSTRUMENTATION_GENAI_UPLOAD_MAX_QUEUE_SIZE. Defaulting to 20." in caplog.records[0].message ) opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/tests/test_utils.py000066400000000000000000000743151527070072300313770ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import os import unittest from collections.abc import Mapping from typing import Any from unittest.mock import patch from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import ( InMemoryLogRecordExporter, SimpleLogRecordProcessor, ) from opentelemetry.sdk.trace import ReadableSpan, TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) from opentelemetry.sdk.trace.sampling import Decision, SamplingResult from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAI, ) from opentelemetry.semconv.attributes import ( error_attributes, server_attributes, ) from opentelemetry.semconv.schemas import Schemas from opentelemetry.trace.status import StatusCode from opentelemetry.util.genai.environment_variables import ( OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT, ) from opentelemetry.util.genai.handler import ( TelemetryHandler, get_telemetry_handler, ) from opentelemetry.util.genai.types import ( ContentCapturingMode, InputMessage, MessagePart, OutputMessage, Text, ) from opentelemetry.util.genai.utils import ( get_content_capturing_mode, ) def patch_env_vars(stability_mode, content_capturing, emit_event): def decorator(test_case): @patch.dict( os.environ, { OTEL_SEMCONV_STABILITY_OPT_IN: stability_mode, OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: content_capturing, OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT: emit_event, }, ) def wrapper(*args, **kwargs): # Reset state. _OpenTelemetrySemanticConventionStability._initialized = False _OpenTelemetrySemanticConventionStability._initialize() return test_case(*args, **kwargs) return wrapper return decorator def _create_input_message(content: str = "hello world", role: str = "Human") -> InputMessage: return InputMessage(role=role, parts=[Text(content=content)]) def _create_output_message(content: str = "hello back", finish_reason: str = "stop", role: str = "AI") -> OutputMessage: return OutputMessage(role=role, parts=[Text(content=content)], finish_reason=finish_reason) def _create_system_instruction( content: str = "You are a helpful assistant.", ) -> list[MessagePart]: return [Text(content=content)] def _get_single_span(span_exporter: InMemorySpanExporter) -> ReadableSpan: spans = span_exporter.get_finished_spans() assert len(spans) == 1 return spans[0] def _assert_span_time_order(span: ReadableSpan) -> None: assert span.start_time is not None assert span.end_time is not None assert span.end_time >= span.start_time def _get_span_attributes(span: ReadableSpan) -> Mapping[str, Any]: attrs = span.attributes assert attrs is not None return attrs def _assert_span_attributes(span_attrs: Mapping[str, Any], expected_values: Mapping[str, Any]) -> None: for key, value in expected_values.items(): assert span_attrs.get(key) == value assert len(span_attrs) == len(expected_values), f"Actual {span_attrs} are different than expected {expected_values}" def _get_messages_from_attr(span_attrs: Mapping[str, Any], attribute_name: str) -> list[dict[str, Any]]: payload = span_attrs.get(attribute_name) assert payload is not None assert isinstance(payload, str) return json.loads(payload) def _get_single_message(span_attrs: Mapping[str, Any], attribute_name: str) -> dict[str, Any]: messages = _get_messages_from_attr(span_attrs, attribute_name) assert len(messages) == 1 return messages[0] def _assert_text_message( message: Mapping[str, Any], role: str, content: str, finish_reason: str | None = None, ) -> None: assert message.get("role") == role parts = message.get("parts") assert isinstance(parts, list) and parts assert parts[0].get("content") == content if finish_reason is not None: assert message.get("finish_reason") == finish_reason def _normalize_to_list(value: Any) -> list[Any]: """Normalize tuple or list to list for OpenTelemetry compatibility.""" return list(value) if isinstance(value, tuple) else value def _normalize_to_dict(value: Any) -> dict[str, Any]: """Normalize tuple or dict to dict for OpenTelemetry compatibility.""" return dict(value) if isinstance(value, tuple) else value class TestVersion(unittest.TestCase): @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="SPAN_ONLY", emit_event="", ) def test_get_content_capturing_mode_parses_valid_envvar(self): # pylint: disable=no-self-use assert get_content_capturing_mode() == ContentCapturingMode.SPAN_ONLY @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="", emit_event="", ) def test_empty_content_capturing_envvar(self): # pylint: disable=no-self-use assert get_content_capturing_mode() == ContentCapturingMode.NO_CONTENT @patch_env_vars( stability_mode="default", content_capturing="True", emit_event="", ) def test_get_content_capturing_mode_raises_exception_when_semconv_stability_default( self, ): # pylint: disable=no-self-use with self.assertRaises(ValueError): get_content_capturing_mode() @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="INVALID_VALUE", emit_event="", ) def test_get_content_capturing_mode_raises_exception_on_invalid_envvar( self, ): # pylint: disable=no-self-use with self.assertLogs(level="WARNING") as cm: assert get_content_capturing_mode() == ContentCapturingMode.NO_CONTENT self.assertEqual(len(cm.output), 1) self.assertIn("INVALID_VALUE is not a valid option for ", cm.output[0]) class TestTelemetryHandler(unittest.TestCase): def setUp(self): self.span_exporter = InMemorySpanExporter() tracer_provider = TracerProvider() tracer_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter)) self.log_exporter = InMemoryLogRecordExporter() logger_provider = LoggerProvider() logger_provider.add_log_record_processor(SimpleLogRecordProcessor(self.log_exporter)) self.telemetry_handler = get_telemetry_handler(tracer_provider=tracer_provider, logger_provider=logger_provider) def tearDown(self): # Clear spans and reset the singleton telemetry handler so each test starts clean self.span_exporter.clear() self.log_exporter.clear() if hasattr(get_telemetry_handler, "_default_handler"): delattr(get_telemetry_handler, "_default_handler") @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="SPAN_ONLY", emit_event="", ) def test_llm_start_and_stop_creates_span(self): # pylint: disable=no-self-use message = _create_input_message("hello world") chat_generation = _create_output_message("hello back") system_instruction = _create_system_instruction() with self.telemetry_handler.inference( "test-provider", request_model="test-model", server_address="custom.server.com", server_port=42, ) as invocation: invocation.input_messages = [message] invocation.system_instruction = system_instruction invocation.attributes = {"custom_attr": "value"} invocation.temperature = 0.5 invocation.top_p = 0.9 invocation.stop_sequences = ["stop"] invocation.finish_reasons = ["stop"] invocation.response_model_name = "test-response-model" invocation.response_id = "response-id" invocation.input_tokens = 321 invocation.output_tokens = 654 assert invocation.span is not None invocation.output_messages = [chat_generation] invocation.attributes.update({"extra": "info"}) invocation.metric_attributes = {"should not be on span": "value"} span = _get_single_span(self.span_exporter) self.assertEqual(span.name, "chat test-model") self.assertEqual(span.kind, trace.SpanKind.CLIENT) _assert_span_time_order(span) span_attrs = _get_span_attributes(span) _assert_span_attributes( span_attrs, { GenAI.GEN_AI_OPERATION_NAME: "chat", GenAI.GEN_AI_REQUEST_MODEL: "test-model", GenAI.GEN_AI_PROVIDER_NAME: "test-provider", GenAI.GEN_AI_INPUT_MESSAGES: AnyNonNone(), GenAI.GEN_AI_OUTPUT_MESSAGES: AnyNonNone(), GenAI.GEN_AI_SYSTEM_INSTRUCTIONS: AnyNonNone(), GenAI.GEN_AI_REQUEST_TEMPERATURE: 0.5, GenAI.GEN_AI_REQUEST_TOP_P: 0.9, GenAI.GEN_AI_REQUEST_STOP_SEQUENCES: ("stop",), GenAI.GEN_AI_RESPONSE_FINISH_REASONS: ("stop",), GenAI.GEN_AI_RESPONSE_MODEL: "test-response-model", GenAI.GEN_AI_RESPONSE_ID: "response-id", GenAI.GEN_AI_USAGE_INPUT_TOKENS: 321, GenAI.GEN_AI_USAGE_OUTPUT_TOKENS: 654, server_attributes.SERVER_ADDRESS: "custom.server.com", server_attributes.SERVER_PORT: 42, "extra": "info", "custom_attr": "value", }, ) input_message = _get_single_message(span_attrs, "gen_ai.input.messages") output_message = _get_single_message(span_attrs, "gen_ai.output.messages") _assert_text_message(input_message, "Human", "hello world") _assert_text_message(output_message, "AI", "hello back", "stop") self.assertEqual(invocation.attributes.get("custom_attr"), "value") self.assertEqual(invocation.attributes.get("extra"), "info") # Verify system instruction is present in span as JSON string self.assertIn(GenAI.GEN_AI_SYSTEM_INSTRUCTIONS, span_attrs) span_system = json.loads(span_attrs[GenAI.GEN_AI_SYSTEM_INSTRUCTIONS]) self.assertIsInstance(span_system, list) self.assertEqual(span_system[0]["content"], "You are a helpful assistant.") self.assertEqual(span_system[0]["type"], "text") @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="SPAN_ONLY", emit_event="", ) def test_llm_manual_start_and_stop_creates_span(self): message = _create_input_message("hi") chat_generation = _create_output_message("ok") invocation = self.telemetry_handler.start_inference("test-provider", request_model="manual-model") invocation.input_messages = [message] invocation.attributes["manual"] = True assert invocation.span is not None invocation.output_messages = [chat_generation] invocation.attributes.update({"extra_manual": "yes"}) invocation.stop() span = _get_single_span(self.span_exporter) assert span.name == "chat manual-model" assert span.kind == trace.SpanKind.CLIENT _assert_span_time_order(span) attrs = _get_span_attributes(span) _assert_span_attributes( attrs, { GenAI.GEN_AI_OPERATION_NAME: "chat", GenAI.GEN_AI_REQUEST_MODEL: "manual-model", GenAI.GEN_AI_PROVIDER_NAME: "test-provider", GenAI.GEN_AI_INPUT_MESSAGES: AnyNonNone(), GenAI.GEN_AI_OUTPUT_MESSAGES: AnyNonNone(), GenAI.GEN_AI_RESPONSE_FINISH_REASONS: ("stop",), "manual": True, "extra_manual": "yes", }, ) def test_start_inference_passes_sampling_attributes_at_span_creation(self): """Verify that sampling-relevant attributes are available at start_span() time.""" captured_attributes = {} class AttributeCapturingSampler: # pylint: disable=no-self-use """A sampler that records the attributes passed to should_sample.""" def should_sample( self, parent_context, trace_id, name, kind=None, attributes=None, links=None, ): captured_attributes.update(attributes or {}) return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes) def get_description(self): return "AttributeCapturingSampler" sampler_provider = TracerProvider(sampler=AttributeCapturingSampler()) sampler_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter)) handler = TelemetryHandler(tracer_provider=sampler_provider) invocation = handler.start_inference( "test-provider", request_model="sampler-model", server_address="api.example.com", server_port=8080, ) invocation.stop() assert captured_attributes[GenAI.GEN_AI_OPERATION_NAME] == "chat" assert captured_attributes[GenAI.GEN_AI_REQUEST_MODEL] == "sampler-model" assert captured_attributes[GenAI.GEN_AI_PROVIDER_NAME] == "test-provider" assert captured_attributes[server_attributes.SERVER_ADDRESS] == "api.example.com" assert captured_attributes[server_attributes.SERVER_PORT] == 8080 def test_start_inference_sampler_can_drop_span_based_on_attributes(self): """Verify that a sampler can reject spans based on attributes passed at creation time.""" class ModelRejectingSampler: # pylint: disable=no-self-use """Drops spans whose gen_ai.request.model matches the reject list.""" def __init__(self, reject_models): self._reject_models = reject_models def should_sample( self, parent_context, trace_id, name, kind=None, attributes=None, links=None, ): model = (attributes or {}).get(GenAI.GEN_AI_REQUEST_MODEL) if model in self._reject_models: return SamplingResult(Decision.DROP) return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes) def get_description(self): return "ModelRejectingSampler" sampler_provider = TracerProvider(sampler=ModelRejectingSampler(reject_models={"rejected-model"})) sampler_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter)) handler = TelemetryHandler(tracer_provider=sampler_provider) # This invocation should be dropped invocation = handler.start_inference("test-provider", request_model="rejected-model") invocation.stop() # This invocation should be recorded invocation = handler.start_inference("test-provider", request_model="accepted-model") invocation.stop() spans = self.span_exporter.get_finished_spans() assert len(spans) == 1 assert spans[0].name == "chat accepted-model" def test_start_embedding_passes_sampling_attributes_at_span_creation(self): """Verify that sampling-relevant attributes are available at start_span() time for embeddings.""" captured_attributes = {} class AttributeCapturingSampler: # pylint: disable=no-self-use def should_sample( self, parent_context, trace_id, name, kind=None, attributes=None, links=None, ): captured_attributes.update(attributes or {}) return SamplingResult(Decision.RECORD_AND_SAMPLE, attributes) def get_description(self): return "AttributeCapturingSampler" sampler_provider = TracerProvider(sampler=AttributeCapturingSampler()) sampler_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter)) handler = TelemetryHandler(tracer_provider=sampler_provider) invocation = handler.start_embedding( "test-provider", request_model="embed-model", server_address="embed.example.com", server_port=443, ) invocation.stop() assert captured_attributes[GenAI.GEN_AI_OPERATION_NAME] == "embeddings" assert captured_attributes[GenAI.GEN_AI_REQUEST_MODEL] == "embed-model" assert captured_attributes[GenAI.GEN_AI_PROVIDER_NAME] == "test-provider" assert captured_attributes[server_attributes.SERVER_ADDRESS] == "embed.example.com" assert captured_attributes[server_attributes.SERVER_PORT] == 443 def test_llm_span_finish_reasons_without_output_messages(self): invocation = self.telemetry_handler.start_inference("test-provider", request_model="model-without-output") invocation.finish_reasons = ["length"] invocation.response_model_name = "alt-model" invocation.response_id = "resp-001" invocation.input_tokens = 12 invocation.output_tokens = 34 assert invocation.span is not None invocation.stop() span = _get_single_span(self.span_exporter) _assert_span_time_order(span) attrs = _get_span_attributes(span) _assert_span_attributes( attrs, { GenAI.GEN_AI_OPERATION_NAME: "chat", GenAI.GEN_AI_REQUEST_MODEL: "model-without-output", GenAI.GEN_AI_PROVIDER_NAME: "test-provider", GenAI.GEN_AI_RESPONSE_FINISH_REASONS: ("length",), GenAI.GEN_AI_RESPONSE_MODEL: "alt-model", GenAI.GEN_AI_RESPONSE_ID: "resp-001", GenAI.GEN_AI_USAGE_INPUT_TOKENS: 12, GenAI.GEN_AI_USAGE_OUTPUT_TOKENS: 34, }, ) def test_llm_span_finish_reasons_from_invocation(self): invocation = self.telemetry_handler.start_inference("test-provider", request_model="model-reasons") invocation.finish_reasons = ["stop", "length", "stop"] assert invocation.span is not None invocation.stop() span = _get_single_span(self.span_exporter) attrs = _get_span_attributes(span) self.assertEqual( attrs[GenAI.GEN_AI_RESPONSE_FINISH_REASONS], ("stop", "length", "stop"), ) def test_llm_span_finish_reasons_from_output_messages(self): invocation = self.telemetry_handler.start_inference("test-provider", request_model="model-output-reasons") assert invocation.span is not None invocation.output_messages = [ _create_output_message("response-1", finish_reason="stop"), _create_output_message("response-2", finish_reason="length"), _create_output_message("response-3", finish_reason="stop"), ] invocation.stop() span = _get_single_span(self.span_exporter) attrs = _get_span_attributes(span) self.assertEqual( attrs[GenAI.GEN_AI_RESPONSE_FINISH_REASONS], ("stop", "length", "stop"), ) def test_llm_span_uses_expected_schema_url(self): invocation = self.telemetry_handler.start_inference("schema-provider", request_model="schema-model") assert invocation.span is not None invocation.stop() span = _get_single_span(self.span_exporter) instrumentation = getattr(span, "instrumentation_scope", None) if instrumentation is None: instrumentation = getattr(span, "instrumentation_info", None) assert instrumentation is not None assert getattr(instrumentation, "schema_url", None) == Schemas.V1_37_0.value @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="EVENT_ONLY", emit_event="true", ) def test_llm_log_uses_expected_schema_url(self): invocation = self.telemetry_handler.start_inference("schema-provider", request_model="schema-model") invocation.output_messages = [_create_output_message()] invocation.stop() logs = self.log_exporter.get_finished_logs() self.assertEqual(len(logs), 1) self.assertEqual(logs[0].instrumentation_scope.schema_url, Schemas.V1_37_0.value) @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="SPAN_ONLY", emit_event="", ) def test_parent_child_span_relationship(self): message = _create_input_message("hi") chat_generation = _create_output_message("ok") with self.telemetry_handler.inference("test-provider", request_model="parent-model") as parent_invocation: parent_invocation.input_messages = [message] with self.telemetry_handler.inference("test-provider", request_model="child-model") as child_invocation: child_invocation.input_messages = [message] # Stop child first by exiting inner context child_invocation.output_messages = [chat_generation] # Then stop parent by exiting outer context parent_invocation.output_messages = [chat_generation] spans = self.span_exporter.get_finished_spans() assert len(spans) == 2 # Identify spans irrespective of export order child_span = next(s for s in spans if s.name == "chat child-model") parent_span = next(s for s in spans if s.name == "chat parent-model") # Same trace assert child_span.context.trace_id == parent_span.context.trace_id # Child has parent set to parent's span id assert child_span.parent is not None assert child_span.parent.span_id == parent_span.context.span_id # Parent should not have a parent (root) assert parent_span.parent is None @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="SPAN_ONLY", emit_event="", ) def test_embedding_parent_child_span_relationship(self): parent_invocation = self.telemetry_handler.start_embedding("test-provider", request_model="embed-parent-model") parent_invocation.input_tokens = 10 assert parent_invocation.span is not None child_invocation = self.telemetry_handler.start_embedding("test-provider", request_model="embed-child-model") child_invocation.input_tokens = 5 assert child_invocation.span is not None child_invocation.stop() parent_invocation.stop() spans = self.span_exporter.get_finished_spans() assert len(spans) == 2 child_span = next(s for s in spans if s.name == "embeddings embed-child-model") parent_span = next(s for s in spans if s.name == "embeddings embed-parent-model") assert child_span.context.trace_id == parent_span.context.trace_id assert child_span.parent is not None assert child_span.parent.span_id == parent_span.context.span_id assert parent_span.parent is None @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="SPAN_ONLY", emit_event="", ) def test_llm_parent_embedding_child_span_relationship(self): message = _create_input_message("hi") chat_generation = _create_output_message("ok") with self.telemetry_handler.inference("test-provider", request_model="parent-model") as parent_invocation: parent_invocation.input_messages = [message] child_invocation = self.telemetry_handler.start_embedding( "test-provider", request_model="embed-child-model" ) child_invocation.input_tokens = 3 assert child_invocation.span is not None child_invocation.stop() parent_invocation.output_messages = [chat_generation] spans = self.span_exporter.get_finished_spans() assert len(spans) == 2 child_span = next(s for s in spans if s.name == "embeddings embed-child-model") parent_span = next(s for s in spans if s.name == "chat parent-model") assert child_span.context.trace_id == parent_span.context.trace_id assert child_span.parent is not None assert child_span.parent.span_id == parent_span.context.span_id assert parent_span.parent is None def test_llm_context_manager_error_path_records_error_status_and_attrs( self, ): class BoomError(RuntimeError): pass message = _create_input_message("hi", role="user") with self.assertRaises(BoomError): with self.telemetry_handler.inference("test-provider", request_model="test-model") as invocation: invocation.input_messages = [message] for attr, value in { "max_tokens": 128, "seed": 123, "finish_reasons": ["error"], "response_model_name": "error-model", "response_id": "error-response", "input_tokens": 11, "output_tokens": 22, }.items(): setattr(invocation, attr, value) raise BoomError("boom") span = _get_single_span(self.span_exporter) assert span.status.status_code == StatusCode.ERROR _assert_span_time_order(span) span_attrs = _get_span_attributes(span) _assert_span_attributes( span_attrs, { GenAI.GEN_AI_OPERATION_NAME: "chat", GenAI.GEN_AI_REQUEST_MODEL: "test-model", GenAI.GEN_AI_PROVIDER_NAME: "test-provider", GenAI.GEN_AI_REQUEST_MAX_TOKENS: 128, GenAI.GEN_AI_REQUEST_SEED: 123, GenAI.GEN_AI_RESPONSE_FINISH_REASONS: ("error",), GenAI.GEN_AI_RESPONSE_MODEL: "error-model", GenAI.GEN_AI_RESPONSE_ID: "error-response", GenAI.GEN_AI_USAGE_INPUT_TOKENS: 11, GenAI.GEN_AI_USAGE_OUTPUT_TOKENS: 22, error_attributes.ERROR_TYPE: BoomError.__qualname__, }, ) def test_embedding_context_manager_error_path_records_error_status_and_attrs( self, ): class BoomError(RuntimeError): pass with self.assertRaises(BoomError): with self.telemetry_handler.embedding( "test-provider", request_model="embed-model", server_address="embed.example.com", server_port=443, ) as invocation: invocation.dimension_count = 1536 invocation.input_tokens = 7 invocation.attributes["custom_embed_attr"] = "value" invocation.response_model_name = "embed-response-model" raise BoomError("embedding boom") span = _get_single_span(self.span_exporter) assert span.status.status_code == StatusCode.ERROR _assert_span_time_order(span) span_attrs = _get_span_attributes(span) _assert_span_attributes( span_attrs, { GenAI.GEN_AI_OPERATION_NAME: "embeddings", GenAI.GEN_AI_REQUEST_MODEL: "embed-model", GenAI.GEN_AI_PROVIDER_NAME: "test-provider", GenAI.GEN_AI_EMBEDDINGS_DIMENSION_COUNT: 1536, GenAI.GEN_AI_USAGE_INPUT_TOKENS: 7, GenAI.GEN_AI_RESPONSE_MODEL: "embed-response-model", server_attributes.SERVER_ADDRESS: "embed.example.com", server_attributes.SERVER_PORT: 443, "custom_embed_attr": "value", error_attributes.ERROR_TYPE: BoomError.__qualname__, }, ) @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="SPAN_ONLY", emit_event="", ) def test_embedding_manual_start_and_stop_creates_span(self): invocation = self.telemetry_handler.start_embedding( "test-provider", request_model="embed-model", server_address="custom.server.com", server_port=42, ) invocation.dimension_count = 1536 invocation.encoding_formats = ["float"] invocation.input_tokens = 123 invocation.attributes["custom_embed_attr"] = "value" assert invocation.span is not None invocation.attributes.update({"extra_embed": "info"}) invocation.metric_attributes = {"should not be on span": "value"} invocation.stop() span = _get_single_span(self.span_exporter) self.assertEqual(span.name, "embeddings embed-model") self.assertEqual(span.kind, trace.SpanKind.CLIENT) _assert_span_time_order(span) attrs = _get_span_attributes(span) _assert_span_attributes( attrs, { GenAI.GEN_AI_OPERATION_NAME: "embeddings", GenAI.GEN_AI_REQUEST_MODEL: "embed-model", GenAI.GEN_AI_PROVIDER_NAME: "test-provider", GenAI.GEN_AI_EMBEDDINGS_DIMENSION_COUNT: 1536, GenAI.GEN_AI_REQUEST_ENCODING_FORMATS: ("float",), GenAI.GEN_AI_USAGE_INPUT_TOKENS: 123, server_attributes.SERVER_ADDRESS: "custom.server.com", server_attributes.SERVER_PORT: 42, "custom_embed_attr": "value", "extra_embed": "info", }, ) def test_fail_with_exception_sets_error_status_and_type(self): class BoomError(RuntimeError): pass invocation = self.telemetry_handler.start_inference("test-provider", request_model="test-model") invocation.fail(BoomError("boom")) span = _get_single_span(self.span_exporter) assert span.status.status_code == StatusCode.ERROR assert span.status.description == "boom" assert _get_span_attributes(span)[error_attributes.ERROR_TYPE] == BoomError.__qualname__ class AnyNonNone: def __eq__(self, other): return other is not None opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/tests/test_utils_events.py000066400000000000000000000324321527070072300327550ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import unittest from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import ( InMemoryLogRecordExporter, SimpleLogRecordProcessor, ) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAI, ) from opentelemetry.semconv.attributes import error_attributes from opentelemetry.util.genai.handler import get_telemetry_handler from opentelemetry.util.genai.types import Error from .test_utils import ( _create_input_message, _create_output_message, _create_system_instruction, _get_single_span, _get_span_attributes, _normalize_to_dict, _normalize_to_list, patch_env_vars, ) class TestTelemetryHandlerEvents(unittest.TestCase): def setUp(self): self.span_exporter = InMemorySpanExporter() tracer_provider = TracerProvider() tracer_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter)) self.log_exporter = InMemoryLogRecordExporter() logger_provider = LoggerProvider() logger_provider.add_log_record_processor(SimpleLogRecordProcessor(self.log_exporter)) self.telemetry_handler = get_telemetry_handler(tracer_provider=tracer_provider, logger_provider=logger_provider) def tearDown(self): self.span_exporter.clear() self.log_exporter.clear() if hasattr(get_telemetry_handler, "_default_handler"): delattr(get_telemetry_handler, "_default_handler") @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="EVENT_ONLY", emit_event="true", ) def test_emits_llm_event(self): invocation = self.telemetry_handler.start_inference("test-provider", request_model="event-model") invocation.input_messages = [_create_input_message("test query")] invocation.system_instruction = _create_system_instruction() invocation.temperature = 0.7 invocation.max_tokens = 100 invocation.response_model_name = "response-model" invocation.response_id = "event-response-id" invocation.input_tokens = 10 invocation.output_tokens = 20 invocation.output_messages = [_create_output_message("test response")] invocation.stop() # Check that event was emitted logs = self.log_exporter.get_finished_logs() self.assertEqual(len(logs), 1) log_record = logs[0].log_record # Verify event name self.assertEqual(log_record.event_name, "gen_ai.client.inference.operation.details") # Verify event attributes attrs = log_record.attributes self.assertIsNotNone(attrs) self.assertEqual(attrs[GenAI.GEN_AI_OPERATION_NAME], "chat") self.assertEqual(attrs[GenAI.GEN_AI_REQUEST_MODEL], "event-model") self.assertEqual(attrs[GenAI.GEN_AI_PROVIDER_NAME], "test-provider") self.assertEqual(attrs[GenAI.GEN_AI_REQUEST_TEMPERATURE], 0.7) self.assertEqual(attrs[GenAI.GEN_AI_REQUEST_MAX_TOKENS], 100) self.assertEqual(attrs[GenAI.GEN_AI_RESPONSE_MODEL], "response-model") self.assertEqual(attrs[GenAI.GEN_AI_RESPONSE_ID], "event-response-id") self.assertEqual(attrs[GenAI.GEN_AI_USAGE_INPUT_TOKENS], 10) self.assertEqual(attrs[GenAI.GEN_AI_USAGE_OUTPUT_TOKENS], 20) # Verify messages are in structured format (not JSON string) # OpenTelemetry may convert lists to tuples, so we normalize input_msg = _normalize_to_dict(_normalize_to_list(attrs[GenAI.GEN_AI_INPUT_MESSAGES])[0]) self.assertEqual(input_msg["role"], "Human") self.assertEqual(_normalize_to_list(input_msg["parts"])[0]["content"], "test query") output_msg = _normalize_to_dict(_normalize_to_list(attrs[GenAI.GEN_AI_OUTPUT_MESSAGES])[0]) self.assertEqual(output_msg["role"], "AI") self.assertEqual( _normalize_to_list(output_msg["parts"])[0]["content"], "test response", ) self.assertEqual(output_msg["finish_reason"], "stop") # Verify system instruction is present in event in structured format sys_instr = _normalize_to_dict(_normalize_to_list(attrs[GenAI.GEN_AI_SYSTEM_INSTRUCTIONS])[0]) self.assertEqual(sys_instr["content"], "You are a helpful assistant.") self.assertEqual(sys_instr["type"], "text") # Verify event context matches span context span = _get_single_span(self.span_exporter) self.assertIsNotNone(log_record.trace_id) self.assertIsNotNone(log_record.span_id) self.assertIsNotNone(span.context) self.assertEqual(log_record.trace_id, span.context.trace_id) self.assertEqual(log_record.span_id, span.context.span_id) @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="SPAN_AND_EVENT", emit_event="true", ) def test_emits_llm_event_and_span(self): message = _create_input_message("combined test") chat_generation = _create_output_message("combined response") system_instruction = _create_system_instruction("System prompt here") invocation = self.telemetry_handler.start_inference("test-provider", request_model="combined-model") invocation.input_messages = [message] invocation.system_instruction = system_instruction invocation.output_messages = [chat_generation] invocation.stop() # Check span was created span = _get_single_span(self.span_exporter) span_attrs = _get_span_attributes(span) self.assertIn(GenAI.GEN_AI_INPUT_MESSAGES, span_attrs) # Check event was emitted logs = self.log_exporter.get_finished_logs() self.assertEqual(len(logs), 1) log_record = logs[0].log_record self.assertEqual(log_record.event_name, "gen_ai.client.inference.operation.details") self.assertIn(GenAI.GEN_AI_INPUT_MESSAGES, log_record.attributes) # Verify system instruction in both span and event self.assertIn(GenAI.GEN_AI_SYSTEM_INSTRUCTIONS, span_attrs) span_system = json.loads(span_attrs[GenAI.GEN_AI_SYSTEM_INSTRUCTIONS]) self.assertEqual(span_system[0]["content"], "System prompt here") event_attrs = log_record.attributes self.assertIn(GenAI.GEN_AI_SYSTEM_INSTRUCTIONS, event_attrs) event_system = event_attrs[GenAI.GEN_AI_SYSTEM_INSTRUCTIONS] event_system_list = list(event_system) if isinstance(event_system, tuple) else event_system event_sys_instr = ( dict(event_system_list[0]) if isinstance(event_system_list[0], tuple) else event_system_list[0] ) self.assertEqual(event_sys_instr["content"], "System prompt here") # Verify event context matches span context span = _get_single_span(self.span_exporter) self.assertIsNotNone(log_record.trace_id) self.assertIsNotNone(log_record.span_id) self.assertIsNotNone(span.context) self.assertEqual(log_record.trace_id, span.context.trace_id) self.assertEqual(log_record.span_id, span.context.span_id) @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="EVENT_ONLY", emit_event="true", ) def test_emits_llm_event_with_error(self): class TestError(RuntimeError): pass message = _create_input_message("error test") invocation = self.telemetry_handler.start_inference("test-provider", request_model="error-model") invocation.input_messages = [message] error = Error(message="Test error occurred", type=TestError) invocation.fail(error) # Check event was emitted logs = self.log_exporter.get_finished_logs() self.assertEqual(len(logs), 1) log_record = logs[0].log_record attrs = log_record.attributes # Verify error attribute is present self.assertEqual(attrs[error_attributes.ERROR_TYPE], TestError.__qualname__) self.assertEqual(attrs[GenAI.GEN_AI_OPERATION_NAME], "chat") self.assertEqual(attrs[GenAI.GEN_AI_REQUEST_MODEL], "error-model") # Verify event context matches span context span = _get_single_span(self.span_exporter) self.assertIsNotNone(log_record.trace_id) self.assertIsNotNone(log_record.span_id) self.assertIsNotNone(span.context) self.assertEqual(log_record.trace_id, span.context.trace_id) self.assertEqual(log_record.span_id, span.context.span_id) @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="EVENT_ONLY", emit_event="false", ) def test_does_not_emit_llm_event_when_emit_event_false(self): message = _create_input_message("emit false test") chat_generation = _create_output_message("emit false response") invocation = self.telemetry_handler.start_inference("test-provider", request_model="emit-false-model") invocation.input_messages = [message] invocation.output_messages = [chat_generation] invocation.stop() # Check no event was emitted logs = self.log_exporter.get_finished_logs() self.assertEqual(len(logs), 0) @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="NO_CONTENT", emit_event="", ) def test_does_not_emit_llm_event_by_default_for_no_content(self): """Test that event is not emitted by default when content_capturing is NO_CONTENT and OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT is not set.""" invocation = self.telemetry_handler.start_inference("test-provider", request_model="default-model") invocation.input_messages = [_create_input_message("default test")] invocation.output_messages = [_create_output_message("default response")] invocation.stop() # Check that no event was emitted (NO_CONTENT defaults to False) logs = self.log_exporter.get_finished_logs() self.assertEqual(len(logs), 0) @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="SPAN_ONLY", emit_event="", ) def test_does_not_emit_llm_event_by_default_for_span_only(self): """Test that event is not emitted by default when content_capturing is SPAN_ONLY and OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT is not set.""" invocation = self.telemetry_handler.start_inference("test-provider", request_model="default-model") invocation.input_messages = [_create_input_message("default test")] invocation.output_messages = [_create_output_message("default response")] invocation.stop() # Check that no event was emitted (SPAN_ONLY defaults to False) logs = self.log_exporter.get_finished_logs() self.assertEqual(len(logs), 0) @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="EVENT_ONLY", emit_event="", ) def test_emits_llm_event_by_default_for_event_only(self): """Test that event is emitted by default when content_capturing is EVENT_ONLY and OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT is not set.""" invocation = self.telemetry_handler.start_inference("test-provider", request_model="default-model") invocation.input_messages = [_create_input_message("default test")] invocation.output_messages = [_create_output_message("default response")] invocation.stop() # Check that event was emitted (EVENT_ONLY defaults to True) logs = self.log_exporter.get_finished_logs() self.assertEqual(len(logs), 1) log_record = logs[0].log_record self.assertEqual(log_record.event_name, "gen_ai.client.inference.operation.details") @patch_env_vars( stability_mode="gen_ai_latest_experimental", content_capturing="SPAN_AND_EVENT", emit_event="", ) def test_emits_llm_event_by_default_for_span_and_event(self): """Test that event is emitted by default when content_capturing is SPAN_AND_EVENT and OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT is not set.""" message = _create_input_message("span and event test") chat_generation = _create_output_message("span and event response") system_instruction = _create_system_instruction("System prompt") invocation = self.telemetry_handler.start_inference("test-provider", request_model="span-and-event-model") invocation.input_messages = [message] invocation.system_instruction = system_instruction invocation.output_messages = [chat_generation] invocation.stop() # Check span was created span = _get_single_span(self.span_exporter) span_attrs = _get_span_attributes(span) self.assertIn(GenAI.GEN_AI_INPUT_MESSAGES, span_attrs) # Check that event was emitted (SPAN_AND_EVENT defaults to True) logs = self.log_exporter.get_finished_logs() self.assertEqual(len(logs), 1) log_record = logs[0].log_record self.assertEqual(log_record.event_name, "gen_ai.client.inference.operation.details") self.assertIn(GenAI.GEN_AI_INPUT_MESSAGES, log_record.attributes) opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/tests/test_version.py000066400000000000000000000011111527070072300317040ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from opentelemetry.util.genai.version import __version__ class TestVersion(unittest.TestCase): def test_version_exists(self): """Test that version is defined and is a string.""" self.assertIsInstance(__version__, str) self.assertTrue(len(__version__) > 0) def test_version_format(self): """Test that version follows expected format.""" # Should be in format like "0.1b0.dev" or similar self.assertRegex(__version__, r"^\d+\.\d+.*") opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-genai/tests/test_workflow_invocation.py000066400000000000000000000074211527070072300343340ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) from opentelemetry.trace import INVALID_SPAN from opentelemetry.util.genai.handler import TelemetryHandler from opentelemetry.util.genai.types import ( InputMessage, OutputMessage, Text, ) class TestWorkflowInvocation(unittest.TestCase): def setUp(self): self.span_exporter = InMemorySpanExporter() tracer_provider = TracerProvider() tracer_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter)) self.handler = TelemetryHandler(tracer_provider=tracer_provider) def test_default_values(self): invocation = self.handler.start_workflow(name=None) invocation.stop() assert invocation.name is None assert invocation._operation_name == "invoke_workflow" assert not invocation.input_messages assert not invocation.output_messages assert invocation.span is not INVALID_SPAN assert not invocation.attributes def test_custom_name(self): invocation = self.handler.start_workflow(name="customer_support_pipeline") invocation.stop() assert invocation.name == "customer_support_pipeline" def test_with_input_messages(self): msg = InputMessage(role="user", parts=[Text(content="hello")]) invocation = self.handler.start_workflow(name="test") invocation.input_messages = [msg] invocation.stop() assert len(invocation.input_messages) == 1 assert invocation.input_messages[0].role == "user" def test_with_output_messages(self): msg = OutputMessage(role="assistant", parts=[Text(content="hi")], finish_reason="stop") invocation = self.handler.start_workflow(name="test") invocation.output_messages = [msg] invocation.stop() assert len(invocation.output_messages) == 1 assert invocation.output_messages[0].finish_reason == "stop" def test_inherits_genai_invocation(self): invocation = self.handler.start_workflow(name="test") invocation.attributes["key"] = "value" invocation.stop() spans = self.span_exporter.get_finished_spans() assert spans[0].attributes is not None assert spans[0].attributes["key"] == "value" def test_default_lists_are_independent(self): """Ensure separate invocations get separate list instances.""" inv1 = self.handler.start_workflow(name=None) inv2 = self.handler.start_workflow(name=None) inv1.input_messages.append(InputMessage(role="user", parts=[])) assert len(inv2.input_messages) == 0 inv1.stop() inv2.stop() def test_default_attributes_are_independent(self): inv1 = self.handler.start_workflow(name=None) inv2 = self.handler.start_workflow(name=None) inv1.attributes["foo"] = "bar" assert "foo" not in inv2.attributes inv1.stop() inv2.stop() def test_full_construction(self): inp = InputMessage(role="user", parts=[Text(content="query")]) out = OutputMessage( role="assistant", parts=[Text(content="answer")], finish_reason="stop", ) invocation = self.handler.start_workflow(name="my_workflow") invocation.input_messages = [inp] invocation.output_messages = [out] invocation.stop() assert invocation.name == "my_workflow" assert len(invocation.input_messages) == 1 assert len(invocation.output_messages) == 1 assert invocation.output_messages[0].parts[0].content == "answer" opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/000077500000000000000000000000001527070072300253465ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/README.rst000066400000000000000000000030641527070072300270400ustar00rootroot00000000000000OpenTelemetry Util HTTP ======================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-util-http.svg :target: https://pypi.org/project/opentelemetry-util-http/ This library provides ASGI, WSGI middleware and other HTTP-related functionality that is common to instrumented web frameworks (such as Django, Starlette, FastAPI, etc.) to track requests timing through OpenTelemetry. Installation ------------ :: pip install opentelemetry-util-http Usage (Quart) ------------- .. code-block:: python from quart import Quart from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware app = Quart(__name__) app.asgi_app = OpenTelemetryMiddleware(app.asgi_app) @app.route("/") async def hello(): return "Hello!" if __name__ == "__main__": app.run(debug=True) Usage (Django 3.0) ------------------ Modify the application's ``asgi.py`` file as shown below. .. code-block:: python import os from django.core.asgi import get_asgi_application from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'asgi_example.settings') application = get_asgi_application() application = OpenTelemetryMiddleware(application) Usage (Raw ASGI) ---------------- .. code-block:: python from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware app = ... # An ASGI application. app = OpenTelemetryMiddleware(app) References ---------- * `OpenTelemetry Project `_ opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/pyproject.toml000066400000000000000000000023101527070072300302560ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-util-http" dynamic = ["version"] description = "Web util for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/util/opentelemetry-util-http" Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" [tool.hatch.version] path = "src/opentelemetry/util/http/version.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/src/000077500000000000000000000000001527070072300261355ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/src/opentelemetry/000077500000000000000000000000001527070072300310315ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/src/opentelemetry/util/000077500000000000000000000000001527070072300320065ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/src/opentelemetry/util/http/000077500000000000000000000000001527070072300327655ustar00rootroot00000000000000__init__.py000066400000000000000000000272331527070072300350260ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/src/opentelemetry/util/http# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Callable, Iterable, Mapping from os import environ from re import IGNORECASE as RE_IGNORECASE from re import compile as re_compile from re import search from typing import overload from urllib.parse import parse_qs, urlencode, urlparse, urlunparse from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_FLAVOR, HTTP_HOST, HTTP_METHOD, HTTP_SCHEME, HTTP_SERVER_NAME, HTTP_STATUS_CODE, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_HOST_NAME, NET_HOST_PORT, ) from opentelemetry.semconv._incubating.attributes.user_agent_attributes import ( UserAgentSyntheticTypeValues, ) from opentelemetry.util.http.constants import BOT_PATTERNS, TEST_PATTERNS OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS = "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS" OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST = "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST" OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE = "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE" OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST = "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST" OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE = "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE" OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS = "OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS" # List of recommended metrics attributes _duration_attrs = { HTTP_METHOD, HTTP_HOST, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_FLAVOR, HTTP_SERVER_NAME, NET_HOST_NAME, NET_HOST_PORT, } _active_requests_count_attrs = { HTTP_METHOD, HTTP_HOST, HTTP_SCHEME, HTTP_FLAVOR, HTTP_SERVER_NAME, } PARAMS_TO_REDACT = ["AWSAccessKeyId", "Signature", "sig", "X-Goog-Signature"] class ExcludeList: """Class to exclude certain paths (given as a list of regexes) from tracing requests""" def __init__(self, excluded_urls: Iterable[str]): self._excluded_urls = excluded_urls if self._excluded_urls: self._regex = re_compile("|".join(excluded_urls)) def url_disabled(self, url: str) -> bool: return bool(self._excluded_urls and search(self._regex, url)) class SanitizeValue: """Class to sanitize (remove sensitive data from) certain headers (given as a list of regexes)""" def __init__(self, sanitized_fields: Iterable[str]): self._sanitized_fields = sanitized_fields if self._sanitized_fields: self._regex = re_compile("|".join(sanitized_fields), RE_IGNORECASE) def sanitize_header_value(self, header: str, value: str) -> str: return "[REDACTED]" if (self._sanitized_fields and search(self._regex, header)) else value def sanitize_header_values( self, headers: Mapping[str, str | list[str]], header_regexes: list[str], normalize_function: Callable[[str], str], ) -> dict[str, list[str]]: values: dict[str, list[str]] = {} if header_regexes: header_regexes_compiled = re_compile( "|".join(header_regexes), RE_IGNORECASE, ) for header_name, header_value in headers.items(): if header_regexes_compiled.fullmatch(header_name): key = normalize_function(header_name.lower()) if isinstance(header_value, str): values[key] = [self.sanitize_header_value(header_name, header_value)] else: values[key] = [self.sanitize_header_value(header_name, value) for value in header_value] return values _root = r"OTEL_PYTHON_{}" def get_traced_request_attrs(instrumentation: str) -> list[str]: traced_request_attrs = environ.get(_root.format(f"{instrumentation}_TRACED_REQUEST_ATTRS")) if traced_request_attrs: return [traced_request_attr.strip() for traced_request_attr in traced_request_attrs.split(",")] return [] def get_excluded_urls(instrumentation: str) -> ExcludeList: # Get instrumentation-specific excluded URLs. If not set, retrieve them # from generic variable. excluded_urls = environ.get( _root.format(f"{instrumentation}_EXCLUDED_URLS"), environ.get(_root.format("EXCLUDED_URLS"), ""), ) return parse_excluded_urls(excluded_urls) def parse_excluded_urls(excluded_urls: str) -> ExcludeList: """ Small helper to put an arbitrary url list inside an ExcludeList """ if excluded_urls: excluded_url_list = [excluded_url.strip() for excluded_url in excluded_urls.split(",")] else: excluded_url_list = [] return ExcludeList(excluded_url_list) def remove_url_credentials(url: str) -> str: """Given a string url, replace the username and password with the keyword `REDACTED` only if it is a valid url""" try: parsed = urlparse(url) if all([parsed.scheme, parsed.netloc]): # checks for valid url if "@" in parsed.netloc: _, _, host = parsed.netloc.rpartition("@") new_netloc = "REDACTED:REDACTED@" + host return urlunparse( ( parsed.scheme, new_netloc, parsed.path, parsed.params, parsed.query, parsed.fragment, ) ) except ValueError: # an unparsable url was passed pass return url def normalise_request_header_name(header: str) -> str: key = header.lower().replace("-", "_") return f"http.request.header.{key}" def normalise_response_header_name(header: str) -> str: key = header.lower().replace("-", "_") return f"http.response.header.{key}" @overload def sanitize_method(method: str) -> str: ... @overload def sanitize_method(method: None) -> None: ... def sanitize_method(method: str | None) -> str | None: if method is None: return None method = method.upper() if ( environ.get(OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS) or # Based on https://www.rfc-editor.org/rfc/rfc9110.html#name-methods, https://www.rfc-editor.org/rfc/rfc5789#section-2 # and https://datatracker.ietf.org/doc/rfc10008/. method in [ "GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH", "QUERY", ] ): return method return "_OTHER" def get_custom_headers(env_var: str) -> list[str]: custom_headers = environ.get(env_var, None) if custom_headers: return [custom_headers.strip() for custom_headers in custom_headers.split(",")] return [] def get_custom_header_attributes( headers: Mapping[str, str | list[str]] | None, captured_headers: list[str] | None, sensitive_headers: list[str] | None, normalize_function: Callable[[str], str], ) -> dict[str, list[str]]: """Extract and sanitize HTTP headers for span attributes. Args: headers: The HTTP headers to process, either from a request or response. Can be None if no headers are available. captured_headers: List of header regexes to capture as span attributes. If None or empty, no headers will be captured. sensitive_headers: List of header regexes whose values should be sanitized (redacted). If None, no sanitization is applied. normalize_function: Function to normalize header names. Returns: Dictionary of normalized header attribute names to their values as lists of strings. """ if not headers or not captured_headers: return {} sanitize: SanitizeValue = SanitizeValue(sensitive_headers or ()) return sanitize.sanitize_header_values(headers, captured_headers, normalize_function) def _parse_active_request_count_attrs(req_attrs): active_requests_count_attrs = { key: req_attrs[key] for key in _active_requests_count_attrs.intersection(req_attrs.keys()) } return active_requests_count_attrs def _parse_duration_attrs(req_attrs): duration_attrs = {key: req_attrs[key] for key in _duration_attrs.intersection(req_attrs.keys())} return duration_attrs def _parse_url_query(url: str) -> tuple[str, str]: """Split a url into its path and query components. Callers may pass request targets that come straight off the request line, so the url need not be parsable. An unparsable one yields empty components rather than raising, since instrumentation must never break the request it is measuring. Returns: The path and the query string, either of which may be empty. """ try: parsed_url = urlparse(url) except ValueError: # an unparsable url was passed return "", "" path = parsed_url.path query_params = parsed_url.query return path, query_params def redact_query_parameters(url: str) -> str: """Given a string url, redact sensitive query parameter values""" try: parsed = urlparse(url) if not parsed.query: # No query parameters to redact return url query_params = parse_qs(parsed.query) if not any(param in query_params for param in PARAMS_TO_REDACT): return url for param in PARAMS_TO_REDACT: if param in query_params: query_params[param] = ["REDACTED"] return urlunparse( ( parsed.scheme, parsed.netloc, parsed.path, parsed.params, urlencode(query_params, doseq=True), parsed.fragment, ) ) except ValueError: # an unparsable url was passed return url def redact_url(url: str) -> str: """Redact sensitive data from the URL, including credentials and query parameters.""" url = remove_url_credentials(url) url = redact_query_parameters(url) return url def normalize_user_agent( user_agent: str | bytes | bytearray | memoryview | None, ) -> str | None: """Convert user-agent header values into a usable string.""" # Different servers/frameworks surface headers as str, bytes, bytearray or memoryview; # keep decoding logic centralized so instrumentation modules just call this helper. if user_agent is None: return None if isinstance(user_agent, str): return user_agent if isinstance(user_agent, (bytes, bytearray)): return user_agent.decode("latin-1") if isinstance(user_agent, memoryview): return user_agent.tobytes().decode("latin-1") return str(user_agent) def detect_synthetic_user_agent(user_agent: str | None) -> str | None: """ Detect synthetic user agent type based on user agent string contents. Args: user_agent: The user agent string to analyze Returns: UserAgentSyntheticTypeValues.TEST if user agent contains any pattern from TEST_PATTERNS UserAgentSyntheticTypeValues.BOT if user agent contains any pattern from BOT_PATTERNS None otherwise Note: Test patterns take priority over bot patterns. """ if not user_agent: return None user_agent_lower = user_agent.lower() if any(test_pattern in user_agent_lower for test_pattern in TEST_PATTERNS): return UserAgentSyntheticTypeValues.TEST.value if any(bot_pattern in user_agent_lower for bot_pattern in BOT_PATTERNS): return UserAgentSyntheticTypeValues.BOT.value return None constants.py000066400000000000000000000011471527070072300352770ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/src/opentelemetry/util/http# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Constants for OpenTelemetry HTTP utilities. This module contains configuration constants and pattern definitions used by HTTP instrumentation utilities for various features like synthetic user agent detection. """ # Test patterns to detect in user agent strings (case-insensitive) # These patterns indicate synthetic test traffic TEST_PATTERNS = [ "alwayson", ] # Bot patterns to detect in user agent strings (case-insensitive) # These patterns indicate automated bot traffic BOT_PATTERNS = [ "googlebot", "bingbot", ] httplib.py000066400000000000000000000117721527070072300347360ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/src/opentelemetry/util/http# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This library provides functionality to enrich HTTP client spans with IPs. It does not create spans on its own. """ from __future__ import annotations import contextlib import http.client import logging import socket # pylint:disable=unused-import # Used for typing import typing from collections.abc import Callable, Collection from typing import Any, TypedDict, cast import wrapt from opentelemetry import context from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_PEER_IP, ) from opentelemetry.trace.span import Span _STATE_KEY = "httpbase_instrumentation_state" logger = logging.getLogger(__name__) R = typing.TypeVar("R") class HttpClientInstrumentor(BaseInstrumentor): def instrumentation_dependencies(self) -> Collection[str]: return () # This instruments http.client from stdlib; no extra deps. def _instrument(self, **kwargs: Any): """Instruments the http.client module (not creating spans on its own)""" _instrument() def _uninstrument(self, **kwargs: Any): _uninstrument() def _remove_nonrecording(spanlist: list[Span]) -> bool: idx = len(spanlist) - 1 while idx >= 0: if not spanlist[idx].is_recording(): logger.debug("Span is not recording: %s", spanlist[idx]) islast = idx + 1 == len(spanlist) if not islast: spanlist[idx] = spanlist[len(spanlist) - 1] spanlist.pop() if islast: if idx == 0: return False # We removed everything idx -= 1 else: idx -= 1 return True def trysetip(conn: http.client.HTTPConnection, loglevel: int = logging.DEBUG) -> bool: """Tries to set the net.peer.ip semantic attribute on the current span from the given HttpConnection. Returns False if the connection is not yet established, False if the IP was captured or there is no need to capture it. """ state = _getstate() if not state: return True spanlist: list[Span] = state.get("need_ip") if not spanlist: return True # Remove all non-recording spans from the list. if not _remove_nonrecording(spanlist): return True sock = "" ip = None try: sock: socket.socket | None = conn.sock logger.debug("Got socket: %s", sock) if sock is None: return False addr = sock.getpeername() if addr and addr[0]: ip = addr[0] except Exception: # pylint:disable=broad-except logger.log( loglevel, "Failed to get peer address from %s", sock, exc_info=True, stack_info=True, ) else: if ip is not None: for span in spanlist: span.set_attribute(NET_PEER_IP, ip) return True def _instrumented_connect( wrapped: Callable[..., R], instance: http.client.HTTPConnection, args: tuple[Any, ...], kwargs: dict[str, Any], ) -> R: result = wrapped(*args, **kwargs) trysetip(instance, loglevel=logging.WARNING) return result def instrument_connect(module: type[Any], name: str = "connect"): """Instrument additional connect() methods, e.g. for derived classes.""" wrapt.wrap_function_wrapper( module, name, _instrumented_connect, ) def _instrument(): def instrumented_send( wrapped: Callable[..., R], instance: http.client.HTTPConnection, args: tuple[Any, ...], kwargs: dict[str, Any], ) -> R: done = trysetip(instance) result = wrapped(*args, **kwargs) if not done: trysetip(instance, loglevel=logging.WARNING) return result wrapt.wrap_function_wrapper( http.client.HTTPConnection, "send", instrumented_send, ) instrument_connect(http.client.HTTPConnection) # No need to instrument HTTPSConnection, as it calls super().connect() class _ConnectionState(TypedDict): need_ip: list[Span] def _getstate() -> _ConnectionState | None: return cast(_ConnectionState, context.get_value(_STATE_KEY)) @contextlib.contextmanager def set_ip_on_next_http_connection(span: Span): state = _getstate() if not state: token = context.attach(context.set_value(_STATE_KEY, {"need_ip": [span]})) try: yield finally: if token: context.detach(token) else: spans = state["need_ip"] spans.append(span) try: yield finally: try: spans.remove(span) except ValueError: # Span might have become non-recording pass def _uninstrument(): unwrap(http.client.HTTPConnection, "send") unwrap(http.client.HTTPConnection, "connect") py.typed000066400000000000000000000000001527070072300343730ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/src/opentelemetry/util/httpversion.py000066400000000000000000000001441527070072300347440ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/src/opentelemetry/util/http# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/test-requirements.txt000066400000000000000000000002711527070072300316070ustar00rootroot00000000000000asgiref==3.8.1 Deprecated==1.2.14 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 tomli==2.0.1 typing_extensions==4.12.2 -e opentelemetry-instrumentation opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/tests/000077500000000000000000000000001527070072300265105ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/tests/__init__.py000066400000000000000000000000001527070072300306070ustar00rootroot00000000000000test_capture_custom_headers.py000066400000000000000000000054761527070072300346060ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from unittest.mock import patch from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, SanitizeValue, get_custom_headers, normalise_request_header_name, normalise_response_header_name, ) class TestCaptureCustomHeaders(unittest.TestCase): @patch.dict( "os.environ", {OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "User-Agent,Test-Header"}, ) def test_get_custom_request_header(self): custom_headers_to_capture = get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST) self.assertEqual(custom_headers_to_capture, ["User-Agent", "Test-Header"]) @patch.dict( "os.environ", {OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,test-header"}, ) def test_get_custom_response_header(self): custom_headers_to_capture = get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE) self.assertEqual( custom_headers_to_capture, [ "content-type", "content-length", "test-header", ], ) @patch.dict( "os.environ", {OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: "My-Secret-Header,My-Secret-Header-2"}, ) def test_get_custom_sanitize_header(self): sanitized_fields = get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS) self.assertEqual( sanitized_fields, ["My-Secret-Header", "My-Secret-Header-2"], ) @patch.dict( "os.environ", {OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: "My-Secret-Header,My-Secret-Header-2"}, ) def test_sanitize(self): sanitized_fields = get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS) sanitize = SanitizeValue(sanitized_fields) self.assertEqual( sanitize.sanitize_header_value(header="My-Secret-Header", value="My-Secret-Value"), "[REDACTED]", ) self.assertEqual( sanitize.sanitize_header_value(header="My-Not-Secret-Header", value="My-Not-Secret-Value"), "My-Not-Secret-Value", ) def test_normalise_request_header_name(self): key = normalise_request_header_name("Test-Header") self.assertEqual(key, "http.request.header.test_header") def test_normalise_response_header_name(self): key = normalise_response_header_name("Test-Header") self.assertEqual(key, "http.response.header.test_header") test_detect_synthetic_user_agent.py000066400000000000000000000110661527070072300356240ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from opentelemetry.semconv._incubating.attributes.user_agent_attributes import ( UserAgentSyntheticTypeValues, ) from opentelemetry.util.http import ( detect_synthetic_user_agent, normalize_user_agent, ) class TestDetectSyntheticUserAgent(unittest.TestCase): def test_detect_bot_googlebot(self): """Test detection of googlebot user agent.""" user_agent = "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" result = detect_synthetic_user_agent(user_agent) self.assertEqual(result, UserAgentSyntheticTypeValues.BOT.value) def test_detect_bot_bingbot(self): """Test detection of bingbot user agent.""" user_agent = "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)" result = detect_synthetic_user_agent(user_agent) self.assertEqual(result, UserAgentSyntheticTypeValues.BOT.value) def test_detect_test_alwayson(self): """Test detection of alwayson test user agent.""" user_agent = "AlwaysOn-Monitor/1.0" result = detect_synthetic_user_agent(user_agent) self.assertEqual(result, UserAgentSyntheticTypeValues.TEST.value) def test_case_insensitive_detection(self): """Test that detection is case insensitive.""" # Test uppercase patterns user_agent_bot = "GOOGLEBOT/2.1" result = detect_synthetic_user_agent(user_agent_bot) self.assertEqual(result, UserAgentSyntheticTypeValues.BOT.value) user_agent_test = "ALWAYSON-Monitor/1.0" result = detect_synthetic_user_agent(user_agent_test) self.assertEqual(result, UserAgentSyntheticTypeValues.TEST.value) def test_normal_user_agent_not_detected(self): """Test that normal browser user agents are not detected as synthetic.""" user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" result = detect_synthetic_user_agent(user_agent) self.assertIsNone(result) def test_none_user_agent(self): """Test that None user agent returns None.""" result = detect_synthetic_user_agent(None) self.assertIsNone(result) def test_empty_user_agent(self): """Test that empty user agent returns None.""" result = detect_synthetic_user_agent("") self.assertIsNone(result) def test_substring_match(self): """Test that substrings are detected correctly.""" # Test googlebot in middle of string user_agent = "MyApp/1.0 googlebot crawler" result = detect_synthetic_user_agent(user_agent) self.assertEqual(result, UserAgentSyntheticTypeValues.BOT.value) # Test alwayson in middle of string user_agent = "TestFramework/1.0 alwayson monitoring" result = detect_synthetic_user_agent(user_agent) self.assertEqual(result, UserAgentSyntheticTypeValues.TEST.value) def test_priority_test_over_bot(self): """Test that test patterns take priority over bot patterns.""" user_agent = "alwayson-googlebot/1.0" result = detect_synthetic_user_agent(user_agent) # alwayson should be checked first and return 'test' self.assertEqual(result, UserAgentSyntheticTypeValues.TEST.value) def test_bytes_like_user_agent(self): """Test that bytes-like user agents are decoded and detected.""" test_cases = [ (b"alwayson-monitor/1.0", UserAgentSyntheticTypeValues.TEST.value), ( bytearray(b"googlebot/2.1"), UserAgentSyntheticTypeValues.BOT.value, ), (memoryview(b"MyApp/1.0"), None), ] for user_agent_raw, expected in test_cases: with self.subTest(user_agent=user_agent_raw): normalized = normalize_user_agent(user_agent_raw) result = detect_synthetic_user_agent(normalized) self.assertEqual(result, expected) class TestNormalizeUserAgent(unittest.TestCase): def test_preserves_string(self): self.assertEqual(normalize_user_agent("Mozilla"), "Mozilla") def test_decodes_bytes(self): self.assertEqual(normalize_user_agent(b"Custom-Client/1.0"), "Custom-Client/1.0") def test_decodes_bytearray(self): self.assertEqual(normalize_user_agent(bytearray(b"Bot/2.0")), "Bot/2.0") def test_decodes_memoryview(self): self.assertEqual(normalize_user_agent(memoryview(b"Monitor/3.0")), "Monitor/3.0") def test_none(self): self.assertIsNone(normalize_user_agent(None)) opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/tests/test_http_base.py000066400000000000000000000106301527070072300320720ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from http.client import HTTPConnection, HTTPResponse, HTTPSConnection from opentelemetry import trace from opentelemetry.test.httptest import HttpTestBase from opentelemetry.test.test_base import TestBase from opentelemetry.trace.span import INVALID_SPAN from opentelemetry.util.http.httplib import ( HttpClientInstrumentor, set_ip_on_next_http_connection, ) # pylint: disable=too-many-public-methods class TestHttpBase(TestBase, HttpTestBase): def setUp(self): super().setUp() HttpClientInstrumentor().instrument() self.server_thread, self.server = self.run_server() def tearDown(self): HttpClientInstrumentor().uninstrument() self.server.shutdown() self.server_thread.join() super().tearDown() def assert_span(self, exporter=None, num_spans=1): if exporter is None: exporter = self.memory_exporter span_list = exporter.get_finished_spans() self.assertEqual(num_spans, len(span_list)) if num_spans == 0: return None if num_spans == 1: return span_list[0] return span_list def test_basic(self): resp, body = self.perform_request() assert resp.status == 200 assert body == b"Hello!" self.assert_span(num_spans=0) def test_basic_with_span(self): tracer = trace.get_tracer(__name__) with ( tracer.start_as_current_span("HTTP GET") as span, set_ip_on_next_http_connection(span), ): resp, body = self.perform_request() assert resp.status == 200 assert body == b"Hello!" span = self.assert_span(num_spans=1) self.assertEqual(span.attributes, {"net.peer.ip": "127.0.0.1"}) def test_with_nested_span(self): tracer = trace.get_tracer(__name__) with ( tracer.start_as_current_span("requests HTTP GET") as span, set_ip_on_next_http_connection(span), ): with ( tracer.start_as_current_span("urllib3 HTTP GET") as span2, set_ip_on_next_http_connection(span2), ): resp, body = self.perform_request() assert resp.status == 200 assert body == b"Hello!" for span in self.assert_span(num_spans=2): self.assertEqual(span.attributes, {"net.peer.ip": "127.0.0.1"}) def test_with_nested_nonrecording_span(self): tracer = trace.get_tracer(__name__) with ( tracer.start_as_current_span("requests HTTP GET") as span, set_ip_on_next_http_connection(span), ): with ( trace.use_span(INVALID_SPAN), set_ip_on_next_http_connection(INVALID_SPAN), ): resp, body = self.perform_request() assert resp.status == 200 assert body == b"Hello!" span = self.assert_span(num_spans=1) self.assertEqual(span.attributes, {"net.peer.ip": "127.0.0.1"}) def test_with_only_nonrecording_span(self): with ( trace.use_span(INVALID_SPAN), set_ip_on_next_http_connection(INVALID_SPAN), ): resp, body = self.perform_request() assert resp.status == 200 assert body == b"Hello!" self.assert_span(num_spans=0) def perform_request(self, secure=False) -> HTTPResponse: conn_cls = HTTPSConnection if secure else HTTPConnection conn = conn_cls(self.server.server_address[0], self.server.server_port) resp = None try: conn.request("GET", "/", headers={"Connection": "close"}) resp = conn.getresponse() return resp, resp.read() finally: if resp: resp.close() conn.close() def test_uninstrument(self): HttpClientInstrumentor().uninstrument() tracer = trace.get_tracer(__name__) with ( tracer.start_as_current_span("HTTP GET") as span, set_ip_on_next_http_connection(span), ): body = self.perform_request()[1] self.assertEqual(b"Hello!", body) # We should have a span, but it should have no attributes self.assertFalse(self.assert_span(num_spans=1).attributes) # instrument again to avoid warning message on tearDown HttpClientInstrumentor().instrument() opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/tests/test_http_excluded_urls.py000066400000000000000000000041561527070072300340300ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from unittest.mock import patch from opentelemetry.util.http import get_excluded_urls class TestGetExcludedUrls(unittest.TestCase): @patch.dict( "os.environ", {"OTEL_PYTHON_DJANGO_EXCLUDED_URLS": "excluded_arg/123,excluded_noarg"}, ) def test_config_from_instrumentation_env(self): exclude_list = get_excluded_urls("DJANGO") self.assertTrue(exclude_list.url_disabled("/excluded_arg/123")) self.assertTrue(exclude_list.url_disabled("/excluded_noarg")) self.assertFalse(exclude_list.url_disabled("/excluded_arg/125")) @patch.dict( "os.environ", {"OTEL_PYTHON_EXCLUDED_URLS": "excluded_arg/123,excluded_noarg"}, ) def test_config_from_generic_env(self): exclude_list = get_excluded_urls("DJANGO") self.assertTrue(exclude_list.url_disabled("/excluded_arg/123")) self.assertTrue(exclude_list.url_disabled("/excluded_noarg")) self.assertFalse(exclude_list.url_disabled("/excluded_arg/125")) @patch.dict( "os.environ", { "OTEL_PYTHON_DJANGO_EXCLUDED_URLS": "excluded_arg/123,excluded_noarg", "OTEL_PYTHON_EXCLUDED_URLS": "excluded_arg/125", }, ) def test_config_from_instrumentation_env_takes_precedence(self): exclude_list = get_excluded_urls("DJANGO") self.assertTrue(exclude_list.url_disabled("/excluded_arg/123")) self.assertTrue(exclude_list.url_disabled("/excluded_noarg")) self.assertFalse(exclude_list.url_disabled("/excluded_arg/125")) @patch.dict( "os.environ", { "OTEL_PYTHON_DJANGO_EXCLUDED_URLS": "", "OTEL_PYTHON_EXCLUDED_URLS": "excluded_arg/125", }, ) def test_config_from_instrumentation_env_empty(self): exclude_list = get_excluded_urls("DJANGO") self.assertFalse(exclude_list.url_disabled("/excluded_arg/123")) self.assertFalse(exclude_list.url_disabled("/excluded_noarg")) self.assertFalse(exclude_list.url_disabled("/excluded_arg/125")) opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/tests/test_parse_url_query.py000066400000000000000000000024551527070072300333500ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from opentelemetry.util.http import _parse_url_query class TestParseUrlQuery(unittest.TestCase): def test_absolute_url(self): self.assertEqual( _parse_url_query("https://example.com/path?color=blue"), ("/path", "color=blue"), ) def test_request_target(self): self.assertEqual(_parse_url_query("/path?color=blue"), ("/path", "color=blue")) def test_no_query(self): self.assertEqual(_parse_url_query("/path"), ("/path", "")) def test_empty_url(self): self.assertEqual(_parse_url_query(""), ("", "")) def test_unparsable_url_returns_empty_components(self): # urlparse raises "Invalid IPv6 URL" on an unbalanced bracket in what # it takes to be the netloc. Request targets are attacker controlled, # so this must not propagate out of instrumentation. self.assertEqual(_parse_url_query("//exa[mple"), ("", "")) def test_unparsable_url_with_query_returns_empty_components(self): self.assertEqual(_parse_url_query("//exa[mple?color=blue"), ("", "")) def test_unparsable_absolute_url_returns_empty_components(self): self.assertEqual(_parse_url_query("http://example.com[invalid"), ("", "")) test_redact_query_parameters.py000066400000000000000000000056401527070072300347610ustar00rootroot00000000000000opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from opentelemetry.util.http import redact_query_parameters class TestRedactSensitiveInfo(unittest.TestCase): def test_redact_goog_signature(self): url = "https://www.example.com/path?color=blue&X-Goog-Signature=secret" self.assertEqual( redact_query_parameters(url), "https://www.example.com/path?color=blue&X-Goog-Signature=REDACTED", ) def test_no_redaction_needed(self): url = "https://www.example.com/path?color=blue&query=secret" self.assertEqual( redact_query_parameters(url), "https://www.example.com/path?color=blue&query=secret", ) def test_no_query_parameters(self): url = "https://www.example.com/path" self.assertEqual(redact_query_parameters(url), "https://www.example.com/path") def test_empty_query_string(self): url = "https://www.example.com/path?" self.assertEqual(redact_query_parameters(url), "https://www.example.com/path?") def test_empty_url(self): url = "" self.assertEqual(redact_query_parameters(url), "") def test_redact_aws_access_key_id(self): url = "https://www.example.com/path?color=blue&AWSAccessKeyId=secrets" self.assertEqual( redact_query_parameters(url), "https://www.example.com/path?color=blue&AWSAccessKeyId=REDACTED", ) def test_api_key_not_in_redact_list(self): url = "https://www.example.com/path?api_key=secret%20key&user=john" self.assertNotEqual( redact_query_parameters(url), "https://www.example.com/path?api_key=REDACTED&user=john", ) def test_password_key_not_in_redact_list(self): url = "https://api.example.com?key=abc&password=123&user=admin" self.assertNotEqual( redact_query_parameters(url), "https://api.example.com?key=REDACTED&password=REDACTED&user=admin", ) def test_url_with_at_symbol_in_path_and_query(self): url = "https://example.com/p@th?foo=b@r" self.assertEqual(redact_query_parameters(url), "https://example.com/p@th?foo=b@r") def test_aws_access_key_with_real_format(self): url = "https://mock.com?AWSAccessKeyId=AKIAIOSFODNN7" self.assertEqual( redact_query_parameters(url), "https://mock.com?AWSAccessKeyId=REDACTED", ) def test_signature_parameter(self): url = "https://service.com?sig=39Up9jzHkxhuIhFE9594DJxe7w6cIRCg0V6ICGS0" self.assertEqual(redact_query_parameters(url), "https://service.com?sig=REDACTED") def test_signature_with_url_encoding(self): url = "https://service.com?Signature=39Up9jzHkxhuIhFE9594DJxe7w6cIRCg0V6ICGS0%3A377" self.assertEqual( redact_query_parameters(url), "https://service.com?Signature=REDACTED", ) opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/tests/test_redact_url.py000066400000000000000000000043711527070072300322520ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from opentelemetry.util.http import redact_url class TestRedactUrl(unittest.TestCase): def test_redact_both_credentials_and_query_params(self): """Test URL with both credentials and sensitive query parameters.""" url = "https://user:password@api.example.com/data?AWSAccessKeyId=AKIAIOSFODNN7&color=blue" expected = "https://REDACTED:REDACTED@api.example.com/data?AWSAccessKeyId=REDACTED&color=blue" self.assertEqual(redact_url(url), expected) def test_multiple_sensitive_query_params(self): """Test URL with multiple sensitive query parameters.""" url = "https://admin:1234@example.com/secure?Signature=abc123&X-Goog-Signature=xyz789&sig=def456" expected = ( "https://REDACTED:REDACTED@example.com/secure?Signature=REDACTED&X-Goog-Signature=REDACTED&sig=REDACTED" ) self.assertEqual(redact_url(url), expected) def test_url_with_special_characters(self): """Test URL with special characters in both credentials and query parameters.""" url = "https://user@domain:p@ss!word@api.example.com/path?Signature=s%40me+special%20chars&normal=fine" expected = "https://REDACTED:REDACTED@api.example.com/path?Signature=REDACTED&normal=fine" self.assertEqual(redact_url(url), expected) def test_edge_cases(self): """Test unusual URL formats and corner cases.""" # URL with fragment url1 = "https://user:pass@api.example.com/data?Signature=secret#section" self.assertEqual( redact_url(url1), "https://REDACTED:REDACTED@api.example.com/data?Signature=REDACTED#section", ) # URL with port number url2 = "https://user:pass@api.example.com:8443/data?AWSAccessKeyId=secret" self.assertEqual( redact_url(url2), "https://REDACTED:REDACTED@api.example.com:8443/data?AWSAccessKeyId=REDACTED", ) # URL with IP address instead of domain url3 = "https://user:pass@192.168.1.1/path?X-Goog-Signature=xyz" self.assertEqual( redact_url(url3), "https://REDACTED:REDACTED@192.168.1.1/path?X-Goog-Signature=REDACTED", ) opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/tests/test_remove_credentials.py000066400000000000000000000024701527070072300337760ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from opentelemetry.util.http import remove_url_credentials class TestRemoveUrlCredentials(unittest.TestCase): def test_remove_no_credentials(self): url = "http://mock/status/200/test/path?query=value" cleaned_url = remove_url_credentials(url) self.assertEqual(cleaned_url, url) def test_remove_credentials(self): url = "http://someuser:somepass@mock/status/200/test/path?sig=value" cleaned_url = remove_url_credentials(url) self.assertEqual( cleaned_url, "http://REDACTED:REDACTED@mock/status/200/test/path?sig=value", ) def test_remove_credentials_ipv4_literal(self): url = "http://someuser:somepass@127.0.0.1:8080/test/path?query=value" cleaned_url = remove_url_credentials(url) self.assertEqual( cleaned_url, "http://REDACTED:REDACTED@127.0.0.1:8080/test/path?query=value", ) def test_remove_credentials_ipv6_literal(self): url = "http://someuser:somepass@[::1]:8080/test/path?query=value" cleaned_url = remove_url_credentials(url) self.assertEqual( cleaned_url, "http://REDACTED:REDACTED@[::1]:8080/test/path?query=value", ) opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/tests/test_sanitize_method.py000066400000000000000000000025361527070072300333150ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from unittest.mock import patch from opentelemetry.util.http import ( OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS, sanitize_method, ) class TestSanitizeMethod(unittest.TestCase): def test_standard_method_uppercase(self): method = sanitize_method("GET") self.assertEqual(method, "GET") def test_standard_method_lowercase(self): method = sanitize_method("get") self.assertEqual(method, "GET") def test_nonstandard_method(self): method = sanitize_method("UNKNOWN") self.assertEqual(method, "_OTHER") def test_known_methods(self): known_methods = [ "GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH", "QUERY", ] for method in known_methods: with self.subTest(method=method): self.assertEqual(method, sanitize_method(method)) @patch.dict( "os.environ", { OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS: "1", }, ) def test_nonstandard_method_allowed(self): method = sanitize_method("UNKNOWN") self.assertEqual(method, "UNKNOWN") opentelemetry-python-contrib-0.66b2/util/opentelemetry-util-http/tests/test_try_set_ip.py000066400000000000000000000033711527070072300323060ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import socket import unittest from http.client import HTTPConnection from unittest.mock import MagicMock, patch from opentelemetry.util.http.httplib import trysetip class TestTrySetIP(unittest.TestCase): def setUp(self): # Setup a mock HTTPConnection self.conn = MagicMock(spec=HTTPConnection) self.conn.sock = MagicMock(spec=socket.socket) # Mock state function and Span class self.mock_state = {"need_ip": [MagicMock()]} self.mock_getstate = patch( "opentelemetry.util.http.httplib._getstate", return_value=self.mock_state, ) self.mock_getstate.start() def test_ip_set_successfully(self): self.conn.sock.getpeername.return_value = ("192.168.1.1", 8080) success = trysetip(self.conn, loglevel=logging.DEBUG) # Verify that the IP was set correctly for span in self.mock_state["need_ip"]: span.set_attribute.assert_called_once_with("net.peer.ip", "192.168.1.1") self.assertTrue(success) def test_no_socket_connection(self): # Setup the connection with no socket self.conn.sock = None success = trysetip(self.conn, loglevel=logging.DEBUG) self.assertFalse(success) def test_exception_during_ip_retrieval(self): self.conn.sock.getpeername.side_effect = Exception("Test Exception") with self.assertLogs(level=logging.WARNING) as warning: success = trysetip(self.conn, loglevel=logging.WARNING) self.assertEqual(len(warning.records), 1) self.assertIn("Failed to get peer address", warning.records[0].message) self.assertTrue(success) opentelemetry-python-contrib-0.66b2/uv.lock000066400000000000000000035454331527070072300210720ustar00rootroot00000000000000version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", "python_full_version < '3.11'", ] [manifest] members = [ "opentelemetry-distro", "opentelemetry-exporter-credential-provider-gcp", "opentelemetry-exporter-prometheus-remote-write", "opentelemetry-exporter-richconsole", "opentelemetry-instrumentation", "opentelemetry-instrumentation-aio-pika", "opentelemetry-instrumentation-aiohttp-client", "opentelemetry-instrumentation-aiohttp-server", "opentelemetry-instrumentation-aiokafka", "opentelemetry-instrumentation-aiopg", "opentelemetry-instrumentation-asgi", "opentelemetry-instrumentation-asyncclick", "opentelemetry-instrumentation-asyncio", "opentelemetry-instrumentation-asyncpg", "opentelemetry-instrumentation-aws-lambda", "opentelemetry-instrumentation-boto3sqs", "opentelemetry-instrumentation-botocore", "opentelemetry-instrumentation-cassandra", "opentelemetry-instrumentation-celery", "opentelemetry-instrumentation-click", "opentelemetry-instrumentation-confluent-kafka", "opentelemetry-instrumentation-dbapi", "opentelemetry-instrumentation-django", "opentelemetry-instrumentation-exceptions", "opentelemetry-instrumentation-falcon", "opentelemetry-instrumentation-fastapi", "opentelemetry-instrumentation-flask", "opentelemetry-instrumentation-grpc", "opentelemetry-instrumentation-httpx", "opentelemetry-instrumentation-jinja2", "opentelemetry-instrumentation-kafka-python", "opentelemetry-instrumentation-logging", "opentelemetry-instrumentation-mysql", "opentelemetry-instrumentation-mysqlclient", "opentelemetry-instrumentation-openai-agents-v2", "opentelemetry-instrumentation-openai-v2", "opentelemetry-instrumentation-pika", "opentelemetry-instrumentation-psycopg", "opentelemetry-instrumentation-psycopg2", "opentelemetry-instrumentation-pymemcache", "opentelemetry-instrumentation-pymongo", "opentelemetry-instrumentation-pymssql", "opentelemetry-instrumentation-pymysql", "opentelemetry-instrumentation-pyramid", "opentelemetry-instrumentation-redis", "opentelemetry-instrumentation-remoulade", "opentelemetry-instrumentation-requests", "opentelemetry-instrumentation-sqlalchemy", "opentelemetry-instrumentation-sqlite3", "opentelemetry-instrumentation-starlette", "opentelemetry-instrumentation-structlog", "opentelemetry-instrumentation-system-metrics", "opentelemetry-instrumentation-threading", "opentelemetry-instrumentation-tornado", "opentelemetry-instrumentation-tortoiseorm", "opentelemetry-instrumentation-urllib", "opentelemetry-instrumentation-urllib3", "opentelemetry-instrumentation-valkey-py", "opentelemetry-instrumentation-vertexai", "opentelemetry-instrumentation-wsgi", "opentelemetry-propagator-aws-xray", "opentelemetry-propagator-ot-trace", "opentelemetry-python-contrib", "opentelemetry-util-genai", "opentelemetry-util-http", ] [[package]] name = "aio-pika" version = "9.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiormq" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "yarl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/63/56354526f2e6e915c93bee6e4dedb35888fe82d6bc1a19f35f5a77e795ff/aio_pika-9.6.2.tar.gz", hash = "sha256:c49e9246080dc8ffa1bb0e4aca407bf3d8ad78c3ee3a93df88b68fe65d7a49b9", size = 70851, upload-time = "2026-03-22T19:03:20.878Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/25/05/256fa313f48bed075056d13593b92ce804be05d75f4f312be24edb82860a/aio_pika-9.6.2-py3-none-any.whl", hash = "sha256:2a5478af920d169795071c9c09c7542cd8cdece60438cf7804533dcbcce93b7f", size = 56269, upload-time = "2026-03-22T19:03:19.558Z" }, ] [[package]] name = "aiobotocore" version = "3.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, { name = "aioitertools" }, { name = "botocore" }, { name = "jmespath" }, { name = "multidict" }, { name = "python-dateutil" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "wrapt" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e7/75/42cce839c2ec263ff74b10b650fe36b066fbb124cbee6f247eac0983e1ab/aiobotocore-3.7.0.tar.gz", hash = "sha256:c64d871ed5491a6571948dd48eabd185b46c6c23b64e3afd0c059fc7593ada30", size = 127054, upload-time = "2026-05-09T10:02:52.332Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/90/5f/85535dfb3cfd6442d66d1df1694062c5d6df02f895329e7e120b2a3d2b8b/aiobotocore-3.7.0-py3-none-any.whl", hash = "sha256:680bde7c64679a821a9312641b759d9497f790ba8b2e88c6959e6273ee765b8e", size = 89539, upload-time = "2026-05-09T10:02:50.389Z" }, ] [[package]] name = "aiohappyeyeballs" version = "2.6.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591, upload-time = "2026-05-20T15:12:24.631Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062, upload-time = "2026-05-20T15:12:23.328Z" }, ] [[package]] name = "aiohttp" version = "3.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, { name = "aiosignal" }, { name = "async-timeout", marker = "python_full_version < '3.11'" }, { name = "attrs" }, { name = "frozenlist" }, { name = "multidict" }, { name = "propcache" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "yarl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6d/67/58ded4b3f2e10f94972d8928050c85330e249a31dd45a0e5f3c0e9c3fa05/aiohttp-3.14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8f6bb621e5863cfe8fe5ff5468002d200ec31f30f1280b259dc505b02595099e", size = 766140, upload-time = "2026-06-07T21:05:37.471Z" }, { url = "https://files.pythonhosted.org/packages/18/68/4ae5b4e08943f316594bb68da89957d3baf5760588fa09509594bd777e4b/aiohttp-3.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f7215cb3933784f79ed20e5f050e15984f390424339b22375d5a53c933a0491", size = 519430, upload-time = "2026-06-07T21:05:40.751Z" }, { url = "https://files.pythonhosted.org/packages/cb/c1/316c8f3549dbe5245f92bfd523ec6f32dd4d98cafe21df3f6a19b1184c75/aiohttp-3.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9d4e294455b23a68c9b8f042d0e8e377a265bcb15332753695f6e5b6819e0ce", size = 514406, upload-time = "2026-06-07T21:05:42.111Z" }, { url = "https://files.pythonhosted.org/packages/5a/ee/fb0ac28684e8d753b83c8a4eebc19a5846912aa0a4daaabb6a9936363840/aiohttp-3.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b238af795833d5731d049d82bc84b768ae6f8f97f0495963b3ed9935c5901cc3", size = 1703649, upload-time = "2026-06-07T21:05:43.427Z" }, { url = "https://files.pythonhosted.org/packages/3b/57/aa2beab673331f111885db8a7b69dfe3ab0e53e446a0ace18ca694b4dc58/aiohttp-3.14.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4e5e0ae56914ecdbf446493addefc0159053dd53962cef37d7839f37f73d505", size = 1675126, upload-time = "2026-06-07T21:05:44.897Z" }, { url = "https://files.pythonhosted.org/packages/47/ea/dad128abe365e79be03b16ed464198ac73e0d257e8260c6f7d6f31cbef26/aiohttp-3.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:092e4ce3619a7c6dee52a6bdabda973d9b34b66781f840ce93c7e0cec30cf521", size = 1771558, upload-time = "2026-06-07T21:05:46.405Z" }, { url = "https://files.pythonhosted.org/packages/63/f3/b5b4e10327cb85d34d24232c6b71b64602f190b3ccb238a043ac6b187dac/aiohttp-3.14.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb33777ea21e8b7ecde0e6fc84f598be0a1192eab1a63bc746d75aa75d38e7bd", size = 1856631, upload-time = "2026-06-07T21:05:47.844Z" }, { url = "https://files.pythonhosted.org/packages/2b/9d/93294c3045775c708ac8310eb3d3622a11d2951345ad590d532d62a1faa4/aiohttp-3.14.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23119f8fd4f5d16902ed459b63b100bcd269628075162bddac56cc7b5273b3fb", size = 1714139, upload-time = "2026-06-07T21:05:49.982Z" }, { url = "https://files.pythonhosted.org/packages/29/c4/93067c85a0373492ce8e577435203c5947c454af074ac48ed4f3a1b9dd4a/aiohttp-3.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57fc6745a4b7d0f5a9eb4f40a69718be6c0bc1b8368cc9fe89e90118719f4f42", size = 1588321, upload-time = "2026-06-07T21:05:51.431Z" }, { url = "https://files.pythonhosted.org/packages/c4/39/9ff91aaf02af8b7b8222a987466da539f154c3e01732c22b5f5a20a8ee66/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6fd35beba67c4183b09375c5fff9accb47524191a244a99f95fd4472f5402c2b", size = 1670375, upload-time = "2026-06-07T21:05:53.109Z" }, { url = "https://files.pythonhosted.org/packages/aa/e4/77452a3676b8d99ac1375f77691d6bf65ea6e9f4b201b82ef77c916dc767/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:672b9d65f42eb877f5c3f234a4547e4e1a226ca8c2eed879bb34670a0ce51192", size = 1690933, upload-time = "2026-06-07T21:05:54.902Z" }, { url = "https://files.pythonhosted.org/packages/7d/84/b0059a7c7fc05ea23f3bc1596ba91c12f79588b9450564a24cac37536d0a/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:24ba13339fed9251d9b1a1bec8c7ab84c0d1675d79d33501e11f94f8b9a84e05", size = 1740798, upload-time = "2026-06-07T21:05:56.458Z" }, { url = "https://files.pythonhosted.org/packages/8f/3a/e2a513ecbfc362591caa51a7f7e011b3bfc8938b388ae44cd95560d36999/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:94da27378da0610e341c4d30de29a191672683cc82b8f9556e8f7c7212a020fe", size = 1576412, upload-time = "2026-06-07T21:05:57.953Z" }, { url = "https://files.pythonhosted.org/packages/a1/10/08f1654f538f93d36dcac66310a06eefce4641cdafca83f9f0a5317be254/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52cdac9432d8b4a719f35094a818d95adcae0f0b4fe9b9b921909e0c87de9e7d", size = 1750199, upload-time = "2026-06-07T21:05:59.488Z" }, { url = "https://files.pythonhosted.org/packages/99/e4/d91b70c57d8b8e9611e4a2e52238ca3698d3dc1c2efe25b7a9bf594ac584/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:672ac254412a24d0d0cf00a9e6c238877e4be5e5fa2d188832c1244f45f31966", size = 1699356, upload-time = "2026-06-07T21:06:01.131Z" }, { url = "https://files.pythonhosted.org/packages/3d/f1/15340176f35ff61b95dbe34020bcf43f9e624a2d7bbac934715ff97d2033/aiohttp-3.14.1-cp310-cp310-win32.whl", hash = "sha256:2fe3607e71acc6ebb0ec8e492a247bf7a291226192dc0084236dfc12478916f6", size = 458939, upload-time = "2026-06-07T21:06:02.86Z" }, { url = "https://files.pythonhosted.org/packages/c3/c2/a2f1ec5b37f903109e43ae2862268cfe4a67a60c1b2cf43169fcdff5995f/aiohttp-3.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:30099eda75a53c32efb0920e9c33c195314d2cc1c680fbfd30894932ac5f27df", size = 482583, upload-time = "2026-06-07T21:06:04.666Z" }, { url = "https://files.pythonhosted.org/packages/d0/7a/7b56f6732ef79530afaa72aa335d41b67c8d79b946995f0b11ad72985435/aiohttp-3.14.1-cp310-cp310-win_arm64.whl", hash = "sha256:5a837f49d901f9e368651b676912bff1104ed8c1a83b280bcd7b29adccef5c9c", size = 453470, upload-time = "2026-06-07T21:06:06.322Z" }, { url = "https://files.pythonhosted.org/packages/26/dd/bf526e6f0a1120dd6f2df2e97bacfe4d358f13d17a0ff5847301a1375a51/aiohttp-3.14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2", size = 765225, upload-time = "2026-06-07T21:06:07.957Z" }, { url = "https://files.pythonhosted.org/packages/8f/e1/a2872aa55495a70f61310d411541c6ee23812d9a884e000c716e1bc3edbf/aiohttp-3.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f", size = 518743, upload-time = "2026-06-07T21:06:09.749Z" }, { url = "https://files.pythonhosted.org/packages/5b/e7/c60c7b209e509cc787de3cea0550a518538cfc08003e1c1e14c1c63fff71/aiohttp-3.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8", size = 514139, upload-time = "2026-06-07T21:06:11.26Z" }, { url = "https://files.pythonhosted.org/packages/5b/8d/614ace2f579702c9840ab1e1447fd8509e35b0b904f7196418fa2f57b25d/aiohttp-3.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04", size = 1784088, upload-time = "2026-06-07T21:06:12.887Z" }, { url = "https://files.pythonhosted.org/packages/49/e0/726e90f99542bf292f81a96a12cc4847deb86f3ccf62c6f4014a201f4d33/aiohttp-3.14.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8", size = 1737835, upload-time = "2026-06-07T21:06:14.564Z" }, { url = "https://files.pythonhosted.org/packages/0b/4b/d176d5c4db9d33dacf0543102ea59503bc1d528af4cfd0b719949ca49389/aiohttp-3.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6", size = 1842801, upload-time = "2026-06-07T21:06:16.228Z" }, { url = "https://files.pythonhosted.org/packages/dc/d6/5a99b563690ea0cbed912ae94a2ce33993a5709a651a3a4fe761e7dd973a/aiohttp-3.14.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af", size = 1929992, upload-time = "2026-06-07T21:06:17.947Z" }, { url = "https://files.pythonhosted.org/packages/76/7f/a987b14a3859094b3cea3f4825219c3e5536242564af6e3f9c2f6c994eb2/aiohttp-3.14.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730", size = 1786989, upload-time = "2026-06-07T21:06:19.677Z" }, { url = "https://files.pythonhosted.org/packages/f1/1a/420e5c85a3e73349372ed22ce0b6af86bfa6ce16a4b20a64a2e94608c781/aiohttp-3.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621", size = 1640129, upload-time = "2026-06-07T21:06:22.558Z" }, { url = "https://files.pythonhosted.org/packages/a7/80/18a592ed3be0a402cc03670bd72ee1f8563ddbe1d8d5542dbf868f274136/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee", size = 1756576, upload-time = "2026-06-07T21:06:24.8Z" }, { url = "https://files.pythonhosted.org/packages/ec/0b/8b3d5713373858ff71a617daf6e3b0e81ad63e79d09a3cf2f6b6b983939c/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573", size = 1754668, upload-time = "2026-06-07T21:06:26.528Z" }, { url = "https://files.pythonhosted.org/packages/9f/49/fd564575cf225821d7ba5a117cb8bc27213d8a7e1811162afb43ae077039/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7", size = 1817019, upload-time = "2026-06-07T21:06:28.297Z" }, { url = "https://files.pythonhosted.org/packages/ed/1b/e850c9ae6fc91356552ae668bb6c51e93fa29c8aef13398a10b56678557f/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf", size = 1631638, upload-time = "2026-06-07T21:06:30.242Z" }, { url = "https://files.pythonhosted.org/packages/eb/94/3c337ba72451a89806ace6f75bddc92bafc5b8d53d90115a512858024b63/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85", size = 1835660, upload-time = "2026-06-07T21:06:31.943Z" }, { url = "https://files.pythonhosted.org/packages/2b/9c/9c18cf367a0498212d9ba7daf990b504a5e8ae064cda4b504e2647c89c03/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3", size = 1775698, upload-time = "2026-06-07T21:06:33.72Z" }, { url = "https://files.pythonhosted.org/packages/b5/63/a251a9d2a6cb45065b2ddc0bde2b3dd10108740a9a42f632c66405a761a2/aiohttp-3.14.1-cp311-cp311-win32.whl", hash = "sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126", size = 458386, upload-time = "2026-06-07T21:06:35.279Z" }, { url = "https://files.pythonhosted.org/packages/17/ca/69274c51dcd6e8947d77b2806cf47a4a15f2c846e2cbeb1882547d3da283/aiohttp-3.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5", size = 483406, upload-time = "2026-06-07T21:06:36.824Z" }, { url = "https://files.pythonhosted.org/packages/2c/8a/c25904f77690c3688ec140f87591ef11a0cfe36bf3d5c0f1f38056fb62b3/aiohttp-3.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b", size = 452987, upload-time = "2026-06-07T21:06:38.371Z" }, { url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402, upload-time = "2026-06-07T21:06:40.311Z" }, { url = "https://files.pythonhosted.org/packages/c2/82/280619e0bd7bf2454987e19282616e84762255dd9c8468f62382e8c191f1/aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", size = 512310, upload-time = "2026-06-07T21:06:42.207Z" }, { url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448, upload-time = "2026-06-07T21:06:43.813Z" }, { url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854, upload-time = "2026-06-07T21:06:45.391Z" }, { url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884, upload-time = "2026-06-07T21:06:47.413Z" }, { url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034, upload-time = "2026-06-07T21:06:50.165Z" }, { url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054, upload-time = "2026-06-07T21:06:52.035Z" }, { url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278, upload-time = "2026-06-07T21:06:54.049Z" }, { url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795, upload-time = "2026-06-07T21:06:55.911Z" }, { url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397, upload-time = "2026-06-07T21:06:57.777Z" }, { url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504, upload-time = "2026-06-07T21:07:00.009Z" }, { url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806, upload-time = "2026-06-07T21:07:02.064Z" }, { url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707, upload-time = "2026-06-07T21:07:03.942Z" }, { url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121, upload-time = "2026-06-07T21:07:05.987Z" }, { url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580, upload-time = "2026-06-07T21:07:07.873Z" }, { url = "https://files.pythonhosted.org/packages/3e/61/a0c0a8f327a9c52095cdd8e312391b00d3ed64ab6c72bb5c33d8ec251cf7/aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", size = 452771, upload-time = "2026-06-07T21:07:09.669Z" }, { url = "https://files.pythonhosted.org/packages/df/d9/ea367c75f16ac9c6cdc8febb25e8318fa21a2b1bc8d6514d4b2d890bface/aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", size = 479873, upload-time = "2026-06-07T21:07:11.538Z" }, { url = "https://files.pythonhosted.org/packages/03/64/8d96784a7851156db8a4c6c3f6f91042fdf39fb15a4cc38c8b3c14833c45/aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", size = 448073, upload-time = "2026-06-07T21:07:13.637Z" }, { url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" }, { url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" }, { url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" }, { url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" }, { url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" }, { url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" }, { url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" }, { url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" }, { url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" }, { url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" }, { url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" }, { url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" }, { url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" }, { url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" }, { url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" }, { url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" }, { url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" }, { url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" }, { url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" }, { url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" }, { url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" }, { url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" }, { url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" }, { url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" }, { url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" }, { url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" }, { url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" }, { url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" }, { url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" }, { url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" }, { url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" }, { url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" }, { url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" }, { url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" }, { url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" }, { url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" }, { url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" }, { url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" }, { url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" }, { url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" }, { url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" }, { url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" }, { url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" }, { url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" }, { url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" }, { url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" }, { url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" }, { url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" }, { url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" }, { url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" }, { url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" }, { url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" }, { url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" }, { url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" }, { url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" }, { url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" }, { url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" }, { url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" }, { url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" }, { url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" }, { url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" }, { url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" }, { url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" }, { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" }, ] [[package]] name = "aioitertools" version = "0.13.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322, upload-time = "2025-11-06T22:17:07.609Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182, upload-time = "2025-11-06T22:17:06.502Z" }, ] [[package]] name = "aiokafka" version = "0.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-timeout" }, { name = "packaging" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/89/5f/dfc1180fd22d1acdc91949ec36e97199c43742dacb057cb8efed3679ed04/aiokafka-0.14.0.tar.gz", hash = "sha256:8ffdc945798ba4d3d132b705d4244d0a1f493925efb57c637a2ca88ee82794e1", size = 601374, upload-time = "2026-04-29T10:43:03.574Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e3/58/254fd1cd51c67976d0a0643ccb34a5603c4cc34fa8e8833895baffe35df6/aiokafka-0.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c767f320c902b126b7b37afb4ade241dc96e7d74b3515f0d0c1c8a800065113", size = 348717, upload-time = "2026-04-29T10:42:16.307Z" }, { url = "https://files.pythonhosted.org/packages/d8/1f/3326cabf36e4a2b05c81a1f2bb521a21b554998d02075151e84e510ad694/aiokafka-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f68e75acf03631ea046b00bc8cc9aca8e3eb89486468b884629586ae6f2c63bc", size = 352349, upload-time = "2026-04-29T10:42:18.116Z" }, { url = "https://files.pythonhosted.org/packages/e3/60/5c7c82e4d0c6f7e965e76e224378edd145c065901dadbb0052d74e8d6634/aiokafka-0.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fd32fbddaae68ff12960ab79e368375e925920547e53997333c41f5c63b076f", size = 1043405, upload-time = "2026-04-29T10:42:19.67Z" }, { url = "https://files.pythonhosted.org/packages/5b/ef/7ee4ca288234d6254357e8adfbb032ef619e7bdecda73bb8bd0291cbd0db/aiokafka-0.14.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffd44afdcc2e982986dc0c80c60b3553b46d8666eafa78a69038f24036e6", size = 1022819, upload-time = "2026-04-29T10:42:21.2Z" }, { url = "https://files.pythonhosted.org/packages/63/6a/ebd7f85a300589c3af49f75f60e1fc0057ece51549efdac20531b4094701/aiokafka-0.14.0-cp310-cp310-win32.whl", hash = "sha256:e51d48110767f228a44ccfd6e41c5444644c55e01f73b0a021227f19803a3714", size = 314631, upload-time = "2026-04-29T10:42:22.7Z" }, { url = "https://files.pythonhosted.org/packages/4c/c3/d58aa1c6fc7b78548fb6619303dcb1d8736ecb5313e03a14d97f7b4de7ce/aiokafka-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:9a7be05a3c72fa53c87b2a1c3979ca64d7fda870edb520ffc2871c2e7c99cd08", size = 331804, upload-time = "2026-04-29T10:42:24.456Z" }, { url = "https://files.pythonhosted.org/packages/57/f6/82b3d4eee9be6e81468f4b8b8a2f3780a0147095de6965d8c79db4c86f48/aiokafka-0.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:549ac4bf3bbc823151fd4bdf761d644db8b0271bd9ae3f110b7f5ab804fcc1aa", size = 348017, upload-time = "2026-04-29T10:42:26.323Z" }, { url = "https://files.pythonhosted.org/packages/f4/45/78cb9ab3e3d16c7fde1523cfefe329f3d1331e55abaff4da67c2f355e942/aiokafka-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9fa8416efd9f260c76125eceb554c4d731115df11d15fe6c4356a4855df7eccb", size = 351463, upload-time = "2026-04-29T10:42:28.441Z" }, { url = "https://files.pythonhosted.org/packages/b3/59/ee9a414470a978ac8edd8711482a973c513e3ef0a980eb71fc2d9dba6173/aiokafka-0.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ccacd1c5e0e3e1ab4d2b3dac5228623e5a682915a61d2adc2e018015aa259475", size = 1090654, upload-time = "2026-04-29T10:42:30.142Z" }, { url = "https://files.pythonhosted.org/packages/22/1f/780842cd28363c00a518859a58b6e802e5f79e88a442ffb482b88cfa891c/aiokafka-0.14.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ceb49c78b3e08ed3f9ff85350932ae59788596e8b45c6a4ca5d599337ba261e9", size = 1071040, upload-time = "2026-04-29T10:42:31.794Z" }, { url = "https://files.pythonhosted.org/packages/fe/a5/d13ee2387feb95fccaae56c6a4ee9df15dbc33bf319693ed7a81293643db/aiokafka-0.14.0-cp311-cp311-win32.whl", hash = "sha256:5383991dcad641868a0af78c42ac86a1406ccf9803a20e2d690fc34a6119134e", size = 313386, upload-time = "2026-04-29T10:42:34.105Z" }, { url = "https://files.pythonhosted.org/packages/12/5d/efe156d9e6cfc1affa8b513c8ec1f2ca0f2770e80a04058ce8a8851e7900/aiokafka-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:91f34a6f8626b20f0adacdd364036f40d1da85d213c2cf7be0607cde2c8d0f2b", size = 331737, upload-time = "2026-04-29T10:42:35.71Z" }, { url = "https://files.pythonhosted.org/packages/f9/9d/3441db94829f9feb802a2f4052df61c0d1a01272accd174c351d7e9e1f6a/aiokafka-0.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:284a90d617584d7e42688a181aaa8c2a909d9c658ab9b69c6cf92f4df5c4b320", size = 348458, upload-time = "2026-04-29T10:42:37.243Z" }, { url = "https://files.pythonhosted.org/packages/a4/10/7297589aac95654596af13301b31da2c9502c80e7e308530ee7a9bd5b9f1/aiokafka-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b4f211d9e03a1fc83871a37eefcf307bc0943ee99adae25aa39bd1722e70747b", size = 351057, upload-time = "2026-04-29T10:42:38.69Z" }, { url = "https://files.pythonhosted.org/packages/26/4e/5c0aa8db717fff0ffb8f3e16deece8f98ded6ca17c6a543b6b20cc9a7f84/aiokafka-0.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be517b9b9513eba43ba19961dd770a6e26d08325743093feb47182770d235dd9", size = 1142238, upload-time = "2026-04-29T10:42:39.96Z" }, { url = "https://files.pythonhosted.org/packages/88/78/322f797b9593a4cc8afd647342fa66b9ad732ee55098e5e084188c6202aa/aiokafka-0.14.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:219d2dc66b97b1aaea100697c928024b6a0348b7baa370b824900054bf86916e", size = 1131567, upload-time = "2026-04-29T10:42:41.542Z" }, { url = "https://files.pythonhosted.org/packages/8d/0a/a45320778385142299a7fc3ae402152ec1f383537130b8aa8e8587742fad/aiokafka-0.14.0-cp312-cp312-win32.whl", hash = "sha256:1086b470f6c452471603a2d9c8d6933739230c75758d777d8d113ff8112bad68", size = 312160, upload-time = "2026-04-29T10:42:42.811Z" }, { url = "https://files.pythonhosted.org/packages/a3/fb/7802a0ed69200e3e8e8791df06bd6daf9b00523839d045662de4ff061b18/aiokafka-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:bcf3a8f6592d73f45965ca0750bfdfccf2555c8625358175c92f75f2cce1261a", size = 331897, upload-time = "2026-04-29T10:42:43.984Z" }, { url = "https://files.pythonhosted.org/packages/30/b0/c9384541b2e4cc52a16402fc53fb9d44af0d78d37954cf8c7271c376ad47/aiokafka-0.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db16e43fac4c1c5006131046c1bf370c580d6ac4495a10ac7778245710943179", size = 345859, upload-time = "2026-04-29T10:42:45.449Z" }, { url = "https://files.pythonhosted.org/packages/7f/d1/fc266d9f4ffba4f197356c6ffdfbb0fe32e7cb874e240f299935d058ac06/aiokafka-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:32a8e91d88cf3ccf0778927715610d6579888c5f4748db4c2022cda25d628a48", size = 348284, upload-time = "2026-04-29T10:42:47.104Z" }, { url = "https://files.pythonhosted.org/packages/b2/8e/0c4c270786dac79f3fca74c6166c3a25b61b0d26132be0d69f0d7f206f0a/aiokafka-0.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aad4a575a506e7784e25e430f27026fe2f4378560b21b7f4e8c9a54f0d06eaee", size = 1117867, upload-time = "2026-04-29T10:42:48.394Z" }, { url = "https://files.pythonhosted.org/packages/9d/7f/3b89fbd0a3be9edfd5b51e20bb5cd695c851219b63c501c051cf84367fa9/aiokafka-0.14.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75e4a003502c9c3b5c705fa7c00d634ba146bf38fa5d525b80bb6ff6e3e779fe", size = 1108860, upload-time = "2026-04-29T10:42:50.249Z" }, { url = "https://files.pythonhosted.org/packages/b3/59/849aba75cff93277bf6bf8b630de79e902949ff7ec48e4b12a64e6e32cae/aiokafka-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a128e213cbc2bce0ea3db65a68920e52cebeeb8209bf001ac7aa022a8bd54d7d", size = 310889, upload-time = "2026-04-29T10:42:52.038Z" }, { url = "https://files.pythonhosted.org/packages/c4/e5/52eab8f8515d23da7b5d90e2c5ba10eab9494a0314f749e3f73e003f4a50/aiokafka-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:d6fa16bef3544be87bd1a7a8317b9d85e3da59f3202326d9ff22735ed052746e", size = 329470, upload-time = "2026-04-29T10:42:53.536Z" }, { url = "https://files.pythonhosted.org/packages/50/9d/984803315fe2b883ea6e08b1d9c8a752bd5c16e966d8714bacc67c72c417/aiokafka-0.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5d70615d1530ad19d0c4da8d87abaec0a12b9fdaabffdcd4e400efa0c50ef80c", size = 346672, upload-time = "2026-04-29T10:42:55.267Z" }, { url = "https://files.pythonhosted.org/packages/49/df/da314966b7f3c3117bd78b082563cb03dbe3007848cb8f4b0932faf390a0/aiokafka-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7e2392360c370b1ba6564c57d2889e154ecdb43157a8f7b7d7afe5e3c02fcc1a", size = 349594, upload-time = "2026-04-29T10:42:56.565Z" }, { url = "https://files.pythonhosted.org/packages/57/7a/160516944ea0e0f68ea78e38f944c52f5248c7c7df26cba22a40b9f25709/aiokafka-0.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:201e38ecc595f9f65a945f1ef9085157ddf28f25cd2e482fd9efa1fcf4638213", size = 1114112, upload-time = "2026-04-29T10:42:57.869Z" }, { url = "https://files.pythonhosted.org/packages/68/c4/9841118a2157e913e8ebfbc0a2b58f7b60f1f7202040c3e1df8925ed1184/aiokafka-0.14.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1cd651e1f56571baae306fdd0b5509047ab9625797a24cd75902e139c5a20318", size = 1098571, upload-time = "2026-04-29T10:42:59.356Z" }, { url = "https://files.pythonhosted.org/packages/d6/a1/0af8a37849a4108ae227f46c4c62f6beab31863cf66ba318fb73b0be5b26/aiokafka-0.14.0-cp314-cp314-win32.whl", hash = "sha256:128127eb96dab98150b636bb5f480c80e15f02f82a118eec206a521c8cf7cf7c", size = 314107, upload-time = "2026-04-29T10:43:01.111Z" }, { url = "https://files.pythonhosted.org/packages/fa/18/fb46c65f758900c71d0f1c73b7802720f99cabcb1f4a11676573f9bc1b8f/aiokafka-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa385039aa9b235359319bbdcf48c9c86a75d81c9c547d645056d00361238903", size = 333320, upload-time = "2026-04-29T10:43:02.424Z" }, ] [[package]] name = "aiopg" version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-timeout" }, { name = "psycopg2-binary" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b0/0a/aba75a9ffcb1704b98c39986344230eaa70c40ac28e5ca635df231db912f/aiopg-1.4.0.tar.gz", hash = "sha256:116253bef86b4d954116716d181e9a0294037f266718b2e1c9766af995639d71", size = 35593, upload-time = "2022-10-26T09:31:49.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9b/2f/ab8690bf995171b9a8b60b98a2ca91d4108a42422abf10bf622397437d26/aiopg-1.4.0-py3-none-any.whl", hash = "sha256:aea46e8aff30b039cfa818e6db4752c97656e893fc75e5a5dc57355a9e9dedbd", size = 34770, upload-time = "2022-10-26T09:31:48.019Z" }, ] [[package]] name = "aiormq" version = "6.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pamqp" }, { name = "yarl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6c/0e/db90154d52d399108903fe603e5110a533c42065180265dd003788264080/aiormq-6.9.4.tar.gz", hash = "sha256:0e7c01b662804e1cc7ace9a17794e8c1192a27fc2afa96162362a6e61ae8e8ef", size = 49232, upload-time = "2026-03-23T09:18:19.493Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6c/48/1ce3773f392f02ceda37aee168fade9d725483a9592c202d06044cd093ff/aiormq-6.9.4-py3-none-any.whl", hash = "sha256:726a8586695e863fba68cf88842065ab12348c9438dcebdfc9d0bddaf6083277", size = 32166, upload-time = "2026-03-23T09:18:17.523Z" }, ] [[package]] name = "aiosignal" version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] [[package]] name = "aiosqlite" version = "0.22.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, ] [[package]] name = "amqp" version = "5.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "vine" }, ] sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, ] [[package]] name = "annotated-doc" version = "0.0.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] name = "anyio" version = "4.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, ] [[package]] name = "asgiref" version = "3.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, ] [[package]] name = "async-timeout" version = "4.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/87/d6/21b30a550dafea84b1b8eee21b5e23fa16d010ae006011221f33dcd8d7f8/async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", size = 8345, upload-time = "2023-08-10T16:35:56.907Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028", size = 5721, upload-time = "2023-08-10T16:35:55.203Z" }, ] [[package]] name = "asyncclick" version = "8.3.0.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/3c/b266df0e8f3af371692d429eee4901d9392a14c864fa658d1cc7a5145881/asyncclick-8.3.0.3.tar.gz", hash = "sha256:2325fa89c13fab81430d4aca8bac17cb191c1b10a7aa7c13a7eea51cab5812c5", size = 288749, upload-time = "2025-10-03T09:03:23.586Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d3/d6/91ba5184580c7d1bb7448749afb0bb515ed5003c81b2cea3eb375362372e/asyncclick-8.3.0.3-py3-none-any.whl", hash = "sha256:a6fca7ed992459d8a807b5409d21aed6245d0342973dc3089a3f60560ea97c95", size = 109718, upload-time = "2025-10-03T09:03:21.854Z" }, ] [[package]] name = "asyncclick" version = "8.4.2.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", ] dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fd/b2/2c90a32d9b9cf1a4a90ed39cc1177890d146f3f89ae375b061d72110659a/asyncclick-8.4.2.1.tar.gz", hash = "sha256:8f259376ae8d4fcd91961c8ad3e85e9b63c9cf181a122201c7dfed3d7f6024b8", size = 340830, upload-time = "2026-06-30T12:40:33.842Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a7/44/ee9baab53fe4ff487fa47a1c50b174d3b3fb583cf2e22ff5ee33f61be899/asyncclick-8.4.2.1-py3-none-any.whl", hash = "sha256:dbd533769c4e3ce831c05394027ea1cd606c09688e18abab13dbcfbf7dc5a142", size = 121917, upload-time = "2026-06-30T12:40:32.213Z" }, ] [[package]] name = "asyncpg" version = "0.31.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-timeout", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c3/d9/507c80bdac2e95e5a525644af94b03fa7f9a44596a84bd48a6e80f854f92/asyncpg-0.31.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:831712dd3cf117eec68575a9b50da711893fd63ebe277fc155ecae1c6c9f0f61", size = 644865, upload-time = "2025-11-24T23:25:23.527Z" }, { url = "https://files.pythonhosted.org/packages/ea/03/f93b5e543f65c5f504e91405e8d21bb9e600548be95032951a754781a41d/asyncpg-0.31.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b17c89312c2f4ccea222a3a6571f7df65d4ba2c0e803339bfc7bed46a96d3be", size = 639297, upload-time = "2025-11-24T23:25:25.192Z" }, { url = "https://files.pythonhosted.org/packages/e5/1e/de2177e57e03a06e697f6c1ddf2a9a7fcfdc236ce69966f54ffc830fd481/asyncpg-0.31.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3faa62f997db0c9add34504a68ac2c342cfee4d57a0c3062fcf0d86c7f9cb1e8", size = 2816679, upload-time = "2025-11-24T23:25:26.718Z" }, { url = "https://files.pythonhosted.org/packages/d0/98/1a853f6870ac7ad48383a948c8ff3c85dc278066a4d69fc9af7d3d4b1106/asyncpg-0.31.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ea599d45c361dfbf398cb67da7fd052affa556a401482d3ff1ee99bd68808a1", size = 2867087, upload-time = "2025-11-24T23:25:28.399Z" }, { url = "https://files.pythonhosted.org/packages/11/29/7e76f2a51f2360a7c90d2cf6d0d9b210c8bb0ae342edebd16173611a55c2/asyncpg-0.31.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:795416369c3d284e1837461909f58418ad22b305f955e625a4b3a2521d80a5f3", size = 2747631, upload-time = "2025-11-24T23:25:30.154Z" }, { url = "https://files.pythonhosted.org/packages/5d/3f/716e10cb57c4f388248db46555e9226901688fbfabd0afb85b5e1d65d5a7/asyncpg-0.31.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a8d758dac9d2e723e173d286ef5e574f0b350ec00e9186fce84d0fc5f6a8e6b8", size = 2855107, upload-time = "2025-11-24T23:25:31.888Z" }, { url = "https://files.pythonhosted.org/packages/7e/ec/3ebae9dfb23a1bd3f68acfd4f795983b65b413291c0e2b0d982d6ae6c920/asyncpg-0.31.0-cp310-cp310-win32.whl", hash = "sha256:2d076d42eb583601179efa246c5d7ae44614b4144bc1c7a683ad1222814ed095", size = 521990, upload-time = "2025-11-24T23:25:33.402Z" }, { url = "https://files.pythonhosted.org/packages/20/b4/9fbb4b0af4e36d96a61d026dd37acab3cf521a70290a09640b215da5ab7c/asyncpg-0.31.0-cp310-cp310-win_amd64.whl", hash = "sha256:9ea33213ac044171f4cac23740bed9a3805abae10e7025314cfbd725ec670540", size = 581629, upload-time = "2025-11-24T23:25:34.846Z" }, { url = "https://files.pythonhosted.org/packages/08/17/cc02bc49bc350623d050fa139e34ea512cd6e020562f2a7312a7bcae4bc9/asyncpg-0.31.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eee690960e8ab85063ba93af2ce128c0f52fd655fdff9fdb1a28df01329f031d", size = 643159, upload-time = "2025-11-24T23:25:36.443Z" }, { url = "https://files.pythonhosted.org/packages/a4/62/4ded7d400a7b651adf06f49ea8f73100cca07c6df012119594d1e3447aa6/asyncpg-0.31.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2657204552b75f8288de08ca60faf4a99a65deef3a71d1467454123205a88fab", size = 638157, upload-time = "2025-11-24T23:25:37.89Z" }, { url = "https://files.pythonhosted.org/packages/d6/5b/4179538a9a72166a0bf60ad783b1ef16efb7960e4d7b9afe9f77a5551680/asyncpg-0.31.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a429e842a3a4b4ea240ea52d7fe3f82d5149853249306f7ff166cb9948faa46c", size = 2918051, upload-time = "2025-11-24T23:25:39.461Z" }, { url = "https://files.pythonhosted.org/packages/e6/35/c27719ae0536c5b6e61e4701391ffe435ef59539e9360959240d6e47c8c8/asyncpg-0.31.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0807be46c32c963ae40d329b3a686356e417f674c976c07fa49f1b30303f109", size = 2972640, upload-time = "2025-11-24T23:25:41.512Z" }, { url = "https://files.pythonhosted.org/packages/43/f4/01ebb9207f29e645a64699b9ce0eefeff8e7a33494e1d29bb53736f7766b/asyncpg-0.31.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e5d5098f63beeae93512ee513d4c0c53dc12e9aa2b7a1af5a81cddf93fe4e4da", size = 2851050, upload-time = "2025-11-24T23:25:43.153Z" }, { url = "https://files.pythonhosted.org/packages/3e/f4/03ff1426acc87be0f4e8d40fa2bff5c3952bef0080062af9efc2212e3be8/asyncpg-0.31.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37fc6c00a814e18eef51833545d1891cac9aa69140598bb076b4cd29b3e010b9", size = 2962574, upload-time = "2025-11-24T23:25:44.942Z" }, { url = "https://files.pythonhosted.org/packages/c7/39/cc788dfca3d4060f9d93e67be396ceec458dfc429e26139059e58c2c244d/asyncpg-0.31.0-cp311-cp311-win32.whl", hash = "sha256:5a4af56edf82a701aece93190cc4e094d2df7d33f6e915c222fb09efbb5afc24", size = 521076, upload-time = "2025-11-24T23:25:46.486Z" }, { url = "https://files.pythonhosted.org/packages/28/fc/735af5384c029eb7f1ca60ccb8fa95521dbdaeef788edf4cecfc604c3cab/asyncpg-0.31.0-cp311-cp311-win_amd64.whl", hash = "sha256:480c4befbdf079c14c9ca43c8c5e1fe8b6296c96f1f927158d4f1e750aacc047", size = 584980, upload-time = "2025-11-24T23:25:47.938Z" }, { url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042, upload-time = "2025-11-24T23:25:49.578Z" }, { url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504, upload-time = "2025-11-24T23:25:51.501Z" }, { url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241, upload-time = "2025-11-24T23:25:53.278Z" }, { url = "https://files.pythonhosted.org/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321, upload-time = "2025-11-24T23:25:54.982Z" }, { url = "https://files.pythonhosted.org/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685, upload-time = "2025-11-24T23:25:57.43Z" }, { url = "https://files.pythonhosted.org/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858, upload-time = "2025-11-24T23:25:59.636Z" }, { url = "https://files.pythonhosted.org/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852, upload-time = "2025-11-24T23:26:01.084Z" }, { url = "https://files.pythonhosted.org/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175, upload-time = "2025-11-24T23:26:02.564Z" }, { url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" }, { url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" }, { url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" }, { url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" }, { url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" }, { url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" }, { url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" }, { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" }, { url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" }, { url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" }, { url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" }, { url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678, upload-time = "2025-11-24T23:26:23.627Z" }, { url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505, upload-time = "2025-11-24T23:26:25.235Z" }, { url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744, upload-time = "2025-11-24T23:26:26.944Z" }, { url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251, upload-time = "2025-11-24T23:26:28.404Z" }, { url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901, upload-time = "2025-11-24T23:26:30.34Z" }, { url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280, upload-time = "2025-11-24T23:26:32Z" }, { url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931, upload-time = "2025-11-24T23:26:33.572Z" }, { url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608, upload-time = "2025-11-24T23:26:35.638Z" }, { url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738, upload-time = "2025-11-24T23:26:37.275Z" }, { url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026, upload-time = "2025-11-24T23:26:39.423Z" }, { url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426, upload-time = "2025-11-24T23:26:41.032Z" }, { url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495, upload-time = "2025-11-24T23:26:42.659Z" }, { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, ] [[package]] name = "attrs" version = "26.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] [[package]] name = "billiard" version = "4.2.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f", size = 156537, upload-time = "2025-11-30T13:28:48.52Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" }, ] [[package]] name = "blinker" version = "1.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, ] [[package]] name = "boto3" version = "1.43.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b7/65/47670987f2f9e181397872c7ee6415b7b95156d711b7eab6c55f66e575bc/boto3-1.43.0.tar.gz", hash = "sha256:80d44a943ef90aba7958ab31d30c155c198acc8a9581b5846b3878b2c8951086", size = 113143, upload-time = "2026-04-29T22:07:49.084Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/a0/3e6a0b1c1ea6bec76f71473727ef27abf3cd40e9709b3ebcbfbcfaae6f79/boto3-1.43.0-py3-none-any.whl", hash = "sha256:8ebe03754a4b73a5cb6ec2f14cca03ac33bd4760d0adea53da4724845130258b", size = 140497, upload-time = "2026-04-29T22:07:46.216Z" }, ] [[package]] name = "botocore" version = "1.43.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/79/2f4be1896db3db7ccf44504253a175d56b6bd6b669619edc5147d1aa21ea/botocore-1.43.0.tar.gz", hash = "sha256:e933b31a2d644253e1d029d7d39e99ba41b87e29300534f189744cc438cdf928", size = 15286817, upload-time = "2026-04-29T22:07:31.723Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bf/4b/afc1fef8a43bafb139f57f73bbd70df82807af5934321e8112ae50668827/botocore-1.43.0-py3-none-any.whl", hash = "sha256:cc5b15eaec3c6eac05d8012cb5ef17ebe891beb88a16ca13c374bfaece1241e6", size = 14970102, upload-time = "2026-04-29T22:07:27Z" }, ] [[package]] name = "cachetools" version = "7.1.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f4/8b/0d3945a13955303b81272f759a0331e54c5c793da455e6f5706b89d2639c/cachetools-7.1.4.tar.gz", hash = "sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6", size = 40085, upload-time = "2026-05-21T22:40:43.376Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8c/7b/1fc1c09cc0756cf25861a3be10565915953876da48bb228fb9a672b20a42/cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54", size = 16761, upload-time = "2026-05-21T22:40:41.845Z" }, ] [[package]] name = "cassandra-driver" version = "3.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecated" }, { name = "geomet" }, ] sdist = { url = "https://files.pythonhosted.org/packages/85/26/8806d0949422b560029a040e7a628d92addd612726468c3fb546354a43a4/cassandra_driver-3.30.0.tar.gz", hash = "sha256:7e4cfd6ec3023576ed0ffa34882d9778e4bacfd918048ae9139ccdd00628ed85", size = 4242039, upload-time = "2026-04-16T05:04:59.838Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/67/01/b13641332953702238b5cdf80340a117b384070fc1cc582030d7dffbde14/cassandra_driver-3.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2637644eac9274e46b0c2a7f729158bdf8582b6842dc48e18297211dd3ee1fec", size = 7612055, upload-time = "2026-04-16T05:01:56.997Z" }, { url = "https://files.pythonhosted.org/packages/a6/52/1361729ce8e5ded444b11e00eee19df09e040d46236c7df7cded51913c7a/cassandra_driver-3.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a5c8982f2b9eb4e789fc12cdd930b1e1511b6d046dde31d0703f855745556a3", size = 7367328, upload-time = "2026-04-16T05:02:06.188Z" }, { url = "https://files.pythonhosted.org/packages/96/c6/b4d5b01e10570dfb3394ebf7af40acbe435caaf246553992a459d42caca6/cassandra_driver-3.30.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a0679ebcfdcecb3763c690b5bc6a517e0c0803f7bc88e0a6c793e5e421b558a", size = 7587311, upload-time = "2026-04-16T05:02:16.909Z" }, { url = "https://files.pythonhosted.org/packages/b3/83/d5868bc1ea2ffd392dea683589c4dd64363f619ba2d36de802f9ed38775f/cassandra_driver-3.30.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e12dfcd3f0074c16f4bfe650242edb406b935864373ae86160e09e3f5e437e84", size = 7837314, upload-time = "2026-04-16T05:02:24.742Z" }, { url = "https://files.pythonhosted.org/packages/ff/e9/ac4f81d96cc9df0ba457ca5f447b405f730de8f343422662f4719ae23619/cassandra_driver-3.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1d64cbdce764c33e284d339b9a749736d68971edf8b537888f2d13c4b0d1313f", size = 6955273, upload-time = "2026-04-16T05:02:31.22Z" }, { url = "https://files.pythonhosted.org/packages/5f/8b/f75fe284ee184b54ba55b2cd69d2ba156e6a8fc88b322e28e2ecbaa6065a/cassandra_driver-3.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5c6cbb396ad6fe456efc799d3b8b6bda360ffc06552c5be2ce1a88ac381a305c", size = 7415263, upload-time = "2026-04-16T05:02:38.441Z" }, { url = "https://files.pythonhosted.org/packages/92/f6/67a0e6f937e85131c5e528d3c74e63b1fb041f0d6351eb7b7d5a403ae149/cassandra_driver-3.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d5e3575ec01d8c043b56ff25de6f61ff4c9ed5cb3ea4c3d9df98def71ba710c", size = 7252160, upload-time = "2026-04-16T05:02:45.14Z" }, { url = "https://files.pythonhosted.org/packages/aa/1f/48691b5bed702bbe4db3acb00c1a3cc585e18bd70dadb12a5cf0697c6159/cassandra_driver-3.30.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff2e9fbdc1be54c1d041ea3f7d09812442f334be14bb5ad7aede175544765d25", size = 7586952, upload-time = "2026-04-16T05:02:52.99Z" }, { url = "https://files.pythonhosted.org/packages/61/56/74b01c48acf401de3f38054bdfcbb493770e13dd2ddbb4735783045f1e3b/cassandra_driver-3.30.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:385134eba72f048707cd800de0a61cf3c23246113edffe9bc6bc2eb86282d26b", size = 7832995, upload-time = "2026-04-16T05:03:01.344Z" }, { url = "https://files.pythonhosted.org/packages/7b/da/25f3542b1b7221c12e323eae591015e2e816fd543e6538cfc29d2c6ff059/cassandra_driver-3.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:0c28a8e84917acebecbaed39844047c2f135739c3627dd7b9f8541af33e11df3", size = 6965120, upload-time = "2026-04-16T05:03:08.538Z" }, { url = "https://files.pythonhosted.org/packages/89/46/8fc42a2c836ec525f9313e6bdffbada2911d8501ff008e5d22ebdbaa4895/cassandra_driver-3.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:212af4d8ff934c30538f4bdf7da61f14dc9a30349f6cac2161c8125e56fad928", size = 7751805, upload-time = "2026-04-16T05:03:15.913Z" }, { url = "https://files.pythonhosted.org/packages/a3/e3/8c3c02380334697c629de2db560d67e44461401175887e7a36374e30bcad/cassandra_driver-3.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61d7eeb17d8f76d5b4a9b1239145250f2a9f7bf949c30e2cc36196b5a0523ce1", size = 7576768, upload-time = "2026-04-16T05:03:23.624Z" }, { url = "https://files.pythonhosted.org/packages/8f/5d/ead64522236077f4a63974ed4f193f26e35f85132751154f9aa839680dc5/cassandra_driver-3.30.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83a9148d408a3dbb48ea1802d643d60fa53cd69dc7b9a244511ecf5b917e4f53", size = 7330139, upload-time = "2026-04-16T05:03:30.567Z" }, { url = "https://files.pythonhosted.org/packages/13/fe/bbed363953eee1774842806527e377a79f0cbfb52887b6379c856ff8fedf/cassandra_driver-3.30.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f4225082a11d9529416c223553ab38a29c4e65da6646b40159c554480dc002c", size = 7581659, upload-time = "2026-04-16T05:03:37.811Z" }, { url = "https://files.pythonhosted.org/packages/a4/60/c776d0d9d6b92bd2f15720db51313af8a194272f725d3fda8827258b728b/cassandra_driver-3.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:6d449f49ce866ac20a1c3d80b1f9245ecdfd1e67b843dccd3d6eccdfe519c02e", size = 6795314, upload-time = "2026-04-16T05:03:44.648Z" }, { url = "https://files.pythonhosted.org/packages/10/e8/32c6063738ce418403750575a1a1f73609c313766849cb930699d607fc8d/cassandra_driver-3.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d2f9e00127f70dff42d4ef932df8a6b81170c2861d4e75c8b13f4b4816b4450c", size = 7738805, upload-time = "2026-04-16T05:03:51.875Z" }, { url = "https://files.pythonhosted.org/packages/48/2c/b88c3747c63eeb36b1ebb46a50734b2490eb05f87944ec907563f861bb76/cassandra_driver-3.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8c4acd28791854c23ca68be50a7a750c9413ba80fec0ca5c27c2be05f6f3fe0a", size = 7568531, upload-time = "2026-04-16T05:03:59.122Z" }, { url = "https://files.pythonhosted.org/packages/1e/c0/f44f3fdb7c40cddad7b58cc8102387ae8ee4da793ed5899d4a231989fbea/cassandra_driver-3.30.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:289e86c81be2543cb9055600c0819850db921e6e138a84e5c88ec160662c7207", size = 7324591, upload-time = "2026-04-16T05:04:06.501Z" }, { url = "https://files.pythonhosted.org/packages/63/7a/54466eff67b69653c83d5bcf3972e42a52b2e881f9a3ee7ca8b996943caf/cassandra_driver-3.30.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d73c0429813045ba86b92fc033fbcfd495aa10e9d4a40fe30b6e9dfe8b5d3ab4", size = 7575795, upload-time = "2026-04-16T05:04:13.645Z" }, { url = "https://files.pythonhosted.org/packages/4a/0d/ff571cb27e56d8112bc3f4a5e807da54c8b875264e038e791e06ddcc1817/cassandra_driver-3.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:17fb53587c9fc6a27b5c4a89b4f3d9169be43fc572d6f3f67494aa74708be936", size = 6798091, upload-time = "2026-04-16T05:04:20.145Z" }, { url = "https://files.pythonhosted.org/packages/73/c0/a8b52c18959d96d313450cdfa97f66d4b67327814671df65a7a2e7fca64f/cassandra_driver-3.30.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c64e20bf46b49f8ef64569208d4a395b0928c27d5960559922a2d13471924d0d", size = 7736773, upload-time = "2026-04-16T05:04:27.315Z" }, { url = "https://files.pythonhosted.org/packages/48/a8/a7aae33ef0a74616210b2cff18a5efba3db99f1c5bb7e2a5a31aeabb8558/cassandra_driver-3.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:923a6e1c3fa5f98f846a028b1a7207ec9e7d8cfa54ea47a507d41122efa2f54f", size = 7562347, upload-time = "2026-04-16T05:04:34.5Z" }, { url = "https://files.pythonhosted.org/packages/99/9e/bed3930f4a726b3783fea179a88cd16b95d968ec8fc1af9d5193d26cfc39/cassandra_driver-3.30.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1b4aa6c7706dec839134adb6a2094d90c5f6f35efa08028ed6aae6e67c8643e", size = 7382080, upload-time = "2026-04-16T05:04:41.48Z" }, { url = "https://files.pythonhosted.org/packages/f6/9b/8cd9c03474bac374b0cda4124c7c4d2f6b2b5f7942b028f82222daf54fcb/cassandra_driver-3.30.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:137498e2a9b6f578d1902e1af8a988e50b8fe134c76a176f1b8a774e906bc66c", size = 7590242, upload-time = "2026-04-16T05:04:48.477Z" }, { url = "https://files.pythonhosted.org/packages/c4/29/aed0ee68f724b9fecc9cebfcae44f331b5dd2e010415f4be2d6e64d77226/cassandra_driver-3.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:136b46437b9902673264e101cdaab309d3e40607bff34430bda86b785badc6e4", size = 6787116, upload-time = "2026-04-16T05:04:55.349Z" }, ] [[package]] name = "celery" version = "5.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "billiard" }, { name = "click" }, { name = "click-didyoumean" }, { name = "click-plugins" }, { name = "click-repl" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "kombu" }, { name = "python-dateutil" }, { name = "tzlocal" }, { name = "vine" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e8/b4/a1233943ab5c8ea05fb877a88a0a0622bf47444b99e4991a8045ac37ea1d/celery-5.6.3.tar.gz", hash = "sha256:177006bd2054b882e9f01be59abd8529e88879ef50d7918a7050c5a9f4e12912", size = 1742243, upload-time = "2026-03-26T12:14:51.76Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cf/c9/6eccdda96e098f7ae843162db2d3c149c6931a24fda69fe4ab84d0027eb5/celery-5.6.3-py3-none-any.whl", hash = "sha256:0808f42f80909c4d5833202360ffafb2a4f83f4d8e23e1285d926610e9a7afa6", size = 451235, upload-time = "2026-03-26T12:14:49.491Z" }, ] [[package]] name = "certifi" version = "2026.6.17" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] [[package]] name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] [[package]] name = "cfgv" version = "3.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] name = "click" version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] name = "click-didyoumean" version = "0.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, ] sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" }, ] [[package]] name = "click-plugins" version = "1.1.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" }, ] [[package]] name = "click-repl" version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "prompt-toolkit" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "confluent-kafka" version = "2.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/51/90/eb998fedefb63b42910b54b76b7d300ccddc56430a5175122eb60cedc4f3/confluent_kafka-2.15.0.tar.gz", hash = "sha256:7ad9bad1cbabf6713ec039b8204b48d322024fd11397eec88d912e048c732ba7", size = 323365, upload-time = "2026-06-30T19:48:43.395Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/8c/b46a53a5025470bcc6c5c1fa556348096484a6e30421180de7bbd4b1af25/confluent_kafka-2.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8ac4addda1cff47adc316146ee74aa2a83eeef14b176252f6f1abd29ae1d5a2", size = 4362388, upload-time = "2026-06-30T19:47:43.269Z" }, { url = "https://files.pythonhosted.org/packages/b5/fa/7be0239c65bdcb47d5aaeaf01090f5e2e118d7899e34a65736c78a5ff31f/confluent_kafka-2.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:64c5a9dbe563c964e21a83f3cb4ff1de6536e484a56289fe9aff69fc33302c73", size = 4334523, upload-time = "2026-06-30T19:47:46.155Z" }, { url = "https://files.pythonhosted.org/packages/cb/29/8445fa9fd66651b1748cbf0a1650b4f9b90411eecedd8390247f311f83b8/confluent_kafka-2.15.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:f4a8f02dbc93f17e125a201c5e5ce2143794b71bb5c91836ef87959fc5dcf6a4", size = 4974790, upload-time = "2026-06-30T19:47:47.861Z" }, { url = "https://files.pythonhosted.org/packages/83/e2/8065a16b2a6390f0612bc387a1879cc35ed7be0875106d9d73f4f25b0412/confluent_kafka-2.15.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d9c9d240b681427568d9e341cf3deb3b7350b69931e6fbc636c5986432fb03f4", size = 4781067, upload-time = "2026-06-30T19:47:49.611Z" }, { url = "https://files.pythonhosted.org/packages/2e/9c/2f4158bfc1a24f3b775f6abbe921045f4b1214c44bb44f8a7fd1631129aa/confluent_kafka-2.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:039444ea430aaef415487df406070f364d956d76bbd980337455c2a7b5c5789b", size = 4548789, upload-time = "2026-06-30T19:47:51.19Z" }, { url = "https://files.pythonhosted.org/packages/5e/42/3522fb404e3b4a323fe25c5993cd2e8fcfa5d3a82f65aa8a7ffd49bdbec9/confluent_kafka-2.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4493caf1f1d2e295f5be8400d3a9854016456241f7062d0fb22dd01e2632e6", size = 4362085, upload-time = "2026-06-30T19:47:52.751Z" }, { url = "https://files.pythonhosted.org/packages/3a/20/7e8aca63fabcbd88bc47c337187f3f6ad34f9950ed58a2184a5473617719/confluent_kafka-2.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c311695a10cefc1579372bd2f3808d28fd0c3d8cc7f578c94a381f351018912e", size = 4334128, upload-time = "2026-06-30T19:47:54.272Z" }, { url = "https://files.pythonhosted.org/packages/77/c2/c2f38b593ce622aba2cab5c3272932d1538394cec8bd5fcb2f072c61bf3e/confluent_kafka-2.15.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:7868fe2c2e1f6434903e3f1ea0ba6976d9382f5a9ef46f85d205ca7eb6f28689", size = 4974285, upload-time = "2026-06-30T19:47:56.062Z" }, { url = "https://files.pythonhosted.org/packages/c6/18/fe516c9e158556b59c16b92da948186ba240925f80242d39795d94255b27/confluent_kafka-2.15.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e31bbb5775da23f14264b3e6efd3839c1611a639f3d8c15516350798fe6f2750", size = 4780780, upload-time = "2026-06-30T19:47:57.765Z" }, { url = "https://files.pythonhosted.org/packages/62/30/3b204e0df740d6941b7da440b3d45cc26c64e1a455e38d6ec4c2360f1a7e/confluent_kafka-2.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c041b784f25d2b0b8b2abab7390020951229b694a5c037402c493b0eeca12020", size = 4548794, upload-time = "2026-06-30T19:47:59.416Z" }, { url = "https://files.pythonhosted.org/packages/7a/b3/34811eb66633dce51f583cd0ea5d78a26c9a135847d7dbd536fc5a963055/confluent_kafka-2.15.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6e5ea2933c0b07782a99dfdfc9d52e92526e7a61839a6994feb994dcf2a78a74", size = 4367689, upload-time = "2026-06-30T19:48:00.855Z" }, { url = "https://files.pythonhosted.org/packages/8f/08/9fea6928cd1e678df0be930206e631bb7e44e7ed54560f5b3f76352187ff/confluent_kafka-2.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e4465a060ab215d5d514b181f3e7502c6fd7887529381c4af940a0b60f9d65cc", size = 4338594, upload-time = "2026-06-30T19:48:02.469Z" }, { url = "https://files.pythonhosted.org/packages/25/5f/f9728a100ba40d31a957c5f5cb594ea7d49fd1188002543d3543349a063a/confluent_kafka-2.15.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b68e6f441866542c9150e6fdf106131eee5a51db591937888c866d799fb354ec", size = 4979384, upload-time = "2026-06-30T19:48:04.127Z" }, { url = "https://files.pythonhosted.org/packages/8b/2e/9f80cae9dd7b4852b8a24f9f91001916b987a2ba359e9f077c43ca41d5da/confluent_kafka-2.15.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:fd833dab1392f456b9dd52b7ed9ac65b2cc36871ed5e3ebe8d53486c965b6453", size = 4785580, upload-time = "2026-06-30T19:48:05.841Z" }, { url = "https://files.pythonhosted.org/packages/6e/b6/02f73ed259d5be5fbbc8dde41527cad097eca56ab7831026039c8abbaffc/confluent_kafka-2.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:2cc8d01a77c534669bb896f731c8917cd63b787618081fdd0d1420d58cd6815b", size = 4549188, upload-time = "2026-06-30T19:48:07.426Z" }, { url = "https://files.pythonhosted.org/packages/b0/80/c7bba18644fb748fc1202e2652230652d243c764fc158798d9f2096513c4/confluent_kafka-2.15.0-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:ff43508f8e929d83545272ef4f17e27fdaa9cc8397f77cf2236b22d67de4edc4", size = 4343586, upload-time = "2026-06-30T19:48:09.272Z" }, { url = "https://files.pythonhosted.org/packages/fa/41/0be8143a944c70c6055e8d0fb713964ebe173494e280451c927fa02247ab/confluent_kafka-2.15.0-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:41d4c7360c51202785e8e9aa56241dd392b9dbcc311eae7c5e6985b239efc12b", size = 4371325, upload-time = "2026-06-30T19:48:10.996Z" }, { url = "https://files.pythonhosted.org/packages/e2/3c/d5c83c20599a82faa065b922b55338c672a268fa2ac75f54e53bed8913d7/confluent_kafka-2.15.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b0ac3831aac47ea23699a6ca5b9836288a4b9d201015dbfc4c3ec9f20592fdb5", size = 4979865, upload-time = "2026-06-30T19:48:12.447Z" }, { url = "https://files.pythonhosted.org/packages/40/79/5be34236697e8fabbed5534a67100b9d9d11c210de55ba3ba2590d7d7634/confluent_kafka-2.15.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0455754e8294e1e76cb5acc083c300b6bd8d88f2d2c551c583cbf4ab55889a57", size = 4785955, upload-time = "2026-06-30T19:48:14.055Z" }, { url = "https://files.pythonhosted.org/packages/94/ff/4da4e954040769d46111f3c0e5b3e76f2d3bc4b27c1deb5fd64fc9cc4055/confluent_kafka-2.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:7c59ea36606e0e946e6b353203136b4d71be6d95c0c8ae5113b81850ff5b7b41", size = 4608827, upload-time = "2026-06-30T19:48:15.746Z" }, { url = "https://files.pythonhosted.org/packages/18/20/5c078b9560b8005404e09a5889b9771e8deb5c9dddeb9003f58c7d028258/confluent_kafka-2.15.0-cp314-cp314-macosx_13_0_arm64.whl", hash = "sha256:e781d39ff31d5d10d0502471f92f95aff4f5be1eaae9a1f57aeac164bc9f9029", size = 4343440, upload-time = "2026-06-30T19:48:17.324Z" }, { url = "https://files.pythonhosted.org/packages/aa/27/fd83281231b6179e3923822e20861dfc1d6c200edb910c3d8e9a15b9e95a/confluent_kafka-2.15.0-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:000463c822a7adc3293369b63688b68d17c03a1a4a64f86182efc08f8b150676", size = 4371051, upload-time = "2026-06-30T19:48:19.29Z" }, { url = "https://files.pythonhosted.org/packages/5e/3e/fa82e6699144e707972bbf57489598defdf67e844ae2a7d29e0ea4e3a187/confluent_kafka-2.15.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:9ddf4cf4647e5d633ef64e3f5a349d5288edfedf974203993e9d86548c2695be", size = 4979624, upload-time = "2026-06-30T19:48:21.454Z" }, { url = "https://files.pythonhosted.org/packages/13/ee/b4b6a0da17584b432c83a0500ac79c04864ef92dafdb405614831b995232/confluent_kafka-2.15.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d8ed33f623ff2a104fb76f99a67e9917f0170fddb4e28380fcdc83347b1646b2", size = 4785678, upload-time = "2026-06-30T19:48:23.068Z" }, { url = "https://files.pythonhosted.org/packages/a8/f4/19a852d16e8e4f8ac930037d8ecda21a220f6d5d050a59bbce10189ac9ec/confluent_kafka-2.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:2e80bd96f61aae2ffba951754a769e6d3c5ebb5a5e778ed0ae8ad899ea91556b", size = 4744444, upload-time = "2026-06-30T19:48:24.698Z" }, ] [[package]] name = "cramjam" version = "2.11.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/14/12/34bf6e840a79130dfd0da7badfb6f7810b8fcfd60e75b0539372667b41b6/cramjam-2.11.0.tar.gz", hash = "sha256:5c82500ed91605c2d9781380b378397012e25127e89d64f460fea6aeac4389b4", size = 99100, upload-time = "2025-07-27T21:25:07.559Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/53/d3/20d0402e4e983b66603117ad3dd3b864a05d7997a830206d3ff9cacef9a2/cramjam-2.11.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d0859c65775e8ebf2cbc084bfd51bd0ffda10266da6f9306451123b89f8e5a63", size = 3558999, upload-time = "2025-07-27T21:21:34.105Z" }, { url = "https://files.pythonhosted.org/packages/f5/a8/a6e2744288938ccd320a5c6f6f3653faa790f933f5edd088c6e5782a2354/cramjam-2.11.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1d77b9b0aca02a3f6eeeff27fcd315ca5972616c0919ee38e522cce257bcd349", size = 1861558, upload-time = "2025-07-27T21:21:36.624Z" }, { url = "https://files.pythonhosted.org/packages/96/29/7961e09a849eea7d8302e7baa6f829dd3ef3faf199cb25ed29b318ae799b/cramjam-2.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66425bc25b5481359b12a6719b6e7c90ffe76d85d0691f1da7df304bfb8ce45c", size = 1699431, upload-time = "2025-07-27T21:21:38.396Z" }, { url = "https://files.pythonhosted.org/packages/7a/60/6665e52f01a8919bf37c43dcf0e03b6dd3866f5c4e95440b357d508ee14e/cramjam-2.11.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd748d3407ec63e049b3aea1595e218814fccab329b7fb10bb51120a30e9fb7e", size = 2025262, upload-time = "2025-07-27T21:21:40.417Z" }, { url = "https://files.pythonhosted.org/packages/d7/80/79bd84dbeb109e2c6efb74e661b7bd4c3ba393208ebcf69e2ae9454ae80c/cramjam-2.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6d9a23a35b3a105c42a8de60fc2e80281ae6e758f05a3baea0b68eb1ddcb679", size = 1766177, upload-time = "2025-07-27T21:21:42.224Z" }, { url = "https://files.pythonhosted.org/packages/28/ef/b43280767ebcde022ba31f1e9902137655a956ae30e920d75630fa67e36e/cramjam-2.11.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:40a75b95e05e38a2a055b2446f09994ce1139151721659315151d4ad6289bbff", size = 1854031, upload-time = "2025-07-27T21:21:43.651Z" }, { url = "https://files.pythonhosted.org/packages/60/1c/79d522757c494dfd9e9b208b0604cc7e97b481483cc477144f5705a06ab7/cramjam-2.11.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5d042c376d2025300da37d65192d06a457918b63b31140f697f85fd8e310b29", size = 2035812, upload-time = "2025-07-27T21:21:45.473Z" }, { url = "https://files.pythonhosted.org/packages/c8/70/3bf0670380069b3abd4c6b53f61d3148f4e08935569c08efbeaf7550e87d/cramjam-2.11.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb148b35ab20c75b19a06c27f05732e2a321adbd86fadc93f9466dbd7b1154a7", size = 2067661, upload-time = "2025-07-27T21:21:47.901Z" }, { url = "https://files.pythonhosted.org/packages/db/7e/4f6ca98a4b474348e965a529b359184785d1119ab7c4c9ec1280b8bea50a/cramjam-2.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ee47c220f0f5179ddc923ab91fc9e282c27b29fabc60c433dfe06f08084f798", size = 1981523, upload-time = "2025-07-27T21:21:49.704Z" }, { url = "https://files.pythonhosted.org/packages/8a/6c/b241511c7ffd5f1da29641429bb0e19b5fbcffafde5ba1bbcbf9394ea456/cramjam-2.11.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0cf1b5a81b21ea175c976c3ab09e00494258f4b49b7995efc86060cced3f0b2e", size = 2034251, upload-time = "2025-07-27T21:21:51.252Z" }, { url = "https://files.pythonhosted.org/packages/14/5c/4ef926c8c3c1bf6da96f9c53450ff334cdb6d0fc1efced0aea97e2090803/cramjam-2.11.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:360c00338ecf48921492455007f904be607fc7818de3d681acbcc542aae2fb36", size = 2155322, upload-time = "2025-07-27T21:21:53.348Z" }, { url = "https://files.pythonhosted.org/packages/be/fb/eb2aef7fb2730e56c5a2c9000817ee8fb4a95c92f19cc6e441afed42ec29/cramjam-2.11.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f31fcc0d30dc3f3e94ea6b4d8e1a855071757c6abf6a7b1e284050ab7d4c299c", size = 2169094, upload-time = "2025-07-27T21:21:55.187Z" }, { url = "https://files.pythonhosted.org/packages/3b/80/925a5c668dcee1c6f61775067185c5dc9a63c766d5393e5c60d2af4217a7/cramjam-2.11.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:033be66fdceb3d63b2c99b257a98380c4ec22c9e4dca54a2bfec3718cd24e184", size = 2159089, upload-time = "2025-07-27T21:21:57.118Z" }, { url = "https://files.pythonhosted.org/packages/1f/ac/b2819640eef0592a6de7ca832c0d23c69bd1620f765ce88b60dbc8da9ba2/cramjam-2.11.0-cp310-cp310-win32.whl", hash = "sha256:1c6cea67f6000b81f6bd27d14c8a6f62d00336ca7252fd03ee16f6b70eb5c0d2", size = 1605046, upload-time = "2025-07-27T21:21:58.617Z" }, { url = "https://files.pythonhosted.org/packages/5a/f4/06af04727b9556721049e2127656d727306d275c518e3d97f9ed4cffd0d8/cramjam-2.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:98aa4a351b047b0f7f9e971585982065028adc2c162c5c23c5d5734c5ccc1077", size = 1710647, upload-time = "2025-07-27T21:22:00.279Z" }, { url = "https://files.pythonhosted.org/packages/d0/89/8001f6a9b6b6e9fa69bec5319789083475d6f26d52aaea209d3ebf939284/cramjam-2.11.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:04cfa39118570e70e920a9b75c733299784b6d269733dbc791d9aaed6edd2615", size = 3559272, upload-time = "2025-07-27T21:22:01.988Z" }, { url = "https://files.pythonhosted.org/packages/0b/f3/001d00070ca92e5fbe6aacc768e455568b0cde46b0eb944561a4ea132300/cramjam-2.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:66a18f68506290349a256375d7aa2f645b9f7993c10fc4cc211db214e4e61d2b", size = 1861743, upload-time = "2025-07-27T21:22:03.754Z" }, { url = "https://files.pythonhosted.org/packages/c9/35/041a3af01bf3f6158f120070f798546d4383b962b63c35cd91dcbf193e17/cramjam-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:50e7d65533857736cd56f6509cf2c4866f28ad84dd15b5bdbf2f8a81e77fa28a", size = 1699631, upload-time = "2025-07-27T21:22:05.192Z" }, { url = "https://files.pythonhosted.org/packages/17/eb/5358b238808abebd0c949c42635c3751204ca7cf82b29b984abe9f5e33c8/cramjam-2.11.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1f71989668458fc327ac15396db28d92df22f8024bb12963929798b2729d2df5", size = 2025603, upload-time = "2025-07-27T21:22:06.726Z" }, { url = "https://files.pythonhosted.org/packages/0e/79/19dba7c03a27408d8d11b5a7a4a7908459cfd4e6f375b73264dc66517bf6/cramjam-2.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee77ac543f1e2b22af1e8be3ae589f729491b6090582340aacd77d1d757d9569", size = 1766283, upload-time = "2025-07-27T21:22:08.568Z" }, { url = "https://files.pythonhosted.org/packages/a4/ad/40e4b3408501d886d082db465c33971655fe82573c535428e52ab905f4d0/cramjam-2.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad52784120e7e4d8a0b5b0517d185b8bf7f74f5e17272857ddc8951a628d9be1", size = 1854407, upload-time = "2025-07-27T21:22:10.518Z" }, { url = "https://files.pythonhosted.org/packages/36/6e/c1b60ceb6d7ea6ff8b0bf197520aefe23f878bf2bfb0de65f2b0c2f82cd1/cramjam-2.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b86f8e6d9c1b3f9a75b2af870c93ceee0f1b827cd2507387540e053b35d7459", size = 2035793, upload-time = "2025-07-27T21:22:12.504Z" }, { url = "https://files.pythonhosted.org/packages/9c/ad/32a8d5f4b1e3717787945ec6d71bd1c6e6bccba4b7e903fc0d9d4e4b08c3/cramjam-2.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:320d61938950d95da2371b46c406ec433e7955fae9f396c8e1bf148ffc187d11", size = 2067499, upload-time = "2025-07-27T21:22:14.067Z" }, { url = "https://files.pythonhosted.org/packages/ff/cd/3b5a662736ea62ff7fa4c4a10a85e050bfdaad375cc53dc80427e8afe41c/cramjam-2.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41eafc8c1653a35a5c7e75ad48138f9f60085cc05cd99d592e5298552d944e9f", size = 1981853, upload-time = "2025-07-27T21:22:15.908Z" }, { url = "https://files.pythonhosted.org/packages/26/8e/1dbcfaaa7a702ee82ee683ec3a81656934dd7e04a7bc4ee854033686f98a/cramjam-2.11.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03a7316c6bf763dfa34279335b27702321da44c455a64de58112968c0818ec4a", size = 2034514, upload-time = "2025-07-27T21:22:17.352Z" }, { url = "https://files.pythonhosted.org/packages/50/62/f11709bfdce74af79a88b410dcb76dedc97612166e759136931bf63cfd7b/cramjam-2.11.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:244c2ed8bd7ccbb294a2abe7ca6498db7e89d7eb5e744691dc511a7dc82e65ca", size = 2155343, upload-time = "2025-07-27T21:22:18.854Z" }, { url = "https://files.pythonhosted.org/packages/8a/6d/3b98b61841a5376d9a9b8468ae58753a8e6cf22be9534a0fa5af4d8621cc/cramjam-2.11.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:405f8790bad36ce0b4bbdb964ad51507bfc7942c78447f25cb828b870a1d86a0", size = 2169367, upload-time = "2025-07-27T21:22:20.389Z" }, { url = "https://files.pythonhosted.org/packages/11/72/bd5db5c49dbebc8b002f1c4983101b28d2e7fc9419753db1c31ec22b03ef/cramjam-2.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6b1b751a5411032b08fb3ac556160229ca01c6bbe4757bb3a9a40b951ebaac23", size = 2159334, upload-time = "2025-07-27T21:22:22.254Z" }, { url = "https://files.pythonhosted.org/packages/34/32/203c57acdb6eea727e7078b2219984e64ed4ad043c996ed56321301ba167/cramjam-2.11.0-cp311-cp311-win32.whl", hash = "sha256:5251585608778b9ac8effed544933df7ad85b4ba21ee9738b551f17798b215ac", size = 1605313, upload-time = "2025-07-27T21:22:24.126Z" }, { url = "https://files.pythonhosted.org/packages/a9/bd/102d6deb87a8524ac11cddcd31a7612b8f20bf9b473c3c645045e3b957c7/cramjam-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:dca88bc8b68ce6d35dafd8c4d5d59a238a56c43fa02b74c2ce5f9dfb0d1ccb46", size = 1710991, upload-time = "2025-07-27T21:22:25.661Z" }, { url = "https://files.pythonhosted.org/packages/0b/0d/7c84c913a5fae85b773a9dcf8874390f9d68ba0fcc6630efa7ff1541b950/cramjam-2.11.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dba5c14b8b4f73ea1e65720f5a3fe4280c1d27761238378be8274135c60bbc6e", size = 3553368, upload-time = "2025-07-27T21:22:27.162Z" }, { url = "https://files.pythonhosted.org/packages/2b/cc/4f6d185d8a744776f53035e72831ff8eefc2354f46ab836f4bd3c4f6c138/cramjam-2.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:11eb40722b3fcf3e6890fba46c711bf60f8dc26360a24876c85e52d76c33b25b", size = 1860014, upload-time = "2025-07-27T21:22:28.738Z" }, { url = "https://files.pythonhosted.org/packages/1c/a8/626c76263085c6d5ded0e71823b411e9522bfc93ba6cc59855a5869296e7/cramjam-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aeb26e2898994b6e8319f19a4d37c481512acdcc6d30e1b5ecc9d8ec57e835cb", size = 1693512, upload-time = "2025-07-27T21:22:30.999Z" }, { url = "https://files.pythonhosted.org/packages/e9/52/0851a16a62447532e30ba95a80e638926fdea869a34b4b5b9d0a020083ba/cramjam-2.11.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f8d82081ed7d8fe52c982bd1f06e4c7631a73fe1fb6d4b3b3f2404f87dc40fe", size = 2025285, upload-time = "2025-07-27T21:22:32.954Z" }, { url = "https://files.pythonhosted.org/packages/98/76/122e444f59dbc216451d8e3d8282c9665dc79eaf822f5f1470066be1b695/cramjam-2.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:092a3ec26e0a679305018380e4f652eae1b6dfe3fc3b154ee76aa6b92221a17c", size = 1761327, upload-time = "2025-07-27T21:22:34.484Z" }, { url = "https://files.pythonhosted.org/packages/a3/bc/3a0189aef1af2b29632c039c19a7a1b752bc21a4053582a5464183a0ad3d/cramjam-2.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:529d6d667c65fd105d10bd83d1cd3f9869f8fd6c66efac9415c1812281196a92", size = 1854075, upload-time = "2025-07-27T21:22:36.157Z" }, { url = "https://files.pythonhosted.org/packages/2e/80/8a6343b13778ce52d94bb8d5365a30c3aa951276b1857201fe79d7e2ad25/cramjam-2.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:555eb9c90c450e0f76e27d9ff064e64a8b8c6478ab1a5594c91b7bc5c82fd9f0", size = 2032710, upload-time = "2025-07-27T21:22:38.17Z" }, { url = "https://files.pythonhosted.org/packages/df/6b/cd1778a207c29eda10791e3dfa018b588001928086e179fc71254793c625/cramjam-2.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5edf4c9e32493035b514cf2ba0c969d81ccb31de63bd05490cc8bfe3b431674e", size = 2068353, upload-time = "2025-07-27T21:22:39.615Z" }, { url = "https://files.pythonhosted.org/packages/dc/f0/5c2a5cd5711032f3b191ca50cb786c17689b4a9255f9f768866e6c9f04d9/cramjam-2.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa2fe41f48c4d58d923803383b0737f048918b5a0d10390de9628bb6272b107", size = 1978104, upload-time = "2025-07-27T21:22:41.106Z" }, { url = "https://files.pythonhosted.org/packages/f9/8b/b363a5fb2c3347504fe9a64f8d0f1e276844f0e532aa7162c061cd1ffee4/cramjam-2.11.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9ca14cf1cabdb0b77d606db1bb9e9ca593b1dbd421fcaf251ec9a5431ec449f3", size = 2030779, upload-time = "2025-07-27T21:22:42.969Z" }, { url = "https://files.pythonhosted.org/packages/78/7b/d83dad46adb6c988a74361f81ad9c5c22642be53ad88616a19baedd06243/cramjam-2.11.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:309e95bf898829476bccf4fd2c358ec00e7ff73a12f95a3cdeeba4bb1d3683d5", size = 2155297, upload-time = "2025-07-27T21:22:44.6Z" }, { url = "https://files.pythonhosted.org/packages/1a/be/60d9be4cb33d8740a4aa94c7513f2ef3c4eba4fd13536f086facbafade71/cramjam-2.11.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:86dca35d2f15ef22922411496c220f3c9e315d5512f316fe417461971cc1648d", size = 2169255, upload-time = "2025-07-27T21:22:46.534Z" }, { url = "https://files.pythonhosted.org/packages/11/b0/4a595f01a243aec8ad272b160b161c44351190c35d98d7787919d962e9e5/cramjam-2.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:193c6488bd2f514cbc0bef5c18fad61a5f9c8d059dd56edf773b3b37f0e85496", size = 2155651, upload-time = "2025-07-27T21:22:48.46Z" }, { url = "https://files.pythonhosted.org/packages/38/47/7776659aaa677046b77f527106e53ddd47373416d8fcdb1e1a881ec5dc06/cramjam-2.11.0-cp312-cp312-win32.whl", hash = "sha256:514e2c008a8b4fa823122ca3ecab896eac41d9aa0f5fc881bd6264486c204e32", size = 1603568, upload-time = "2025-07-27T21:22:50.084Z" }, { url = "https://files.pythonhosted.org/packages/75/b1/d53002729cfd94c5844ddfaf1233c86d29f2dbfc1b764a6562c41c044199/cramjam-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:53fed080476d5f6ad7505883ec5d1ec28ba36c2273db3b3e92d7224fe5e463db", size = 1709287, upload-time = "2025-07-27T21:22:51.534Z" }, { url = "https://files.pythonhosted.org/packages/0a/8b/406c5dc0f8e82385519d8c299c40fd6a56d97eca3fcd6f5da8dad48de75b/cramjam-2.11.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2c289729cc1c04e88bafa48b51082fb462b0a57dbc96494eab2be9b14dca62af", size = 3553330, upload-time = "2025-07-27T21:22:53.124Z" }, { url = "https://files.pythonhosted.org/packages/00/ad/4186884083d6e4125b285903e17841827ab0d6d0cffc86216d27ed91e91d/cramjam-2.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:045201ee17147e36cf43d8ae2fa4b4836944ac672df5874579b81cf6d40f1a1f", size = 1859756, upload-time = "2025-07-27T21:22:54.821Z" }, { url = "https://files.pythonhosted.org/packages/54/01/91b485cf76a7efef638151e8a7d35784dae2c4ff221b1aec2c083e4b106d/cramjam-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:619cd195d74c9e1d2a3ad78d63451d35379c84bd851aec552811e30842e1c67a", size = 1693609, upload-time = "2025-07-27T21:22:56.331Z" }, { url = "https://files.pythonhosted.org/packages/cd/84/d0c80d279b2976870fc7d10f15dcb90a3c10c06566c6964b37c152694974/cramjam-2.11.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6eb3ae5ab72edb2ed68bdc0f5710f0a6cad7fd778a610ec2c31ee15e32d3921e", size = 2024912, upload-time = "2025-07-27T21:22:57.915Z" }, { url = "https://files.pythonhosted.org/packages/d6/70/88f2a5cb904281ed5d3c111b8f7d5366639817a5470f059bcd26833fc870/cramjam-2.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df7da3f4b19e3078f9635f132d31b0a8196accb2576e3213ddd7a77f93317c20", size = 1760715, upload-time = "2025-07-27T21:22:59.528Z" }, { url = "https://files.pythonhosted.org/packages/b2/06/cf5b02081132537d28964fb385fcef9ed9f8a017dd7d8c59d317e53ba50d/cramjam-2.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57286b289cd557ac76c24479d8ecfb6c3d5b854cce54ccc7671f9a2f5e2a2708", size = 1853782, upload-time = "2025-07-27T21:23:01.07Z" }, { url = "https://files.pythonhosted.org/packages/57/27/63525087ed40a53d1867021b9c4858b80cc86274ffe7225deed067d88d92/cramjam-2.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28952fbbf8b32c0cb7fa4be9bcccfca734bf0d0989f4b509dc7f2f70ba79ae06", size = 2032354, upload-time = "2025-07-27T21:23:03.021Z" }, { url = "https://files.pythonhosted.org/packages/c3/ef/dbba082c6ebfb6410da4dd39a64e654d7194fcfd4567f85991a83fa4ec32/cramjam-2.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78ed2e4099812a438b545dfbca1928ec825e743cd253bc820372d6ef8c3adff4", size = 2068007, upload-time = "2025-07-27T21:23:04.526Z" }, { url = "https://files.pythonhosted.org/packages/35/ce/d902b9358a46a086938feae83b2251720e030f06e46006f4c1fc0ac9da20/cramjam-2.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9aecd5c3845d415bd6c9957c93de8d93097e269137c2ecb0e5a5256374bdc8", size = 1977485, upload-time = "2025-07-27T21:23:06.058Z" }, { url = "https://files.pythonhosted.org/packages/e8/03/982f54553244b0afcbdb2ad2065d460f0ab05a72a96896a969a1ca136a1e/cramjam-2.11.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:362fcf4d6f5e1242a4540812455f5a594949190f6fbc04f2ffbfd7ae0266d788", size = 2030447, upload-time = "2025-07-27T21:23:07.679Z" }, { url = "https://files.pythonhosted.org/packages/74/5f/748e54cdb665ec098ec519e23caacc65fc5ae58718183b071e33fc1c45b4/cramjam-2.11.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:13240b3dea41b1174456cb9426843b085dc1a2bdcecd9ee2d8f65ac5703374b0", size = 2154949, upload-time = "2025-07-27T21:23:09.366Z" }, { url = "https://files.pythonhosted.org/packages/69/81/c4e6cb06ed69db0dc81f9a8b1dc74995ebd4351e7a1877143f7031ff2700/cramjam-2.11.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:c54eed83726269594b9086d827decc7d2015696e31b99bf9b69b12d9063584fe", size = 2168925, upload-time = "2025-07-27T21:23:10.976Z" }, { url = "https://files.pythonhosted.org/packages/13/5b/966365523ce8290a08e163e3b489626c5adacdff2b3da9da1b0823dfb14e/cramjam-2.11.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f8195006fdd0fc0a85b19df3d64a3ef8a240e483ae1dfc7ac6a4316019eb5df2", size = 2154950, upload-time = "2025-07-27T21:23:12.514Z" }, { url = "https://files.pythonhosted.org/packages/3a/7d/7f8eb5c534b72b32c6eb79d74585bfee44a9a5647a14040bb65c31c2572d/cramjam-2.11.0-cp313-cp313-win32.whl", hash = "sha256:ccf30e3fe6d770a803dcdf3bb863fa44ba5dc2664d4610ba2746a3c73599f2e4", size = 1603199, upload-time = "2025-07-27T21:23:14.38Z" }, { url = "https://files.pythonhosted.org/packages/37/05/47b5e0bf7c41a3b1cdd3b7c2147f880c93226a6bef1f5d85183040cbdece/cramjam-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:ee36348a204f0a68b03400f4736224e9f61d1c6a1582d7f875c1ca56f0254268", size = 1708924, upload-time = "2025-07-27T21:23:16.332Z" }, { url = "https://files.pythonhosted.org/packages/de/07/a1051cdbbe6d723df16d756b97f09da7c1adb69e29695c58f0392bc12515/cramjam-2.11.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7ba5e38c9fbd06f086f4a5a64a1a5b7b417cd3f8fc07a20e5c03651f72f36100", size = 3554141, upload-time = "2025-07-27T21:23:17.938Z" }, { url = "https://files.pythonhosted.org/packages/74/66/58487d2e16ef3d04f51a7c7f0e69823e806744b4c21101e89da4873074bc/cramjam-2.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8adeee57b41fe08e4520698a4b0bd3cc76dbd81f99424b806d70a5256a391d3", size = 1860353, upload-time = "2025-07-27T21:23:19.593Z" }, { url = "https://files.pythonhosted.org/packages/67/b4/67f6254d166ffbcc9d5fa1b56876eaa920c32ebc8e9d3d525b27296b693b/cramjam-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b96a74fa03a636c8a7d76f700d50e9a8bc17a516d6a72d28711225d641e30968", size = 1693832, upload-time = "2025-07-27T21:23:21.185Z" }, { url = "https://files.pythonhosted.org/packages/55/a3/4e0b31c0d454ae70c04684ed7c13d3c67b4c31790c278c1e788cb804fa4a/cramjam-2.11.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c3811a56fa32e00b377ef79121c0193311fd7501f0fb378f254c7f083cc1fbe0", size = 2027080, upload-time = "2025-07-27T21:23:23.303Z" }, { url = "https://files.pythonhosted.org/packages/d9/c7/5e8eed361d1d3b8be14f38a54852c5370cc0ceb2c2d543b8ba590c34f080/cramjam-2.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5d927e87461f8a0d448e4ab5eb2bca9f31ca5d8ea86d70c6f470bb5bc666d7e", size = 1761543, upload-time = "2025-07-27T21:23:24.991Z" }, { url = "https://files.pythonhosted.org/packages/09/0c/06b7f8b0ce9fde89470505116a01fc0b6cb92d406c4fb1e46f168b5d3fa5/cramjam-2.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f1f5c450121430fd89cb5767e0a9728ecc65997768fd4027d069cb0368af62f9", size = 1854636, upload-time = "2025-07-27T21:23:26.987Z" }, { url = "https://files.pythonhosted.org/packages/6f/c6/6ebc02c9d5acdf4e5f2b1ec6e1252bd5feee25762246798ae823b3347457/cramjam-2.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:724aa7490be50235d97f07e2ca10067927c5d7f336b786ddbc868470e822aa25", size = 2032715, upload-time = "2025-07-27T21:23:28.603Z" }, { url = "https://files.pythonhosted.org/packages/a2/77/a122971c23f5ca4b53e4322c647ac7554626c95978f92d19419315dddd05/cramjam-2.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54c4637122e7cfd7aac5c1d3d4c02364f446d6923ea34cf9d0e8816d6e7a4936", size = 2069039, upload-time = "2025-07-27T21:23:30.319Z" }, { url = "https://files.pythonhosted.org/packages/19/0f/f6121b90b86b9093c066889274d26a1de3f29969d45c2ed1ecbe2033cb78/cramjam-2.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17eb39b1696179fb471eea2de958fa21f40a2cd8bf6b40d428312d5541e19dc4", size = 1979566, upload-time = "2025-07-27T21:23:32.002Z" }, { url = "https://files.pythonhosted.org/packages/e0/a3/f95bc57fd7f4166ce6da816cfa917fb7df4bb80e669eb459d85586498414/cramjam-2.11.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:36aa5a798aa34e11813a80425a30d8e052d8de4a28f27bfc0368cfc454d1b403", size = 2030905, upload-time = "2025-07-27T21:23:33.696Z" }, { url = "https://files.pythonhosted.org/packages/fc/52/e429de4e8bc86ee65e090dae0f87f45abd271742c63fb2d03c522ffde28a/cramjam-2.11.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:449fca52774dc0199545fbf11f5128933e5a6833946707885cf7be8018017839", size = 2155592, upload-time = "2025-07-27T21:23:35.375Z" }, { url = "https://files.pythonhosted.org/packages/6c/6c/65a7a0207787ad39ad804af4da7f06a60149de19481d73d270b540657234/cramjam-2.11.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:d87d37b3d476f4f7623c56a232045d25bd9b988314702ea01bd9b4a94948a778", size = 2170839, upload-time = "2025-07-27T21:23:37.197Z" }, { url = "https://files.pythonhosted.org/packages/b2/c5/5c5db505ba692bc844246b066e23901d5905a32baf2f33719c620e65887f/cramjam-2.11.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:26cb45c47d71982d76282e303931c6dd4baee1753e5d48f9a89b3a63e690b3a3", size = 2157236, upload-time = "2025-07-27T21:23:38.854Z" }, { url = "https://files.pythonhosted.org/packages/b0/22/88e6693e60afe98901e5bbe91b8dea193e3aa7f42e2770f9c3339f5c1065/cramjam-2.11.0-cp314-cp314-win32.whl", hash = "sha256:4efe919d443c2fd112fe25fe636a52f9628250c9a50d9bddb0488d8a6c09acc6", size = 1604136, upload-time = "2025-07-27T21:23:40.56Z" }, { url = "https://files.pythonhosted.org/packages/cc/f8/01618801cd59ccedcc99f0f96d20be67d8cfc3497da9ccaaad6b481781dd/cramjam-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ccec3524ea41b9abd5600e3e27001fd774199dbb4f7b9cb248fcee37d4bda84c", size = 1710272, upload-time = "2025-07-27T21:23:42.236Z" }, { url = "https://files.pythonhosted.org/packages/40/81/6cdb3ed222d13ae86bda77aafe8d50566e81a1169d49ed195b6263610704/cramjam-2.11.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:966ac9358b23d21ecd895c418c048e806fd254e46d09b1ff0cdad2eba195ea3e", size = 3559671, upload-time = "2025-07-27T21:23:44.504Z" }, { url = "https://files.pythonhosted.org/packages/cb/43/52b7e54fe5ba1ef0270d9fdc43dabd7971f70ea2d7179be918c997820247/cramjam-2.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:387f09d647a0d38dcb4539f8a14281f8eb6bb1d3e023471eb18a5974b2121c86", size = 1867876, upload-time = "2025-07-27T21:23:46.987Z" }, { url = "https://files.pythonhosted.org/packages/9d/28/30d5b8d10acd30db3193bc562a313bff722888eaa45cfe32aa09389f2b24/cramjam-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:665b0d8fbbb1a7f300265b43926457ec78385200133e41fef19d85790fc1e800", size = 1695562, upload-time = "2025-07-27T21:23:48.644Z" }, { url = "https://files.pythonhosted.org/packages/d9/86/ec806f986e01b896a650655024ea52a13e25c3ac8a3a382f493089483cdc/cramjam-2.11.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ca905387c7a371531b9622d93471be4d745ef715f2890c3702479cd4fc85aa51", size = 2025056, upload-time = "2025-07-27T21:23:50.404Z" }, { url = "https://files.pythonhosted.org/packages/09/43/c2c17586b90848d29d63181f7d14b8bd3a7d00975ad46e3edf2af8af7e1f/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c1aa56aef2c8af55a21ed39040a94a12b53fb23beea290f94d19a76027e2ffb", size = 1764084, upload-time = "2025-07-27T21:23:52.265Z" }, { url = "https://files.pythonhosted.org/packages/2b/a9/68bc334fadb434a61df10071dc8606702aa4f5b6cdb2df62474fc21d2845/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5db59c1cdfaa2ab85cc988e602d6919495f735ca8a5fd7603608eb1e23c26d5", size = 1854859, upload-time = "2025-07-27T21:23:54.085Z" }, { url = "https://files.pythonhosted.org/packages/5b/4e/b48e67835b5811ec5e9cb2e2bcba9c3fd76dab3e732569fe801b542c6ca9/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1f893014f00fe5e89a660a032e813bf9f6d91de74cd1490cdb13b2b59d0c9a3", size = 2035970, upload-time = "2025-07-27T21:23:55.758Z" }, { url = "https://files.pythonhosted.org/packages/c4/70/d2ac33d572b4d90f7f0f2c8a1d60fb48f06b128fdc2c05f9b49891bb0279/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c26a1eb487947010f5de24943bd7c422dad955b2b0f8650762539778c380ca89", size = 2069320, upload-time = "2025-07-27T21:23:57.494Z" }, { url = "https://files.pythonhosted.org/packages/1d/4c/85cec77af4a74308ba5fca8e296c4e2f80ec465c537afc7ab1e0ca2f9a00/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d5c8bfb438d94e7b892d1426da5fc4b4a5370cc360df9b8d9d77c33b896c37e", size = 1982668, upload-time = "2025-07-27T21:23:59.126Z" }, { url = "https://files.pythonhosted.org/packages/55/45/938546d1629e008cc3138df7c424ef892719b1796ff408a2ab8550032e5e/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:cb1fb8c9337ab0da25a01c05d69a0463209c347f16512ac43be5986f3d1ebaf4", size = 2034028, upload-time = "2025-07-27T21:24:00.865Z" }, { url = "https://files.pythonhosted.org/packages/01/76/b5a53e20505555f1640e66dcf70394bcf51a1a3a072aa18ea35135a0f9ed/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:1f6449f6de52dde3e2f1038284910c8765a397a25e2d05083870f3f5e7fc682c", size = 2155513, upload-time = "2025-07-27T21:24:02.92Z" }, { url = "https://files.pythonhosted.org/packages/84/12/8d3f6ceefae81bbe45a347fdfa2219d9f3ac75ebc304f92cd5fcb4fbddc5/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_i686.whl", hash = "sha256:382dec4f996be48ed9c6958d4e30c2b89435d7c2c4dbf32480b3b8886293dd65", size = 2170035, upload-time = "2025-07-27T21:24:04.558Z" }, { url = "https://files.pythonhosted.org/packages/4b/85/3be6f0a1398f976070672be64f61895f8839857618a2d8cc0d3ab529d3dc/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:d388bd5723732c3afe1dd1d181e4213cc4e1be210b080572e7d5749f6e955656", size = 2160229, upload-time = "2025-07-27T21:24:06.729Z" }, { url = "https://files.pythonhosted.org/packages/57/5e/66cfc3635511b20014bbb3f2ecf0095efb3049e9e96a4a9e478e4f3d7b78/cramjam-2.11.0-cp314-cp314t-win32.whl", hash = "sha256:0a70ff17f8e1d13f322df616505550f0f4c39eda62290acb56f069d4857037c8", size = 1610267, upload-time = "2025-07-27T21:24:08.428Z" }, { url = "https://files.pythonhosted.org/packages/ce/c6/c71e82e041c95ffe6a92ac707785500aa2a515a4339c2c7dd67e3c449249/cramjam-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:028400d699442d40dbda02f74158c73d05cb76587a12490d0bfedd958fd49188", size = 1713108, upload-time = "2025-07-27T21:24:10.147Z" }, { url = "https://files.pythonhosted.org/packages/bf/8f/82e35ec3c5387f1864f46b3c24bce89a07af8bb3ef242ae47281db2c1848/cramjam-2.11.0-pp310-pypy310_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:37bed927abc4a7ae2d2669baa3675e21904d8a038ed8e4313326ea7b3be62b2b", size = 3573104, upload-time = "2025-07-27T21:24:40.069Z" }, { url = "https://files.pythonhosted.org/packages/f0/4e/0c821918080a32ba1e52c040e12dd02dada67728f07305c5f778b808a807/cramjam-2.11.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:50e4a58635fa8c6897d84847d6e065eb69f92811670fc5e9f2d9e3b6279a02b6", size = 1873441, upload-time = "2025-07-27T21:24:42.333Z" }, { url = "https://files.pythonhosted.org/packages/a8/fd/848d077bf6abc4ce84273d8e3f3a70d61a2240519a339462f699d8acf829/cramjam-2.11.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d1ba626dd5f81f7f09bbf59f70b534e2b75e0d6582b056b7bd31b397f1c13e9", size = 1702589, upload-time = "2025-07-27T21:24:44.305Z" }, { url = "https://files.pythonhosted.org/packages/9d/1c/899818999bbdb59c601756b413e87d37fd65875d1315346c10e367bb3505/cramjam-2.11.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c71e140d5eb3145d61d59d0be0bf72f07cc4cf4b32cb136b09f712a3b1040f5f", size = 1773646, upload-time = "2025-07-27T21:24:46.495Z" }, { url = "https://files.pythonhosted.org/packages/5f/26/c2813c5422c43b3dcd8b6645bc359f08870737c44325ee4accc18f24eee0/cramjam-2.11.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a6ed7926a5cca28edebad7d0fedd2ad492710ae3524d25fc59a2b20546d9ce1", size = 1994179, upload-time = "2025-07-27T21:24:49.131Z" }, { url = "https://files.pythonhosted.org/packages/2e/4f/af984f8d7f963f0301812cdd620ddcfd8276461ed7a786c0f89e82b14739/cramjam-2.11.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5eb4ed3cea945b164b0513fd491884993acac2153a27b93a84019c522e8eda82", size = 1714790, upload-time = "2025-07-27T21:24:51.045Z" }, { url = "https://files.pythonhosted.org/packages/81/da/b3301962ccd6fce9fefa1ecd8ea479edaeaa38fadb1f34d5391d2587216a/cramjam-2.11.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:52d5db3369f95b27b9f3c14d067acb0b183333613363ed34268c9e04560f997f", size = 3573546, upload-time = "2025-07-27T21:24:52.944Z" }, { url = "https://files.pythonhosted.org/packages/b6/c2/410ddb8ad4b9dfb129284666293cb6559479645da560f7077dc19d6bee9e/cramjam-2.11.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4820516366d455b549a44d0e2210ee7c4575882dda677564ce79092588321d54", size = 1873654, upload-time = "2025-07-27T21:24:54.958Z" }, { url = "https://files.pythonhosted.org/packages/d5/99/f68a443c64f7ce7aff5bed369b0aa5b2fac668fa3dfd441837e316e97a1f/cramjam-2.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d9e5db525dc0a950a825202f84ee68d89a072479e07da98795a3469df942d301", size = 1702846, upload-time = "2025-07-27T21:24:57.124Z" }, { url = "https://files.pythonhosted.org/packages/6c/02/0ff358ab773def1ee3383587906c453d289953171e9c92db84fdd01bf172/cramjam-2.11.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62ab4971199b2270005359cdc379bc5736071dc7c9a228581c5122d9ffaac50c", size = 1773683, upload-time = "2025-07-27T21:24:59.28Z" }, { url = "https://files.pythonhosted.org/packages/e9/31/3298e15f87c9cf2aabdbdd90b153d8644cf989cb42a45d68a1b71e1f7aaf/cramjam-2.11.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24758375cc5414d3035ca967ebb800e8f24604ececcba3c67d6f0218201ebf2d", size = 1994136, upload-time = "2025-07-27T21:25:01.565Z" }, { url = "https://files.pythonhosted.org/packages/c7/90/20d1747255f1ee69a412e319da51ea594c18cca195e7a4d4c713f045eff5/cramjam-2.11.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6c2eea545fef1065c7dd4eda991666fd9c783fbc1d226592ccca8d8891c02f23", size = 1714982, upload-time = "2025-07-27T21:25:05.79Z" }, ] [[package]] name = "cryptography" version = "50.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, { url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" }, { url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" }, { url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" }, { url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" }, { url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" }, { url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" }, ] [[package]] name = "deprecated" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wrapt" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, ] [[package]] name = "distlib" version = "0.4.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, ] [[package]] name = "distro" version = "1.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] [[package]] name = "django" version = "5.2.15" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.11.*'", "python_full_version < '3.11'", ] dependencies = [ { name = "asgiref" }, { name = "sqlparse" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2b/e3/31722f7284c9f43333daff9aee9184678e4487adcb5506af0db8cea09ce1/django-5.2.15.tar.gz", hash = "sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7", size = 10873669, upload-time = "2026-06-03T13:03:35.892Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/92/b5/38140b1643c00d5c46ce69c78e6980fd285aee223100319631bedee4f5e7/django-5.2.15-py3-none-any.whl", hash = "sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970", size = 8311957, upload-time = "2026-06-03T13:03:31.329Z" }, ] [[package]] name = "django" version = "6.0.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", "python_full_version == '3.12.*'", ] dependencies = [ { name = "asgiref" }, { name = "sqlparse" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/29/ac41e16097af67066d97a7d5775c5d8e7efc5d0284f6b0a159e07b9adb92/django-6.0.6.tar.gz", hash = "sha256:ad03916ba59523d781ae5c3f631960c23d69a9d9c43cecda52fc23b47e953713", size = 10905525, upload-time = "2026-06-03T13:02:46.503Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/eb/50/23f9dc45483419a3cc2085b498b25adfbf10642b2941c73e6d2dfaffc9ab/django-6.0.6-py3-none-any.whl", hash = "sha256:25148b1194c47c2e685e5f5e9c5d59c78b075dfd282cb9618861ba6c1708f4d2", size = 8373354, upload-time = "2026-06-03T13:02:41.72Z" }, ] [[package]] name = "dnspython" version = "2.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] [[package]] name = "docstring-parser" version = "0.18.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, ] [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] name = "falcon" version = "4.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a0/02/a51af369a4feded77801d744f2d4669c7fa0a294bc117acf6ee06439937b/falcon-4.3.1.tar.gz", hash = "sha256:b5081be62f1e2d4f5140f4bad6993fd2bf8b1e503ee7261f56d324b87e41a67a", size = 687415, upload-time = "2026-06-16T05:31:25.864Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2f/05/f620a7dcf22de682dbc3f976470a2f8f586bf8ea87bc14093619e00b2fd6/falcon-4.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee842e59de8bd3961bb64b3f922524069187be6d050f09a5e008a10c69952d62", size = 426392, upload-time = "2026-06-16T05:41:56.467Z" }, { url = "https://files.pythonhosted.org/packages/11/ad/9042ede1b1f96b4c3426f0ec0eba8eb70dccae84eae93db599368fb6098e/falcon-4.3.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04e3c32e124e7dad3e1aa499f5ad9e9cd47ec472de2a10340692700cb2543371", size = 830156, upload-time = "2026-06-16T05:41:58.244Z" }, { url = "https://files.pythonhosted.org/packages/3d/ce/d71d3eba8c90958af83e3399a22dacfe49bd9449ea1b8a72f4cb71c9e6f5/falcon-4.3.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d06262b828126f4bfbc2507286d237d69393ca7cca683c5ea9b86f13b65e2706", size = 862605, upload-time = "2026-06-16T05:41:59.731Z" }, { url = "https://files.pythonhosted.org/packages/33/77/dd15a34749d505350890b316ec1bd703251d17818f69da7707c8c3159f38/falcon-4.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:617cc212b3d6fd79685bb37541053043418bb2d5788189b1c5621f7d83b6efce", size = 831041, upload-time = "2026-06-16T05:42:01.435Z" }, { url = "https://files.pythonhosted.org/packages/9c/4f/a222885905418f61a9e2a259e53b3dc4b21b7957cb4e75966afd2314eeca/falcon-4.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e75b6f840fae3c74a81b8caca269e06a2a95e6681f2e459bb414aeae1926495a", size = 820220, upload-time = "2026-06-16T05:42:03.183Z" }, { url = "https://files.pythonhosted.org/packages/f0/72/27673348a076e85f215365231a706d1a6f03c2c9929485bbcdac97a17632/falcon-4.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:508bad80e00438e3f99fc60bf3e4dd8bb3aae3cf0a0ecec546074648cc7b35e2", size = 829199, upload-time = "2026-06-16T05:42:04.601Z" }, { url = "https://files.pythonhosted.org/packages/a6/fe/41f226f77d57681e758eae690d9d216ca5ce9add5ef633ca8a199842f79d/falcon-4.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:1639cbf6a49a8450a34ae43961f43e12ff0d50666412280a58d67ea4821e1da9", size = 415589, upload-time = "2026-06-16T05:42:06.125Z" }, { url = "https://files.pythonhosted.org/packages/5f/f9/c4d672e28d76ed6c1676fb21254ab70f3665d99d00c43fcd50d18547fbac/falcon-4.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a89d2cc1c004ee1840115dafc7ed0fee6f3f5bad1c9e7177edc6a844e296fd97", size = 425988, upload-time = "2026-06-16T05:42:07.509Z" }, { url = "https://files.pythonhosted.org/packages/3c/e7/851ad3104a0fd4c18254e76d05a1303a6afb9cd683da2548e538832b0c6c/falcon-4.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:703a1fc6de0a4d506e70854b84530b888ffa09f5af9a2a2f7a48e906809715a1", size = 856248, upload-time = "2026-06-16T05:42:08.95Z" }, { url = "https://files.pythonhosted.org/packages/20/fe/869b086a8546a0c48a2c8a85f08b902660dabb488bd613d199a53ff73fb2/falcon-4.3.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67919093747e6adc7decc84d4df1ee2d71d0d281929d733865948f94d419e279", size = 889614, upload-time = "2026-06-16T05:42:11.075Z" }, { url = "https://files.pythonhosted.org/packages/35/ed/1694fa4be4ef5fa5e969c67489c2086a719f51e8f4b129ef5fb4d6d34978/falcon-4.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54da8078000be4ed935a6b9a601be890d2d31d1d9cdb1d150a9950af71e895f2", size = 857364, upload-time = "2026-06-16T05:42:12.579Z" }, { url = "https://files.pythonhosted.org/packages/93/56/8c6e6a4cf011dca0acc1a9efc13a999c125c9e6305374ea4c86601ee4bd8/falcon-4.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9edc63eec91395ed7e04a7031a36abacbb87b9215b45711870c2fe0b399bd3e5", size = 847710, upload-time = "2026-06-16T05:42:14.157Z" }, { url = "https://files.pythonhosted.org/packages/b9/3d/063d0b40166ac54bf6a7a894a24d5c178fff575aed466209aace57e0458d/falcon-4.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d25a7762f38db5dfb2136841b7845d675117e989bd3e2b1356ffb1b7b5220ecc", size = 855102, upload-time = "2026-06-16T05:42:15.638Z" }, { url = "https://files.pythonhosted.org/packages/8d/ed/7da444afe0fc2d6051a813af5a56184301227343f991b69b58453f4b0a79/falcon-4.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:10b6822e9413cc968083c88aebc905de2ecdeab0f9ffaa6e76449a51ce9d3624", size = 415578, upload-time = "2026-06-16T05:42:17.464Z" }, { url = "https://files.pythonhosted.org/packages/95/15/80a2d45ed40963eda6fd4e37f636203de2c89ec31ce87d1a718abce9358c/falcon-4.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b8ade932d130c370ac53405f8d86f88d17017ba440a350ec62e8d5e69fef37a", size = 426167, upload-time = "2026-06-16T05:42:18.926Z" }, { url = "https://files.pythonhosted.org/packages/13/c3/865280fa52e2714d337e2b022938075b5306eb693fb734bd792cfad052ae/falcon-4.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b1c6604233d42bccdbf290680e08a3f2757ba826181bdc8a350afad39919d2a", size = 889515, upload-time = "2026-06-16T05:42:20.66Z" }, { url = "https://files.pythonhosted.org/packages/2b/69/38bba172e199de9bfdbdef7f013b37e18da40ad721e288cb73a160dbc6d2/falcon-4.3.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a813f35a0ae68b50358084fb6eedb28f42e04f1d0db0aa9e792d059804cbbdfa", size = 930249, upload-time = "2026-06-16T05:42:22.205Z" }, { url = "https://files.pythonhosted.org/packages/43/97/1890b8e9bbd5b8bba59059483e5588d2b31f3c31d58cd1d1784e422db6ae/falcon-4.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c15edd6f2a512aa01c5feb3a685ab0f4dd85df6a6d50ad624d0f95fb467c8e55", size = 898315, upload-time = "2026-06-16T05:42:23.866Z" }, { url = "https://files.pythonhosted.org/packages/b1/eb/3723f1fa828ae15d67a1954bb1128529cd870a192d2b59f8fe5eb0a4fec6/falcon-4.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bcd4c02401b5f969cdc2aa93b7f1d8b9067360bafe4e3b4d710293eddc50652", size = 877001, upload-time = "2026-06-16T05:42:25.471Z" }, { url = "https://files.pythonhosted.org/packages/c9/2b/62d0d7a4f119b9d5d3aea9244899731b691c6819da429779d29cf76c2af3/falcon-4.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:86c1f3ebcf321a81248fdb3c9d1dd468569740069a3ad584888b89ba799bdf77", size = 894499, upload-time = "2026-06-16T05:42:27.225Z" }, { url = "https://files.pythonhosted.org/packages/6c/5c/9f14f408d26c781ea640eda84053ad5a5699cf80dcbd3ca02632aa0f0304/falcon-4.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ab4c4df2784eda8d4a2089649fba0f0575a0989b608e23079009ec71a552b23a", size = 416597, upload-time = "2026-06-16T05:42:28.61Z" }, { url = "https://files.pythonhosted.org/packages/ed/d1/9e5c219a283ac39f41d88ae5ea3951537d373c10f3f500049706cd3087d9/falcon-4.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23ec85650482a719902321627258b8297c7ecc38227001ce602078d9a157481c", size = 425427, upload-time = "2026-06-16T05:42:30.251Z" }, { url = "https://files.pythonhosted.org/packages/13/db/034e0929c5af463dc1b96fde707b1d6dd55ff5224ec53ccc4341180b80f8/falcon-4.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0462c4f2125c5656826b4a4b4bbb262e77d9cdedfedfe03ababf2480e1a6f342", size = 870018, upload-time = "2026-06-16T05:42:31.699Z" }, { url = "https://files.pythonhosted.org/packages/15/99/82fda8e5da5953454c174b9570f0e6ff9e9a3d7f818e39d43c2ae6f864d3/falcon-4.3.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b9462c55ee6b83f6897361b711649c3981d3cc31dc90b1efca18e16bba4c872", size = 909674, upload-time = "2026-06-16T05:42:33.324Z" }, { url = "https://files.pythonhosted.org/packages/bb/f3/c930cf8309c4e46e50de8c29d5023d3fe8c307dbb9d146322e8ab9cab9c9/falcon-4.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ed2566579cda476dbceddc29d356c6bfd56501d9109ac4d3eacd1f994653b9ba", size = 880182, upload-time = "2026-06-16T05:42:34.853Z" }, { url = "https://files.pythonhosted.org/packages/d0/13/6e15639ac0e0e20940fa66087411e3e4b964f6da9c72f8a684c65d3d0774/falcon-4.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c99f7f8d50ac3240182ad2f5ef22dee1d1852762ba594c7029f7b65d804c2057", size = 859594, upload-time = "2026-06-16T05:42:36.395Z" }, { url = "https://files.pythonhosted.org/packages/b8/50/16605e42475dc363ec74007bc017262dcd06660ca5a6c3110caf1af2bd06/falcon-4.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ed6e8e2a89ea1fc5cb9ecddd488ea0c89321f852390a1be31ef44034005b565", size = 875601, upload-time = "2026-06-16T05:42:38.201Z" }, { url = "https://files.pythonhosted.org/packages/93/a0/f9f1d46d7ba5fa4de5bdb71fe1ca53690458a54909fdf1a98a97231c102c/falcon-4.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:3648849d0a0426d571ad846aa20a5cb20718db3b040447ffc6c4436de728a9a7", size = 415648, upload-time = "2026-06-16T05:42:39.95Z" }, { url = "https://files.pythonhosted.org/packages/99/54/68a589193349f7db5c25ccc0823b22e5395ad7325551e20590680eecbddf/falcon-4.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7baf12d82e48998efdb6a270d384ac18bd3ec12665ca4356498c35a707e37359", size = 426480, upload-time = "2026-06-16T05:42:41.517Z" }, { url = "https://files.pythonhosted.org/packages/9e/eb/7f711c2be80eb8a297727ab17ce5dfa013a3b6275c7d8a9b23a9a27e19f0/falcon-4.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:974a4f8efc0b33268ae56dd4379c4386bd7f7c8b3635c236a9289bea1e899c17", size = 872708, upload-time = "2026-06-16T05:42:43.174Z" }, { url = "https://files.pythonhosted.org/packages/4c/70/72d281b4181851e8e9f4bb8d2fd8bf3b35c3ecc269358b67bf24cb8f0388/falcon-4.3.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3504fa4c63d2ab892434b1375706b6f89d8ca73488cc1a69976562928d5a658f", size = 905775, upload-time = "2026-06-16T05:42:44.754Z" }, { url = "https://files.pythonhosted.org/packages/21/73/68bfa5e7db8a039a71e7c0e99c067aa192c4eeddaad8427c38a1ec4bb584/falcon-4.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:761c79d34480e3f9fe4cd89bc82fa554b8d54a56810f796e5cddc5b8314780a4", size = 875880, upload-time = "2026-06-16T05:42:46.391Z" }, { url = "https://files.pythonhosted.org/packages/0e/fa/fc02220e7ab96fb7fec86c72e5ba317732cebb3a088986d32584e64a0061/falcon-4.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:318837add4ec1ad04a28d507911985f2b08cfcb56b34b69eb87188bd98e017c0", size = 862812, upload-time = "2026-06-16T05:42:48.199Z" }, { url = "https://files.pythonhosted.org/packages/eb/07/aa1e2be6c63306cba060cec3500f047b041b4f26129ed352ee5a02f3c86a/falcon-4.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9449bc13e0f8ebaedf80a1ce2344d7935183d45680a9fd75057e40fa0ebc5409", size = 874413, upload-time = "2026-06-16T05:42:49.659Z" }, { url = "https://files.pythonhosted.org/packages/11/21/a9b295702898c688d7c0f5aaed4d33dcd26ac1f47683d27f55dcaccc9e1d/falcon-4.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:1d8d54db0fa01d232c05cc16e17dd69f76628789afac49beef85fcad61af1668", size = 417560, upload-time = "2026-06-16T05:42:51.248Z" }, { url = "https://files.pythonhosted.org/packages/92/7d/d5dd88d6afcf5dade4090115635d34a78fdacede7c8deff6636e8d3aa6d0/falcon-4.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b76706dcede435bbf6dcbbde8e63ce223454e379f9f0463d3d686c10a9b2c844", size = 987965, upload-time = "2026-06-16T05:42:53.064Z" }, { url = "https://files.pythonhosted.org/packages/f1/61/5caa5986123dc0378a0f296be251502543c55f7d98a06647ff050eb57772/falcon-4.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f4cdd80ffd112766e3e53a166b9f46218bebb3619dd2abeaf4e618386a0cfec", size = 973455, upload-time = "2026-06-16T05:42:55.671Z" }, { url = "https://files.pythonhosted.org/packages/30/f3/41a4c8fe25af0f7f84b437b87d2b654387bc2a1efcae8076c33079651a7d/falcon-4.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c78fdd7fa7c39f9e179c494716efb5c75b476938078e83a7ff6b247d461b64a", size = 960935, upload-time = "2026-06-16T05:42:57.346Z" }, { url = "https://files.pythonhosted.org/packages/59/da/99632e285b4622aa9687bac3e682270b09f9f857309158b9668582d71297/falcon-4.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2bb8cb5c4e5cfb7819169f3bd11683ca9ef57e0169f9fee0189d0a1b71af6b77", size = 965542, upload-time = "2026-06-16T05:42:58.991Z" }, { url = "https://files.pythonhosted.org/packages/da/ef/5790e74a7b39d2c085554f347be22a1882649a29a7a65e9c81bfaf9e8fc6/falcon-4.3.1-py3-none-any.whl", hash = "sha256:5402602d14bc426afc01c388538f2ad402ac4c8c2477415dfe972a2deee17248", size = 332719, upload-time = "2026-06-16T05:31:23.373Z" }, ] [[package]] name = "fastapi" version = "0.138.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0c/a9/9f8f7e00195c29836e9bf58bbbaf579e29878b8a67851efff93d9b6d4eb7/fastapi-0.138.2.tar.gz", hash = "sha256:6432359d067a432134620e7c5e4c6e5063e7f37815bbbbf20acef14b0d2e3fc8", size = 420423, upload-time = "2026-06-29T12:44:12.556Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f2/b3/38be2c074bdd0c986340db1d72d7b2321b805b1c5a68069aa00b5d31fd02/fastapi-0.138.2-py3-none-any.whl", hash = "sha256:db90c1ffb5517fba5d4a9f80e866daa008747e646310c9ce155c8c535f9d1615", size = 129271, upload-time = "2026-06-29T12:44:13.905Z" }, ] [[package]] name = "filelock" version = "3.29.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" }, ] [[package]] name = "flask" version = "3.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blinker" }, { name = "click" }, { name = "itsdangerous" }, { name = "jinja2" }, { name = "markupsafe" }, { name = "werkzeug" }, ] sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, ] [[package]] name = "frozenlist" version = "1.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] [[package]] name = "fsspec" version = "2026.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" }, ] [[package]] name = "geomet" version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2a/8c/dde022aa6747b114f6b14a7392871275dea8867e2bd26cddb80cc6d66620/geomet-1.1.0.tar.gz", hash = "sha256:51e92231a0ef6aaa63ac20c443377ba78a303fd2ecd179dc3567de79f3c11605", size = 28732, upload-time = "2023-11-14T15:43:36.764Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/af/90/3bc780df088d439714af8295196a4332a26559ae66fd99865e36f92efa9e/geomet-1.1.0-py3-none-any.whl", hash = "sha256:4372fe4e286a34acc6f2e9308284850bd8c4aa5bc12065e2abbd4995900db12f", size = 31522, upload-time = "2023-11-14T15:43:35.305Z" }, ] [[package]] name = "google-api-core" version = "2.31.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, { name = "googleapis-common-protos" }, { name = "proto-plus" }, { name = "protobuf" }, { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c6/22/155cadf1d49272a9cf48f3168c0f3874fa13397297e611a5ea00cd093880/google_api_core-2.31.0.tar.gz", hash = "sha256:2be84ee0f584c48e6bde1b36766e23348b361fb7e55e56135fc76ce1c397f9c2", size = 176492, upload-time = "2026-06-03T14:52:17.257Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/86/40/9bdbb60b03a332bd45acb8703da08bbc27d991d35286b62e42acc86d243a/google_api_core-2.31.0-py3-none-any.whl", hash = "sha256:ef79fb3784c71cbac89cbd03301ba0c8fb8ad2aa95d7f9204dd9628f7adf59ab", size = 173102, upload-time = "2026-06-03T14:51:26.729Z" }, ] [package.optional-dependencies] grpc = [ { name = "grpcio" }, { name = "grpcio-status" }, ] [[package]] name = "google-auth" version = "2.55.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/f3f4ac177c67bbee8fe8e88f2ab4f36af88c44a096e165c5217accf6e5d3/google_auth-2.55.1.tar.gz", hash = "sha256:fb2d9b730f2c9b8d326ec8d7222f21aef2ead15bf0513793d6442485d87af0a1", size = 349527, upload-time = "2026-06-25T23:39:27.182Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e8/1d/f6d3ca1ad0725f2e08a1c6915640748a52de2e66596160a4d53b010cccf0/google_auth-2.55.1-py3-none-any.whl", hash = "sha256:eada68dfd52b3b81191827601e2a0c3fa12540c818534b630ddc5355769c3995", size = 252349, upload-time = "2026-06-25T23:38:52.946Z" }, ] [package.optional-dependencies] pyopenssl = [ { name = "pyopenssl" }, ] requests = [ { name = "requests" }, ] [[package]] name = "google-cloud-aiplatform" version = "1.159.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "docstring-parser" }, { name = "google-api-core", extra = ["grpc"] }, { name = "google-auth" }, { name = "google-cloud-bigquery" }, { name = "google-cloud-resource-manager" }, { name = "google-cloud-storage" }, { name = "google-genai" }, { name = "packaging" }, { name = "proto-plus" }, { name = "protobuf" }, { name = "pydantic" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8d/fd/233c98fd7cbabb657d6fd96ed8cb8006614c89f122f10ce9da5b73d88e54/google_cloud_aiplatform-1.159.0.tar.gz", hash = "sha256:cfd0df4511d07385801e024d084ea47c0c5b0cf4af0d90dc48797b67ce49847a", size = 11169829, upload-time = "2026-06-30T22:55:48.06Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/99/71/169d3ee76d2c3aa89f2ca6cb613d42516ee8976dcb52c6199a1e9785f99a/google_cloud_aiplatform-1.159.0-py2.py3-none-any.whl", hash = "sha256:a2ee8eab3c50a66f4a3330948c4475531863a9085f0cdb907387f8084bf2d8fd", size = 9366792, upload-time = "2026-06-30T22:55:44.887Z" }, ] [[package]] name = "google-cloud-bigquery" version = "3.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, { name = "google-auth", extra = ["pyopenssl"] }, { name = "google-cloud-core" }, { name = "google-resumable-media" }, { name = "packaging" }, { name = "python-dateutil" }, { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/15/a2/be1cffbb1a9894ecf394ab0096b113ae9e4f73686595d906a9f3082b07c2/google_cloud_bigquery-3.42.1.tar.gz", hash = "sha256:3c6878f424fc2b21f0bb414ca7262abd008465575a7b7c44c552105278a18914", size = 515411, upload-time = "2026-06-22T23:21:05.884Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7a/1e/8b098aeb69ef87de5195d076ce6d82002c3ebf1920373d386ca7b6e63cfa/google_cloud_bigquery-3.42.1-py3-none-any.whl", hash = "sha256:159143c1b7248858f35549c09efea5d031c2883145623419ea7c505a50c0899a", size = 263814, upload-time = "2026-06-22T23:18:42.313Z" }, ] [[package]] name = "google-cloud-core" version = "2.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, { name = "google-auth" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/dd/1eef226e470369b26824a505c34482c0b493bc35fe8e0c6b003b5feca21a/google_cloud_core-2.6.0.tar.gz", hash = "sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83", size = 36001, upload-time = "2026-05-07T08:04:04.124Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl", hash = "sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e", size = 29390, upload-time = "2026-05-07T08:02:34.672Z" }, ] [[package]] name = "google-cloud-resource-manager" version = "1.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, { name = "google-auth" }, { name = "grpc-google-iam-v1" }, { name = "grpcio" }, { name = "proto-plus" }, { name = "protobuf" }, ] sdist = { url = "https://files.pythonhosted.org/packages/12/70/d467d93528905700826d26a48ad16ac39d4b36dd6efb5a86fb338efa81aa/google_cloud_resource_manager-1.18.0.tar.gz", hash = "sha256:db689f800a14c66d041196a7fbb8bb8aae8dc87f28c2929e101a5ec766b15512", size = 463674, upload-time = "2026-06-22T23:22:27.09Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3f/62/2dbecda08a32aa9d4e6909eba3e31731c2d4452687bb6364a8d7950aed74/google_cloud_resource_manager-1.18.0-py3-none-any.whl", hash = "sha256:69bab144acd75878ebe44b720903dc3d140cb7d3be3261eaa8bc81e48afaff33", size = 404211, upload-time = "2026-06-22T23:20:25.88Z" }, ] [[package]] name = "google-cloud-storage" version = "3.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, { name = "google-auth" }, { name = "google-cloud-core" }, { name = "google-crc32c" }, { name = "google-resumable-media" }, { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/72/86f94e1639a8bcd9d33e8e01b49afcaa1c3a13bda7683c681717e0901e15/google_cloud_storage-3.12.0.tar.gz", hash = "sha256:03ae9847c6babb368f35f054126b8a08cbc0e3266efb990eb17b9926a45cf3be", size = 17338620, upload-time = "2026-06-12T18:03:29.215Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1b/bd/a89eaebd2f9db5f92ddcc8e4f23c266be1dbd11058bb83451d8dd029f34c/google_cloud_storage-3.12.0-py3-none-any.whl", hash = "sha256:3880773754ddf7c27567b04e2a4d193950b6b99429f37b9097d873686e95b09c", size = 340605, upload-time = "2026-06-12T18:03:12.677Z" }, ] [[package]] name = "google-crc32c" version = "1.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/95/ac/6f7bc93886a823ab545948c2dd48143027b2355ad1944c7cf852b338dc91/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff", size = 31296, upload-time = "2025-12-16T00:19:07.261Z" }, { url = "https://files.pythonhosted.org/packages/f7/97/a5accde175dee985311d949cfcb1249dcbb290f5ec83c994ea733311948f/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288", size = 30870, upload-time = "2025-12-16T00:29:17.669Z" }, { url = "https://files.pythonhosted.org/packages/3d/63/bec827e70b7a0d4094e7476f863c0dbd6b5f0f1f91d9c9b32b76dcdfeb4e/google_crc32c-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d", size = 33214, upload-time = "2025-12-16T00:40:19.618Z" }, { url = "https://files.pythonhosted.org/packages/63/bc/11b70614df04c289128d782efc084b9035ef8466b3d0a8757c1b6f5cf7ac/google_crc32c-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092", size = 33589, upload-time = "2025-12-16T00:40:20.7Z" }, { url = "https://files.pythonhosted.org/packages/3e/00/a08a4bc24f1261cc5b0f47312d8aebfbe4b53c2e6307f1b595605eed246b/google_crc32c-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733", size = 34437, upload-time = "2025-12-16T00:35:19.437Z" }, { url = "https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8", size = 31298, upload-time = "2025-12-16T00:20:32.241Z" }, { url = "https://files.pythonhosted.org/packages/c5/b8/f8413d3f4b676136e965e764ceedec904fe38ae8de0cdc52a12d8eb1096e/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7", size = 30872, upload-time = "2025-12-16T00:33:58.785Z" }, { url = "https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15", size = 33243, upload-time = "2025-12-16T00:40:21.46Z" }, { url = "https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a", size = 33608, upload-time = "2025-12-16T00:40:22.204Z" }, { url = "https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2", size = 34439, upload-time = "2025-12-16T00:35:20.458Z" }, { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" }, { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" }, { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, { url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301, upload-time = "2025-12-16T00:24:48.527Z" }, { url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868, upload-time = "2025-12-16T00:48:12.163Z" }, { url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" }, { url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" }, { url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" }, { url = "https://files.pythonhosted.org/packages/52/c5/c171e4d8c44fec1422d801a6d2e5d7ddabd733eeda505c79730ee9607f07/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93", size = 28615, upload-time = "2025-12-16T00:40:29.298Z" }, { url = "https://files.pythonhosted.org/packages/9c/97/7d75fe37a7a6ed171a2cf17117177e7aab7e6e0d115858741b41e9dd4254/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c", size = 28800, upload-time = "2025-12-16T00:40:30.322Z" }, ] [[package]] name = "google-genai" version = "2.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "distro" }, { name = "google-auth", extra = ["requests"] }, { name = "httpx" }, { name = "pydantic" }, { name = "requests" }, { name = "sniffio" }, { name = "tenacity" }, { name = "typing-extensions" }, { name = "websockets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/fe/b796087493c3c55371aa58b9f264841ace5bfdf8c668cafa7afa33c44bec/google_genai-2.10.0.tar.gz", hash = "sha256:77912cd558cd7dfd5b75c25fd1c609e78d7954dde583331104022a46ea90f9ee", size = 600039, upload-time = "2026-06-24T01:33:18.157Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e0/39/00bcfd94de255d24249401efff4f48d77bf6066b46447e519fa193c0c299/google_genai-2.10.0-py3-none-any.whl", hash = "sha256:d5350311567ae660c24cbc1752aee4b3d660f89c0106d2dcd2a69978c35afe1e", size = 957974, upload-time = "2026-06-24T01:33:16.296Z" }, ] [[package]] name = "google-resumable-media" version = "2.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-crc32c" }, ] sdist = { url = "https://files.pythonhosted.org/packages/48/f8/1ca5781d6be9cb9f73f7d40f4958c4bd1226a60598e3e39e1d6aaf838c4b/google_resumable_media-2.10.0.tar.gz", hash = "sha256:e324bc9d0fdae4c52a08ae90456edc4e71ece858399e1217ac0eb3a51d6bc6ee", size = 2164570, upload-time = "2026-06-03T16:14:26.103Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl", hash = "sha256:88152884bee37b2bf36a0ab81ad8c7fd12212c9803dd981d77c1b35b02d34e7c", size = 81533, upload-time = "2026-06-03T16:13:12.51Z" }, ] [[package]] name = "googleapis-common-protos" version = "1.75.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, ] [package.optional-dependencies] grpc = [ { name = "grpcio" }, ] [[package]] name = "greenlet" version = "3.5.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e2/f1/fbbfef6af0bad0548f09bc28948ea3c275b4edb19e17fc5ca9900a6a634d/greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1", size = 200270, upload-time = "2026-06-26T19:28:24.832Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f5/a1/1f7f0c555f5858fd2906fe9f7b0a3554fddb85cb70df7a6aaec41dc292c2/greenlet-3.5.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c", size = 285838, upload-time = "2026-06-26T18:21:05.167Z" }, { url = "https://files.pythonhosted.org/packages/0a/29/be9f43ed61677a5759b38c8a9389248133c8c731bbfc0574ecdff66c99fc/greenlet-3.5.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483d08c11181c83a6ce1a7a61df0f624a208ec40817a3bb2302714592eee4f04", size = 602342, upload-time = "2026-06-26T19:07:06.908Z" }, { url = "https://files.pythonhosted.org/packages/b9/42/ba41c97ec36aa4b3ec25e5aa691d79561254805fad7f2f826dd6770587e2/greenlet-3.5.3-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1dae6e0091eae084317e411f047f0b7cb241c6db570f7c45fd6b900a274914ce", size = 615541, upload-time = "2026-06-26T19:10:04.909Z" }, { url = "https://files.pythonhosted.org/packages/f5/c7/28747042e1df8a9cd120a1ebe15529fc4be3b486e13e8d551ff307a82412/greenlet-3.5.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec", size = 615675, upload-time = "2026-06-26T18:32:14.444Z" }, { url = "https://files.pythonhosted.org/packages/cc/a8/b85525a6c8fba9f009a5f7c8df1545de8fb0f0bf3e0179194ef4e500317f/greenlet-3.5.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:73f152c895e09907e0dbe24f6c2db37beb085cd63db91c3825a0fcd0064124a8", size = 1575057, upload-time = "2026-06-26T19:09:00.264Z" }, { url = "https://files.pythonhosted.org/packages/03/79/fb76edb218fe6735ab0edeba176c7ab80df9618f7c02ce4208979f3ae7db/greenlet-3.5.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8bdb43e1a1d1873721acab2be99c5befd4d2044ddfd52e4d610801019880a702", size = 1641692, upload-time = "2026-06-26T18:31:41.454Z" }, { url = "https://files.pythonhosted.org/packages/6b/79/86fe3ee50ed55d9b3907eecd3208b5c3fe8a79515519aae98b4753c3fa1d/greenlet-3.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:0909f9355a9f24845d3299f3112e266a06afb68302041989fd26bd68894933db", size = 238742, upload-time = "2026-06-26T18:20:40.758Z" }, { url = "https://files.pythonhosted.org/packages/51/58/5404031044f55afad7aad1aff8be3f22b1bed03e237cfeabbc7e5c8cfde0/greenlet-3.5.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4", size = 287424, upload-time = "2026-06-26T18:20:31.469Z" }, { url = "https://files.pythonhosted.org/packages/b4/bf/1c65e9b94a54d547068fa5b5a8a06f221f3316b48908e08668d29c77cb50/greenlet-3.5.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc", size = 606523, upload-time = "2026-06-26T19:07:08.859Z" }, { url = "https://files.pythonhosted.org/packages/b8/c7/b66baacc95775ad511287acb0137b95574a9ce5491902372b7564799d790/greenlet-3.5.3-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6", size = 618315, upload-time = "2026-06-26T19:10:06.055Z" }, { url = "https://files.pythonhosted.org/packages/78/2b/28ed29463522fdbe4c15b1f63922041626a7478316b34ab4adda3f0a4aba/greenlet-3.5.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7", size = 617381, upload-time = "2026-06-26T18:32:16.077Z" }, { url = "https://files.pythonhosted.org/packages/2a/7b/ad04e9d1337fc04965dc9fc616b6a72cb65a24b800a014c011ec812f5489/greenlet-3.5.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8", size = 1577771, upload-time = "2026-06-26T19:09:01.537Z" }, { url = "https://files.pythonhosted.org/packages/d8/33/6c87ab7ba663f70ca21f3022aad1ffe56d3f3e0521e836c2415e13abcc3c/greenlet-3.5.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8", size = 1644048, upload-time = "2026-06-26T18:31:42.996Z" }, { url = "https://files.pythonhosted.org/packages/1c/35/f0d8ee998b422cf8693b270f098e55d8d4ec8006b061b333f54f177d28d9/greenlet-3.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7", size = 239137, upload-time = "2026-06-26T18:23:21.664Z" }, { url = "https://files.pythonhosted.org/packages/fb/96/b9820295576ef18c9edc404f10e260ae7215ceaf3781a54b720ed2627862/greenlet-3.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44", size = 237630, upload-time = "2026-06-26T18:24:00.281Z" }, { url = "https://files.pythonhosted.org/packages/5d/6e/4c37d51a2b7f82d2ff11bb6b5f7d766d9a011726624af255e843727627a3/greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2", size = 288685, upload-time = "2026-06-26T18:22:08.977Z" }, { url = "https://files.pythonhosted.org/packages/7a/73/815dd90131c1b71ebdf53dbc7c276cafec2a1173b97559f97aba72724a87/greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b", size = 604761, upload-time = "2026-06-26T19:07:10.114Z" }, { url = "https://files.pythonhosted.org/packages/9f/57/079cfe76bcef36b153b25607ee91c6fcb58f17f8b23c86bbbeabe0c88d72/greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab", size = 617044, upload-time = "2026-06-26T19:10:07.25Z" }, { url = "https://files.pythonhosted.org/packages/37/87/b4d095775a3fb1bcafbb483fc206b27ebb785724c83051447737085dc54e/greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861", size = 614244, upload-time = "2026-06-26T18:32:17.594Z" }, { url = "https://files.pythonhosted.org/packages/8a/70/7559b609683650fa2b95b8ab84b4ab0b26556a635d19675e12aa832d826d/greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149", size = 1574210, upload-time = "2026-06-26T19:09:03.077Z" }, { url = "https://files.pythonhosted.org/packages/ae/73/be55392074c60fc37655ca40fa6022457bfbf6718e9e342a7b0b41f96dd2/greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea", size = 1638627, upload-time = "2026-06-26T18:31:44.748Z" }, { url = "https://files.pythonhosted.org/packages/14/40/c57489acf8e37d74e2913d4eff63aa0dba17acccc4bdeef874dde2dbbec9/greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c", size = 239882, upload-time = "2026-06-26T18:23:27.518Z" }, { url = "https://files.pythonhosted.org/packages/71/fd/6fea0e3d6600f785069481ee637e09378dd4118acdfd38ad88ae2db31c98/greenlet-3.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d", size = 238211, upload-time = "2026-06-26T18:22:37.671Z" }, { url = "https://files.pythonhosted.org/packages/9b/ff/a620267401db30a50cc8450ee90730e2d4a85658c055c0e760d4ed47fb13/greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550", size = 287609, upload-time = "2026-06-26T18:21:14.724Z" }, { url = "https://files.pythonhosted.org/packages/d6/fa/5401ac78021c826a25b6dde0c705e0a8f29b617509f9185a31dac15fbe1b/greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5", size = 607435, upload-time = "2026-06-26T19:07:11.412Z" }, { url = "https://files.pythonhosted.org/packages/e9/76/1dc144a2e56e65d36405078ed774224375ea520a1870a6e46e08bb4ac7bf/greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3", size = 619787, upload-time = "2026-06-26T19:10:08.396Z" }, { url = "https://files.pythonhosted.org/packages/bf/87/c298cee62df1de4ad7fec32abda73526cff347fd143a6ed4ac369246668a/greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a", size = 616786, upload-time = "2026-06-26T18:32:19.128Z" }, { url = "https://files.pythonhosted.org/packages/9e/2e/e6f009885ed0705ccf33fe0583c117cfd03cde77e31a596dd5785a30762b/greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb", size = 1574316, upload-time = "2026-06-26T19:09:04.273Z" }, { url = "https://files.pythonhosted.org/packages/ef/fe/43fd110b01e40da0adb7c90ac7ea744bef2d43dca00de5095fd2351c2a68/greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b", size = 1638614, upload-time = "2026-06-26T18:31:46.297Z" }, { url = "https://files.pythonhosted.org/packages/0f/7c/062447147a61f8b4337b156fe70d32a165fcf2f89d7ca6255e572806705c/greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b", size = 239850, upload-time = "2026-06-26T18:21:54.613Z" }, { url = "https://files.pythonhosted.org/packages/c7/7e/220a7f5824a64a60443fc03b39dfac4ea63a7fb6d481efa27eafa928e7f4/greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4", size = 238141, upload-time = "2026-06-26T18:22:48.507Z" }, { url = "https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117", size = 288202, upload-time = "2026-06-26T18:23:49.604Z" }, { url = "https://files.pythonhosted.org/packages/82/2f/146d218299046a43d1f029fd544b3d110d0f175a09c715c7e8da4a4a345d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8", size = 654096, upload-time = "2026-06-26T19:07:12.71Z" }, { url = "https://files.pythonhosted.org/packages/a0/cc/04738cafb3f45fa991ea44f9de94c47dcec964f5a972300988a6751f49d9/greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d", size = 666304, upload-time = "2026-06-26T19:10:09.503Z" }, { url = "https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c", size = 663635, upload-time = "2026-06-26T18:32:20.802Z" }, { url = "https://files.pythonhosted.org/packages/d1/50/13efdbea246fe3d3b735e191fec08fb50809f53cd2383ebe123d0809e44b/greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a", size = 1621252, upload-time = "2026-06-26T19:09:05.647Z" }, { url = "https://files.pythonhosted.org/packages/f7/22/c0a336ae4a1410fd5f5121098e5bfbf1865f64c5ef80b4b5412886c4a332/greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154", size = 1684824, upload-time = "2026-06-26T18:31:47.738Z" }, { url = "https://files.pythonhosted.org/packages/7a/94/91aec0030bea75c4b3244251d0de60a1f3432d1ecb53ab6c437fb5c3ba61/greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e", size = 240754, upload-time = "2026-06-26T18:22:15.669Z" }, { url = "https://files.pythonhosted.org/packages/e5/06/68d0983e79e02138f64b4d303c500c27ddb48e5e77f3debb80888a921eae/greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605", size = 239549, upload-time = "2026-06-26T18:22:42.996Z" }, { url = "https://files.pythonhosted.org/packages/91/95/3e161213d7f1d378d15aa9e792093e9bfe01844680d04b7fd6e0107c9098/greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be", size = 296389, upload-time = "2026-06-26T18:22:20.657Z" }, { url = "https://files.pythonhosted.org/packages/00/92/715c44721abe2b4d1ae9abde4179411868a5bff312479f54e105d372f131/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310", size = 653382, upload-time = "2026-06-26T19:07:14.209Z" }, { url = "https://files.pythonhosted.org/packages/a0/83/37a10372a1090a6624cca8e74c12df1a36c2dc36429ed0255b7fb1aeee23/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8", size = 659401, upload-time = "2026-06-26T19:10:10.876Z" }, { url = "https://files.pythonhosted.org/packages/db/e2/d1509cad4207da559cc42986ecdd8fc67ad0d1bba2bf03023c467fd5e0f3/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f", size = 656969, upload-time = "2026-06-26T18:32:22.272Z" }, { url = "https://files.pythonhosted.org/packages/86/7d/eaf70de20aadca3a5884aec58362861c64ce45e7b277f47ed026926a3b89/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21", size = 1617822, upload-time = "2026-06-26T19:09:06.893Z" }, { url = "https://files.pythonhosted.org/packages/8a/f9/414d38fc400ae4350d4185eaad1827676f7cf5287b9136e0ed1cbbe20a7f/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da", size = 1677983, upload-time = "2026-06-26T18:31:49.396Z" }, { url = "https://files.pythonhosted.org/packages/e4/15/7edb977e08f9bff702fe42d6c902702786ff6b9694058b4e6a2a6ac90e57/greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3", size = 243626, upload-time = "2026-06-26T18:24:41.485Z" }, { url = "https://files.pythonhosted.org/packages/2c/8a/93928dce91e6b3598b5e779e8d1fd6576a504640c58e78627077f6a7a91a/greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc", size = 288860, upload-time = "2026-06-26T18:22:48.07Z" }, { url = "https://files.pythonhosted.org/packages/4f/ca/69db42d447a1378043e2c8f19c09cbbd1263371505053c496b49066d3d16/greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47", size = 659747, upload-time = "2026-06-26T19:07:15.565Z" }, { url = "https://files.pythonhosted.org/packages/a8/0b/af7ac2ef8dd41e3da1a40dda6305c23b9a03e13ba975ec916357b50f8575/greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81", size = 670419, upload-time = "2026-06-26T19:10:12.293Z" }, { url = "https://files.pythonhosted.org/packages/51/1e/1d51640cacbfc455dbe9f9a9f594c49e4e244f63b9971a2f4764e46cc53d/greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d", size = 668787, upload-time = "2026-06-26T18:32:24.298Z" }, { url = "https://files.pythonhosted.org/packages/21/66/4030d5b0b5894500023f003bb054d9bb354dfbd1e186c3a296759172f5f5/greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34", size = 1626305, upload-time = "2026-06-26T19:09:08.281Z" }, { url = "https://files.pythonhosted.org/packages/0e/50/5221371c7550108dfa3c378debc41d032aa9c78e89abb01d8011cfc93289/greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b", size = 1688631, upload-time = "2026-06-26T18:31:51.278Z" }, { url = "https://files.pythonhosted.org/packages/68/5d/00d469daae3c65d2bf620b10eee82eb022127d483c6bc8c69fae6f3fbf17/greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930", size = 241027, upload-time = "2026-06-26T18:22:38.203Z" }, { url = "https://files.pythonhosted.org/packages/e7/e8/883785b44c5780ed71e83d3e4437e710470be17a2e181e8b601e2da0dc4a/greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227", size = 240085, upload-time = "2026-06-26T18:23:54.217Z" }, { url = "https://files.pythonhosted.org/packages/1c/da/4f4a8450962fad137c1c8981a3f1b8919d06c829993d4d476f9c525d5173/greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c", size = 297221, upload-time = "2026-06-26T18:23:27.176Z" }, { url = "https://files.pythonhosted.org/packages/57/66/b3bfae3e220a9b63ea539a0eea681800c69ab1aada757eae8789f183e7ce/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f", size = 657221, upload-time = "2026-06-26T19:07:16.973Z" }, { url = "https://files.pythonhosted.org/packages/7b/81/b6d4d73a709684fc77e7fa034d7c2fe82cffa9fc920fadcaa659c2626213/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2", size = 663226, upload-time = "2026-06-26T19:10:13.723Z" }, { url = "https://files.pythonhosted.org/packages/f5/07/e210b02b589f16e74ff48b730690e4a34ffe984219fce4f3c1a0e7ec8545/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608", size = 660802, upload-time = "2026-06-26T18:32:26.081Z" }, { url = "https://files.pythonhosted.org/packages/eb/2e/5303eb3fa06bca089060f479707182a93e360683bc252acf846c3090d34e/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb", size = 1622157, upload-time = "2026-06-26T19:09:09.527Z" }, { url = "https://files.pythonhosted.org/packages/54/70/50de47a488f14df260b50ae34fb5d56016e308b098eab02c878b5223c26a/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16", size = 1681159, upload-time = "2026-06-26T18:31:52.986Z" }, { url = "https://files.pythonhosted.org/packages/a7/13/1055e1dda7882073eda533e2b96c62e55bbd2db7fda6d5ece992febc7071/greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf", size = 244007, upload-time = "2026-06-26T18:22:04.353Z" }, { url = "https://files.pythonhosted.org/packages/b4/0d/ca7d15afbdc397e3401134c9e1800d51d12b829661786187a4ad08fe484f/greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31", size = 242586, upload-time = "2026-06-26T18:23:37.93Z" }, ] [[package]] name = "griffelib" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, ] [[package]] name = "grpc-google-iam-v1" version = "0.14.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos", extra = ["grpc"] }, { name = "grpcio" }, { name = "protobuf" }, ] sdist = { url = "https://files.pythonhosted.org/packages/44/4f/d098419ad0bfc06c9ce440575f05aa22d8973b6c276e86ac7890093d3c37/grpc_google_iam_v1-0.14.4.tar.gz", hash = "sha256:392b3796947ed6334e61171d9ab06bf7eb357f554e5fc7556ad7aab6d0e17038", size = 23706, upload-time = "2026-04-01T01:57:49.813Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/89/22/c2dd50c09bf679bd38173656cd4402d2511e563b33bc88f90009cf50613c/grpc_google_iam_v1-0.14.4-py3-none-any.whl", hash = "sha256:412facc320fcbd94034b4df3d557662051d4d8adfa86e0ddb4dca70a3f739964", size = 32675, upload-time = "2026-04-01T01:57:47.69Z" }, ] [[package]] name = "grpcio" version = "1.78.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5a/a8/690a085b4d1fe066130de97a87de32c45062cf2ecd218df9675add895550/grpcio-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5", size = 5946986, upload-time = "2026-02-06T09:54:34.043Z" }, { url = "https://files.pythonhosted.org/packages/c7/1b/e5213c5c0ced9d2d92778d30529ad5bb2dcfb6c48c4e2d01b1f302d33d64/grpcio-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2", size = 11816533, upload-time = "2026-02-06T09:54:37.04Z" }, { url = "https://files.pythonhosted.org/packages/18/37/1ba32dccf0a324cc5ace744c44331e300b000a924bf14840f948c559ede7/grpcio-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d", size = 6519964, upload-time = "2026-02-06T09:54:40.268Z" }, { url = "https://files.pythonhosted.org/packages/ed/f5/c0e178721b818072f2e8b6fde13faaba942406c634009caf065121ce246b/grpcio-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb", size = 7198058, upload-time = "2026-02-06T09:54:42.389Z" }, { url = "https://files.pythonhosted.org/packages/5b/b2/40d43c91ae9cd667edc960135f9f08e58faa1576dc95af29f66ec912985f/grpcio-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7", size = 6727212, upload-time = "2026-02-06T09:54:44.91Z" }, { url = "https://files.pythonhosted.org/packages/ed/88/9da42eed498f0efcfcd9156e48ae63c0cde3bea398a16c99fb5198c885b6/grpcio-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec", size = 7300845, upload-time = "2026-02-06T09:54:47.562Z" }, { url = "https://files.pythonhosted.org/packages/23/3f/1c66b7b1b19a8828890e37868411a6e6925df5a9030bfa87ab318f34095d/grpcio-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a", size = 8284605, upload-time = "2026-02-06T09:54:50.475Z" }, { url = "https://files.pythonhosted.org/packages/94/c4/ca1bd87394f7b033e88525384b4d1e269e8424ab441ea2fba1a0c5b50986/grpcio-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813", size = 7726672, upload-time = "2026-02-06T09:54:53.11Z" }, { url = "https://files.pythonhosted.org/packages/41/09/f16e487d4cc65ccaf670f6ebdd1a17566b965c74fc3d93999d3b2821e052/grpcio-1.78.0-cp310-cp310-win32.whl", hash = "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de", size = 4076715, upload-time = "2026-02-06T09:54:55.549Z" }, { url = "https://files.pythonhosted.org/packages/2a/32/4ce60d94e242725fd3bcc5673c04502c82a8e87b21ea411a63992dc39f8f/grpcio-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf", size = 4799157, upload-time = "2026-02-06T09:54:59.838Z" }, { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, ] [[package]] name = "grpcio-status" version = "1.78.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, { name = "grpcio" }, { name = "protobuf" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/cd/89ce482a931b543b92cdd9b2888805518c4620e0094409acb8c81dd4610a/grpcio_status-1.78.0.tar.gz", hash = "sha256:a34cfd28101bfea84b5aa0f936b4b423019e9213882907166af6b3bddc59e189", size = 13808, upload-time = "2026-02-06T10:01:48.034Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/83/8a/1241ec22c41028bddd4a052ae9369267b4475265ad0ce7140974548dc3fa/grpcio_status-1.78.0-py3-none-any.whl", hash = "sha256:b492b693d4bf27b47a6c32590701724f1d3b9444b36491878fb71f6208857f34", size = 14523, upload-time = "2026-02-06T10:01:32.584Z" }, ] [[package]] name = "h11" version = "0.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[package]] name = "httpcore2" version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "h11" }, { name = "truststore" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, ] [[package]] name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [[package]] name = "httpx-sse" version = "0.4.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] [[package]] name = "httpx2" version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform != 'emscripten'" }, { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, { name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" }, { name = "idna" }, { name = "truststore", marker = "sys_platform != 'emscripten'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, ] [[package]] name = "httpx2-jsfetch" version = "1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, ] [[package]] name = "hupper" version = "1.12.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bd/e6/bb064537288eee2be97f3e0fcad8e7242bc5bbe9664ae57c7d29b3fa18c2/hupper-1.12.1.tar.gz", hash = "sha256:06bf54170ff4ecf4c84ad5f188dee3901173ab449c2608ad05b9bfd6b13e32eb", size = 43231, upload-time = "2024-01-26T09:14:57.294Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/86/7d/3888833e4f5ea56af4a9935066ec09a83228e533d7b8877f65889d706ee4/hupper-1.12.1-py3-none-any.whl", hash = "sha256:e872b959f09d90be5fb615bd2e62de89a0b57efc037bdf9637fb09cdf8552b19", size = 22830, upload-time = "2024-01-26T09:14:55.176Z" }, ] [[package]] name = "identify" version = "2.6.19" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, ] [[package]] name = "idna" version = "3.18" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "iso8601" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b9/f3/ef59cee614d5e0accf6fd0cbba025b93b272e626ca89fb70a3e9187c5d15/iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df", size = 6522, upload-time = "2023-10-03T00:25:39.317Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6c/0c/f37b6a241f0759b7653ffa7213889d89ad49a2b76eb2ddf3b57b2738c347/iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242", size = 7545, upload-time = "2023-10-03T00:25:32.304Z" }, ] [[package]] name = "itsdangerous" version = "2.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, ] [[package]] name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "jiter" version = "0.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/76/d8/b959609e44012a42b1f3e5ba98ea3b33c7e41e6d4b77cd8f00fd19b1d3ad/jiter-0.16.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fc4f8def331036a7b8e981b4347ebe409981edbc8308a5ea842b8c3614fa6c", size = 310082, upload-time = "2026-06-29T13:02:31.356Z" }, { url = "https://files.pythonhosted.org/packages/c6/3d/4d7f5667ea0e0548534ba880b84bb3d12924fd133aa83ad6c6c80fca3d76/jiter-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a71d0d2014c3275043e1170bf3d4e771493cb0dcf07be54c567155f4d8ee64b", size = 315643, upload-time = "2026-06-29T13:02:33.204Z" }, { url = "https://files.pythonhosted.org/packages/9b/83/bed2dcb5c9f3e1ccfcbc67dda48265fe7d5ad0c9cadda5fe95f6e3b87f94/jiter-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:741eed508c233a76313a1c7b001f8f21b82f14327e9196ae8bd29a2cc164ae84", size = 341363, upload-time = "2026-06-29T13:02:34.853Z" }, { url = "https://files.pythonhosted.org/packages/f4/2f/6bb3c3dda668ebc0445689c81a2b0f26a82b10843d67ed9c9b2c3edc177f/jiter-0.16.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fb7bc819187b56dc48aa5c833aaf92257da8e07efdb9306156667bd2eeb491c", size = 365483, upload-time = "2026-06-29T13:02:36.295Z" }, { url = "https://files.pythonhosted.org/packages/92/35/8a045ccb39164e70dcdae696413b661771f148b68b12b175c3a04d901937/jiter-0.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c9610fd25ebccb43fca584136f5c2fbb26802447eccd430dfdbab95a0fd5126", size = 461219, upload-time = "2026-06-29T13:02:38.116Z" }, { url = "https://files.pythonhosted.org/packages/e7/99/22292dbbf0ed0c610cfe5ddc7f3bd67237a412f121318f865196e62a07bd/jiter-0.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a1d68ff7ca1d3b5dee20a97a3decda7d5f15003823bf6d140c81f8561d3bc5c", size = 374905, upload-time = "2026-06-29T13:02:40.357Z" }, { url = "https://files.pythonhosted.org/packages/29/ac/2f55ccb1f0eeafa6d89d24caf52f6f0944a59290ee199e9ade62177dca42/jiter-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb08c276dd02dac3a284acdd02cacc630d2e3cd6572a4b85519f35cbd133c3de", size = 348320, upload-time = "2026-06-29T13:02:41.923Z" }, { url = "https://files.pythonhosted.org/packages/50/e3/7d88b9174c40064fabc07c84a9b62e6b10f5644562ec0e0a29392edbe978/jiter-0.16.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:8fc4d94713c4697347e38faf7d6ef91547c142219bdcfc7220c4870879974244", size = 356519, upload-time = "2026-06-29T13:02:43.436Z" }, { url = "https://files.pythonhosted.org/packages/27/57/c4a33aeef513a9d5e26e31534e0bcc752d6ea0e54c94ddb7b68bade669c2/jiter-0.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0f05e229edb29e68cdd0ccb83cea13b64263416120cf943767a6fd72e6787f", size = 394204, upload-time = "2026-06-29T13:02:44.987Z" }, { url = "https://files.pythonhosted.org/packages/9d/70/c6c23e76ebb3766b111bc399437bbc9f870a76e2a92e10b2a5f561d57372/jiter-0.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c842cbf374a8daf50b2c04212995bee34ca2ac2cdc29a901b4cdb072c9c4131", size = 521477, upload-time = "2026-06-29T13:02:46.724Z" }, { url = "https://files.pythonhosted.org/packages/2a/d3/0001c8c0c5976af2625bb1cfb1895e8ec693b6589fe4574b8e6fc2c85501/jiter-0.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5ed466aee31294d7cdcd4d37dfe5c42c97bc29d9a5f00eacf24504358309cb9b", size = 552187, upload-time = "2026-06-29T13:02:48.144Z" }, { url = "https://files.pythonhosted.org/packages/f6/76/311b718e07e85740e48619c0632b36f7e0b8d113984499e436452ed13a9a/jiter-0.16.0-cp310-cp310-win32.whl", hash = "sha256:b42e9ff5376819c053da25809a8d4b6fa6e473b4856ebe42e298ac958be3d7f9", size = 206513, upload-time = "2026-06-29T13:02:49.515Z" }, { url = "https://files.pythonhosted.org/packages/db/7f/ac680eeb0777dc0eb7dc824800ba27880d7f6bc712e362d34ad8ee559f36/jiter-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:10438939205546132189c8e74a2d536a707841f3a25cd7c74ee91fe503407a26", size = 199505, upload-time = "2026-06-29T13:02:50.829Z" }, { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" }, { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" }, { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" }, { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" }, { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" }, { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" }, { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" }, { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" }, { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" }, { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" }, { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" }, { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" }, { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" }, { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" }, { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" }, { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" }, { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" }, { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" }, { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, ] [[package]] name = "jmespath" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] [[package]] name = "jsonschema" version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "jsonschema-specifications" }, { name = "referencing" }, { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, ] [[package]] name = "jsonschema-specifications" version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] [[package]] name = "kafka-python" version = "2.3.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c6/7e/9eb4363e5018b71edf77de3d2fbd687f1e3a55d43101138a5b7f2ca56d97/kafka_python-2.3.2.tar.gz", hash = "sha256:2d2469bcabc2551be7eaf7fb512e1241484d358800f2665b90e7e24fea84259d", size = 357589, upload-time = "2026-06-03T22:46:49.022Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4f/db/cfe23cfacc9aabeef59e349aa6c2738d195df1d56ccf8c31362abdd68ddb/kafka_python-2.3.2-py2.py3-none-any.whl", hash = "sha256:e8a5e6f2c4ce13a8041669337038d5125e0e09e6d6f0911902ae5939e5b9595e", size = 326623, upload-time = "2026-06-03T22:46:47.417Z" }, ] [[package]] name = "kombu" version = "5.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "amqp" }, { name = "packaging" }, { name = "tzdata" }, { name = "vine" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b6/a5/607e533ed6c83ae1a696969b8e1c137dfebd5759a2e9682e26ff1b97740b/kombu-5.6.2.tar.gz", hash = "sha256:8060497058066c6f5aed7c26d7cd0d3b574990b09de842a8c5aaed0b92cc5a55", size = 472594, upload-time = "2025-12-29T20:30:07.779Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fb/0f/834427d8c03ff1d7e867d3db3d176470c64871753252b21b4f4897d1fa45/kombu-5.6.2-py3-none-any.whl", hash = "sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93", size = 214219, upload-time = "2025-12-29T20:30:05.74Z" }, ] [[package]] name = "legacy-cgi" version = "2.6.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f4/9c/91c7d2c5ebbdf0a1a510bfa0ddeaa2fbb5b78677df5ac0a0aa51cf7125b0/legacy_cgi-2.6.4.tar.gz", hash = "sha256:abb9dfc7835772f7c9317977c63253fd22a7484b5c9bbcdca60a29dcce97c577", size = 24603, upload-time = "2025-10-27T05:20:05.395Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8c/7e/e7394eeb49a41cc514b3eb49020223666cbf40d86f5721c2f07871e6d84a/legacy_cgi-2.6.4-py3-none-any.whl", hash = "sha256:7e235ce58bf1e25d1fc9b2d299015e4e2cd37305eccafec1e6bac3fc04b878cd", size = 20035, upload-time = "2025-10-27T05:20:04.289Z" }, ] [[package]] name = "markdown-it-py" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] name = "mcp" version = "1.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "httpx" }, { name = "httpx-sse" }, { name = "jsonschema" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "pyjwt", extra = ["crypto"] }, { name = "python-multipart" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "sse-starlette" }, { name = "starlette" }, { name = "typing-extensions" }, { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "multidict" version = "6.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] [[package]] name = "mysql-connector-python" version = "9.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/26/c9/a9446dbebbcdf7d828d0a3be9049607eab6eeffb4e46ef1ee8ac304baede/mysql_connector_python-9.7.0.tar.gz", hash = "sha256:933887e71c871b6e9d8908459fe8303ebcf8feb5cc1e1c49caa6490e525cf78e", size = 12254829, upload-time = "2026-04-30T07:55:39.797Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7a/7b/bfbe1732bdc413fa29d4431e04f257bed32b0f3efe775ca2e70e9d347008/mysql_connector_python-9.7.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:ee90c5f44f706f012be17f03f6ad158ff96e7f2dcc077896fe4537d3d28b3cf4", size = 20265583, upload-time = "2026-04-23T07:15:43.703Z" }, { url = "https://files.pythonhosted.org/packages/43/40/cba971fdc54522742955f12d4b019e9f3325d9a5c734abf5f012fde7cfff/mysql_connector_python-9.7.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:a2f371ab69d65c61136c51ad7026017400166cef3c959cab7a9fb668c7acbfba", size = 19826949, upload-time = "2026-04-23T07:15:46.443Z" }, { url = "https://files.pythonhosted.org/packages/83/5c/724577da77cd33d056ad48d1e29149f6c123371d651c0d824f6bfd2af28f/mysql_connector_python-9.7.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:9bdfc2d4c4444cd1cc79cc6487c047b28fe2b26d0327b27eb9f5737bb553cb5c", size = 21917561, upload-time = "2026-04-23T07:15:49.077Z" }, { url = "https://files.pythonhosted.org/packages/f3/40/f0184970f6483a4e5ffcb99028f8402f3789b885872a5779edd3fa53da44/mysql_connector_python-9.7.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6546e0b60c275409a5add9e3308c3897fcf478d1338cd845b1664c1a8946f72f", size = 21687512, upload-time = "2026-04-23T07:15:51.614Z" }, { url = "https://files.pythonhosted.org/packages/50/ee/0be8e060376e518897f4b3433e768ccd05bc8bb3d08c436cc2441b44ac0b/mysql_connector_python-9.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:c51be697bfdfdf63bb71c5ecc51f7c6faf4aaa3d14a0136fa16e97cc37df1185", size = 17678391, upload-time = "2026-04-23T07:15:54.626Z" }, { url = "https://files.pythonhosted.org/packages/70/fa/babe981ec8c24eece7f47dc52c5e3fe3f126bc99cc80d637b49ac2fe50a4/mysql_connector_python-9.7.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:b5cb8a3ba42b539f79cd13e4c8376d28506f3180f7079c9b04ea7bfd0424fb03", size = 20265659, upload-time = "2026-04-23T07:15:57.375Z" }, { url = "https://files.pythonhosted.org/packages/a5/4b/c45b8b601b0270faf1d4384e4c7270af9abb8d95ea39425253217c3c236c/mysql_connector_python-9.7.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:5492d57a6a0e5127a928290737fbb91b66b46d31dac8de3e7604e550bf3b3a6e", size = 19826940, upload-time = "2026-04-23T07:16:00.156Z" }, { url = "https://files.pythonhosted.org/packages/ba/cf/9d93f8325697b4cde1e9ebb06cecf5eca61135503e111d1bf533255f03d6/mysql_connector_python-9.7.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:daf70f7da64a2e7c17e6dfd1fc98d2deb653fd955d0bd0b1fef246356e682b0f", size = 21923121, upload-time = "2026-04-30T07:54:04.273Z" }, { url = "https://files.pythonhosted.org/packages/de/f5/2177f1e0371483204998389d8d999264c2d6567875b9f1a6b59df8f68e76/mysql_connector_python-9.7.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b8d3d6f32b95ae1d0a2f29d1a2b4e628849bcf7e84a70dc56c85b4929361d86b", size = 21693369, upload-time = "2026-04-30T07:54:06.841Z" }, { url = "https://files.pythonhosted.org/packages/75/68/1f7469669ba1b7d70bec4076766f7672190021090d6eb7e9a0ca6097f501/mysql_connector_python-9.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:079ce68d617250ef5cefffd5243056dc9f87c6034c804235fd6f45a0daf5a6ae", size = 17677742, upload-time = "2026-04-30T07:54:09.486Z" }, { url = "https://files.pythonhosted.org/packages/6b/2d/3fb86f8646c07c32b58e9f5bf0975472c62d07efd9e0918f2b5b64ef1f20/mysql_connector_python-9.7.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2eae45230b5cbd783d68bdfe8b05ad9b4ebd06799f8d302a6169d7f025572baf", size = 20270146, upload-time = "2026-04-30T07:54:12.214Z" }, { url = "https://files.pythonhosted.org/packages/e8/87/f25195a824e8eb33f51113d85ff5bc3a3ec0430f6a537198d7d1a82fa1b4/mysql_connector_python-9.7.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:15106ed73d74487f86de6b1859ff7f362efca7c7f9c494497ccb7439d3139fe6", size = 19830228, upload-time = "2026-04-30T07:54:14.498Z" }, { url = "https://files.pythonhosted.org/packages/2a/b0/f1ffcc88781a7099a8f32ec61a70d53f83432fd0c1f7be349d44810b65d5/mysql_connector_python-9.7.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:45491d4ce56722cb335e6d0bde2d4f4a98b7073421bd02a04ad5e6220d69e499", size = 21925372, upload-time = "2026-04-30T07:54:16.732Z" }, { url = "https://files.pythonhosted.org/packages/ae/6a/5488a05a7c56eac2ef127f5f74ccf604cc659d33616a3d48eb82ce35f1f5/mysql_connector_python-9.7.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d5924a76b530159c02f2fe8da4d3c6377ce1f5e195827e8ecbd36124673651d3", size = 21695866, upload-time = "2026-04-30T07:54:19.031Z" }, { url = "https://files.pythonhosted.org/packages/6f/f5/9b07250bc98c7abae0e538423fe8a2fda0f799c606dbe82014412299340c/mysql_connector_python-9.7.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e3842ecd62391a28c1dda5eb817f40418715e68482a8c146b3c478ac8bb7a23f", size = 20270283, upload-time = "2026-04-30T07:54:21.23Z" }, { url = "https://files.pythonhosted.org/packages/3c/2e/70a1549a52add00775e073fdd7b7b57c21a330ea6f01b26ea8ef0ac3dba8/mysql_connector_python-9.7.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:9da73e212bb08e4df286506ffbda943063bbce448375a7db29748bb367f4e0af", size = 19830388, upload-time = "2026-04-30T07:54:23.51Z" }, { url = "https://files.pythonhosted.org/packages/81/f1/ef64f3c715f55d238ac56b0e0278408501ddc60aa62affa056eab167e28c/mysql_connector_python-9.7.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:39bf31cdbe920eae08801eb829584dc27f70dadbdb3a5c3b78402f54cea87ab0", size = 21925396, upload-time = "2026-04-30T07:54:25.657Z" }, { url = "https://files.pythonhosted.org/packages/2d/17/e957dad49741680b7d0f69aef2b2144b22061c50581aaf0022ccf8de7e38/mysql_connector_python-9.7.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:236a4b8abfac8217517ce9e9edc735decf55282ada3890b3cd33770009f33d68", size = 21695834, upload-time = "2026-04-30T07:54:28.179Z" }, { url = "https://files.pythonhosted.org/packages/08/b6/bc9018fb02748ef83acbabbb4ef8467162cf39fc207e4e575595ffe22fd0/mysql_connector_python-9.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:229ffce2333b88b59f8f034881e12dc85b309615338be9f843ed63923db99d52", size = 17678686, upload-time = "2026-04-30T07:54:30.317Z" }, { url = "https://files.pythonhosted.org/packages/46/db/453c6916b4b1cad6c8e56631e67f81e0d01b52690de541894728c2122f70/mysql_connector_python-9.7.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e17ceb79c1c4c137a5994f8ca8c6a1ce8ebf83eda3ccc21dd67f2c1398680f0", size = 20270277, upload-time = "2026-04-30T07:54:32.628Z" }, { url = "https://files.pythonhosted.org/packages/d7/6d/3ff4dc01806511a3741764eafd5c28b73de18d3a3dfb391d4e4cfe51e1ca/mysql_connector_python-9.7.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:272c4e8263f6a514e4ae65e9553ed214fc9d4cf1f0f6bedca30def20c2ae1d52", size = 19830318, upload-time = "2026-04-30T07:54:34.952Z" }, { url = "https://files.pythonhosted.org/packages/3e/79/b55c8f84a86071e5038567849c9cae4e3c96b211ce843c0f1808528bc202/mysql_connector_python-9.7.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:9549a2974353407427b574f005c92f03972e303182b4c3b4a272bf4ca10855ed", size = 21924702, upload-time = "2026-04-30T07:54:37.418Z" }, { url = "https://files.pythonhosted.org/packages/61/3e/2ab7a525209826bde620f9029b2c8f340a16b4151384813827477d475569/mysql_connector_python-9.7.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ea4e05ab864ab8b0b5066d885d89ca51e5ba1320dba520c20a6f9388a8eca022", size = 21696426, upload-time = "2026-04-30T07:54:39.938Z" }, { url = "https://files.pythonhosted.org/packages/af/94/e370c40a6863752ba6886f20b960284daeef78b2a0e23f38d141b4aa7e76/mysql_connector_python-9.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:5a5abbc152bc28cb2e64a04605ecd9941eff6b0dc5f9528cb84adb873e9a1e49", size = 18197576, upload-time = "2026-04-30T07:54:42.041Z" }, { url = "https://files.pythonhosted.org/packages/38/55/4ebb602d270108ea6e81ccdfe4aa0a511fedffdc3ef6c193ac9ff76402fe/mysql_connector_python-9.7.0-py2.py3-none-any.whl", hash = "sha256:af80b1e7179d5c2d983cf62470ad9b134a7e9ef05cf31108ae587f15873530cc", size = 480646, upload-time = "2026-04-30T07:54:43.82Z" }, ] [[package]] name = "mysqlclient" version = "2.2.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/eb/b0/9df076488cb2e536d40ce6dbd4273c1f20a386e31ffe6e7cb613902b3c2a/mysqlclient-2.2.8.tar.gz", hash = "sha256:8ed20c5615a915da451bb308c7d0306648a4fd9a2809ba95c992690006306199", size = 92287, upload-time = "2026-02-10T10:58:37.405Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/60/d0/22b896a2f8f797ee3517a0c1c1fe576cd38768cd25e2a7362a7277725ed4/mysqlclient-2.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:86db31bba7b3480fec2751350e9790e24f016f89af33a87bab7e79f7196474e8", size = 206873, upload-time = "2026-02-10T10:58:40.996Z" }, { url = "https://files.pythonhosted.org/packages/44/f6/6253d116c024aeb11fb193ad14e2bae2bd3380690e885028abf798b1b0b4/mysqlclient-2.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:60c9ed339dc09e3d5380cc2a9f42e86754fee25a661ced77a02df77990664c92", size = 206875, upload-time = "2026-02-10T10:58:42.223Z" }, { url = "https://files.pythonhosted.org/packages/db/1f/7d6b3fbfc8a317805fdb8bbfbc23af99f2090497090e82513daf491a29ce/mysqlclient-2.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:000c7ec3d11e7c411db832e4cfcd7f05db47464326381f5d5ae991b4bb572f93", size = 207171, upload-time = "2026-02-10T10:58:39.827Z" }, { url = "https://files.pythonhosted.org/packages/e6/d5/76e369b0fdccd2eb9ed7d890e4e3e23aa1344fea62f0180d7f1574285e54/mysqlclient-2.2.8-cp313-cp313-win_amd64.whl", hash = "sha256:a81f5e12f8d05439709cb02fba97f9f76d1a6c528164f2260d8798fec969e300", size = 207158, upload-time = "2026-02-10T10:58:38.663Z" }, { url = "https://files.pythonhosted.org/packages/42/44/6ae21b4e8490eb5167d04fa7452c462ffd72a502b96bf531477ef044a380/mysqlclient-2.2.8-cp314-cp314-win_amd64.whl", hash = "sha256:9bed7c8d3b629bdc09e17fb628d5b3b0a5fd1f12b09432b464b9126c727bedc0", size = 211644, upload-time = "2026-02-10T10:58:45.022Z" }, { url = "https://files.pythonhosted.org/packages/66/e5/037d55623be9f681236e04abe12e1290847c06bd48270c3f19ac33493cbf/mysqlclient-2.2.8-cp314-cp314t-win_amd64.whl", hash = "sha256:260cce0e81446c83bf0a389e0fae38d68547d9f8fc0833bc733014e10ce28a99", size = 213067, upload-time = "2026-02-10T10:58:43.389Z" }, ] [[package]] name = "nodeenv" version = "1.10.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] name = "openai" version = "2.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "distro" }, { name = "httpx" }, { name = "jiter" }, { name = "pydantic" }, { name = "sniffio" }, { name = "tqdm" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/f5/7c7cb955305cb41f7f3c5fd7e0e38bf6bbf2658468863d4b7b868a5cb8df/openai-2.44.0.tar.gz", hash = "sha256:68a5a5ffad82b8ff7d451c437529fb64f7c3b8123aaf0c021966a882d9e3947d", size = 988753, upload-time = "2026-06-24T20:56:02.293Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ae/f4/561ed79fd94876160018a5e75254cfcb9b0e62d4dded9dcb20072e86d623/openai-2.44.0-py3-none-any.whl", hash = "sha256:0a2a3ab2e29aeda368700f662ff9ba0f9df17ba4c54577a64e08b8115a3cc0ad", size = 1366216, upload-time = "2026-06-24T20:55:58.882Z" }, ] [[package]] name = "openai-agents" version = "0.17.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffelib" }, { name = "mcp" }, { name = "openai" }, { name = "pydantic" }, { name = "requests" }, { name = "typing-extensions" }, { name = "websockets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d4/b2/235cbfdefe86623fc77f65fc1d016686372f61d4a1bf3fc66151de2eb847/openai_agents-0.17.7.tar.gz", hash = "sha256:ca76e7f882c9d8f06e3dfb8064cc33bcb5a5f34a29816cb9af863f395964ff0c", size = 5485068, upload-time = "2026-06-24T05:15:33.705Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/2e/2e96ca6928951fe1d16744c22dc1355eda1dd5b0dd920ca1d3ab602929f8/openai_agents-0.17.7-py3-none-any.whl", hash = "sha256:51b5ae43756eea37032e430f95979ba3999af6b1ade397df6c0ffeaf1939646a", size = 856074, upload-time = "2026-06-24T05:15:31.741Z" }, ] [[package]] name = "opentelemetry-api" version = "1.44.0.dev0" source = { git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main#9ffd585e2f5eb296e2c9e834887b382af0c18727" } dependencies = [ { name = "typing-extensions" }, ] [[package]] name = "opentelemetry-distro" source = { editable = "opentelemetry-distro" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-sdk" }, ] [package.optional-dependencies] otlp = [ { name = "opentelemetry-exporter-otlp" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-exporter-otlp", marker = "extra == 'otlp'", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=exporter%2Fopentelemetry-exporter-otlp&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-sdk", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-sdk&branch=main" }, ] provides-extras = ["otlp"] [[package]] name = "opentelemetry-exporter-credential-provider-gcp" source = { editable = "exporter/opentelemetry-exporter-credential-provider-gcp" } dependencies = [ { name = "google-auth" }, { name = "grpcio" }, { name = "requests" }, ] [package.metadata] requires-dist = [ { name = "google-auth", specifier = ">=2.25" }, { name = "grpcio", specifier = ">=1.67.0" }, { name = "requests", specifier = ">=2.7" }, ] [[package]] name = "opentelemetry-exporter-otlp" version = "1.44.0.dev0" source = { git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=exporter%2Fopentelemetry-exporter-otlp&branch=main#9ffd585e2f5eb296e2c9e834887b382af0c18727" } dependencies = [ { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" version = "1.44.0.dev0" source = { git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=exporter%2Fopentelemetry-exporter-otlp-proto-common&branch=main#9ffd585e2f5eb296e2c9e834887b382af0c18727" } dependencies = [ { name = "opentelemetry-proto" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" version = "1.44.0.dev0" source = { git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=exporter%2Fopentelemetry-exporter-otlp-proto-grpc&branch=main#9ffd585e2f5eb296e2c9e834887b382af0c18727" } dependencies = [ { name = "googleapis-common-protos" }, { name = "grpcio" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-proto" }, { name = "opentelemetry-sdk" }, { name = "typing-extensions" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" version = "1.44.0.dev0" source = { git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=exporter%2Fopentelemetry-exporter-otlp-proto-http&branch=main#9ffd585e2f5eb296e2c9e834887b382af0c18727" } dependencies = [ { name = "googleapis-common-protos" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-proto" }, { name = "opentelemetry-sdk" }, { name = "requests" }, { name = "typing-extensions" }, ] [[package]] name = "opentelemetry-exporter-prometheus-remote-write" source = { editable = "exporter/opentelemetry-exporter-prometheus-remote-write" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, { name = "protobuf" }, { name = "python-snappy" }, { name = "requests" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-sdk", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-sdk&branch=main" }, { name = "protobuf", specifier = ">=5.26,<7.0" }, { name = "python-snappy", specifier = "~=0.6" }, { name = "requests", specifier = "~=2.28" }, ] [[package]] name = "opentelemetry-exporter-richconsole" source = { editable = "exporter/opentelemetry-exporter-richconsole" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, { name = "opentelemetry-semantic-conventions" }, { name = "rich" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-sdk", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-sdk&branch=main" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "rich", specifier = ">=10.0.0" }, ] [[package]] name = "opentelemetry-instrumentation" source = { editable = "opentelemetry-instrumentation" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "packaging" }, { name = "wrapt" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "packaging", specifier = ">=18.0" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] [[package]] name = "opentelemetry-instrumentation-aio-pika" source = { editable = "instrumentation/opentelemetry-instrumentation-aio-pika" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments = [ { name = "aio-pika" }, ] [package.metadata] requires-dist = [ { name = "aio-pika", marker = "extra == 'instruments'", specifier = ">=7.2.0,<11.0.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-aiohttp-client" source = { editable = "instrumentation/opentelemetry-instrumentation-aiohttp-client" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments = [ { name = "aiohttp" }, ] [package.metadata] requires-dist = [ { name = "aiohttp", marker = "extra == 'instruments'", specifier = "~=3.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-util-http", editable = "util/opentelemetry-util-http" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-aiohttp-server" source = { editable = "instrumentation/opentelemetry-instrumentation-aiohttp-server" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments = [ { name = "aiohttp" }, ] [package.metadata] requires-dist = [ { name = "aiohttp", marker = "extra == 'instruments'", specifier = "~=3.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-util-http", editable = "util/opentelemetry-util-http" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-aiokafka" source = { editable = "instrumentation/opentelemetry-instrumentation-aiokafka" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] [package.optional-dependencies] instruments = [ { name = "aiokafka" }, ] [package.metadata] requires-dist = [ { name = "aiokafka", marker = "extra == 'instruments'", specifier = ">=0.8,<1.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "typing-extensions", specifier = "~=4.1" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-aiopg" source = { editable = "instrumentation/opentelemetry-instrumentation-aiopg" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments = [ { name = "aiopg" }, ] [package.metadata] requires-dist = [ { name = "aiopg", marker = "extra == 'instruments'", specifier = ">=0.13.0,<2.0.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi", editable = "instrumentation/opentelemetry-instrumentation-dbapi" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-asgi" source = { editable = "instrumentation/opentelemetry-instrumentation-asgi" } dependencies = [ { name = "asgiref" }, { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] [package.optional-dependencies] instruments = [ { name = "asgiref" }, ] [package.metadata] requires-dist = [ { name = "asgiref", specifier = "~=3.0" }, { name = "asgiref", marker = "extra == 'instruments'", specifier = "~=3.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-util-http", editable = "util/opentelemetry-util-http" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-asyncclick" source = { editable = "instrumentation/opentelemetry-instrumentation-asyncclick" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments = [ { name = "asyncclick", version = "8.3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "asyncclick", version = "8.4.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] [package.metadata] requires-dist = [ { name = "asyncclick", marker = "extra == 'instruments'", specifier = "~=8.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "typing-extensions", specifier = "~=4.12" }, { name = "wrapt", specifier = "~=1.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-asyncio" source = { editable = "instrumentation/opentelemetry-instrumentation-asyncio" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-asyncpg" source = { editable = "instrumentation/opentelemetry-instrumentation-asyncpg" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, ] [package.optional-dependencies] instruments = [ { name = "asyncpg" }, ] [package.metadata] requires-dist = [ { name = "asyncpg", marker = "extra == 'instruments'", specifier = ">=0.12.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-aws-lambda" source = { editable = "instrumentation/opentelemetry-instrumentation-aws-lambda" } dependencies = [ { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-propagator-aws-xray" }, { name = "opentelemetry-semantic-conventions" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-propagator-aws-xray", editable = "propagator/opentelemetry-propagator-aws-xray" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-boto3sqs" source = { editable = "instrumentation/opentelemetry-instrumentation-boto3sqs" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments = [ { name = "boto3" }, ] [package.metadata] requires-dist = [ { name = "boto3", marker = "extra == 'instruments'", specifier = "~=1.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-botocore" source = { editable = "instrumentation/opentelemetry-instrumentation-botocore" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-propagator-aws-xray" }, { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments-any = [ { name = "aiobotocore" }, { name = "botocore" }, ] [package.metadata] requires-dist = [ { name = "aiobotocore", marker = "extra == 'instruments-any'", specifier = ">=2.0,<4.0" }, { name = "botocore", marker = "extra == 'instruments-any'", specifier = "~=1.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-propagator-aws-xray", editable = "propagator/opentelemetry-propagator-aws-xray" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments", "instruments-any"] [[package]] name = "opentelemetry-instrumentation-cassandra" source = { editable = "instrumentation/opentelemetry-instrumentation-cassandra" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments-any = [ { name = "cassandra-driver" }, { name = "scylla-driver" }, ] [package.metadata] requires-dist = [ { name = "cassandra-driver", marker = "extra == 'instruments-any'", specifier = "~=3.25" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "scylla-driver", marker = "extra == 'instruments-any'", specifier = "~=3.25" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments", "instruments-any"] [[package]] name = "opentelemetry-instrumentation-celery" source = { editable = "instrumentation/opentelemetry-instrumentation-celery" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, ] [package.optional-dependencies] instruments = [ { name = "celery" }, ] [package.metadata] requires-dist = [ { name = "celery", marker = "extra == 'instruments'", specifier = ">=4.0,<6.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-click" source = { editable = "instrumentation/opentelemetry-instrumentation-click" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments = [ { name = "click" }, ] [package.metadata] requires-dist = [ { name = "click", marker = "extra == 'instruments'", specifier = ">=8.1.3,<9.0.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-confluent-kafka" source = { editable = "instrumentation/opentelemetry-instrumentation-confluent-kafka" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments = [ { name = "confluent-kafka" }, ] [package.metadata] requires-dist = [ { name = "confluent-kafka", marker = "extra == 'instruments'", specifier = ">=1.8.2,<3.0.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-dbapi" source = { editable = "instrumentation/opentelemetry-instrumentation-dbapi" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-django" source = { editable = "instrumentation/opentelemetry-instrumentation-django" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-wsgi" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] [package.optional-dependencies] asgi = [ { name = "opentelemetry-instrumentation-asgi" }, ] instruments = [ { name = "django", version = "5.2.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "django", version = "6.0.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] [package.metadata] requires-dist = [ { name = "django", marker = "extra == 'instruments'", specifier = ">=2.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-asgi", marker = "extra == 'asgi'", editable = "instrumentation/opentelemetry-instrumentation-asgi" }, { name = "opentelemetry-instrumentation-wsgi", editable = "instrumentation/opentelemetry-instrumentation-wsgi" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-util-http", editable = "util/opentelemetry-util-http" }, ] provides-extras = ["asgi", "instruments"] [[package]] name = "opentelemetry-instrumentation-exceptions" source = { editable = "instrumentation/opentelemetry-instrumentation-exceptions" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-falcon" source = { editable = "instrumentation/opentelemetry-instrumentation-falcon" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-wsgi" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, { name = "packaging" }, ] [package.optional-dependencies] instruments = [ { name = "falcon" }, ] [package.metadata] requires-dist = [ { name = "falcon", marker = "extra == 'instruments'", specifier = ">=1.4.1,<5.0.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-wsgi", editable = "instrumentation/opentelemetry-instrumentation-wsgi" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-util-http", editable = "util/opentelemetry-util-http" }, { name = "packaging", specifier = ">=20.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-fastapi" source = { editable = "instrumentation/opentelemetry-instrumentation-fastapi" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-asgi" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] [package.optional-dependencies] instruments = [ { name = "fastapi" }, ] [package.metadata] requires-dist = [ { name = "fastapi", marker = "extra == 'instruments'", specifier = "~=0.92" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-asgi", editable = "instrumentation/opentelemetry-instrumentation-asgi" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-util-http", editable = "util/opentelemetry-util-http" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-flask" source = { editable = "instrumentation/opentelemetry-instrumentation-flask" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-wsgi" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, { name = "packaging" }, ] [package.optional-dependencies] instruments = [ { name = "flask" }, ] [package.metadata] requires-dist = [ { name = "flask", marker = "extra == 'instruments'", specifier = ">=1.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-wsgi", editable = "instrumentation/opentelemetry-instrumentation-wsgi" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-util-http", editable = "util/opentelemetry-util-http" }, { name = "packaging", specifier = ">=21.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-grpc" source = { editable = "instrumentation/opentelemetry-instrumentation-grpc" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments = [ { name = "grpcio" }, ] [package.metadata] requires-dist = [ { name = "grpcio", marker = "extra == 'instruments'", specifier = ">=1.42.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-httpx" source = { editable = "instrumentation/opentelemetry-instrumentation-httpx" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments-any = [ { name = "httpx" }, { name = "httpx2" }, ] [package.metadata] requires-dist = [ { name = "httpx", marker = "extra == 'instruments-any'", specifier = ">=0.18.0" }, { name = "httpx2", marker = "extra == 'instruments-any'", specifier = ">=2.0.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-util-http", editable = "util/opentelemetry-util-http" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments", "instruments-any"] [[package]] name = "opentelemetry-instrumentation-jinja2" source = { editable = "instrumentation/opentelemetry-instrumentation-jinja2" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments = [ { name = "jinja2" }, ] [package.metadata] requires-dist = [ { name = "jinja2", marker = "extra == 'instruments'", specifier = ">=2.7,<4.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-kafka-python" source = { editable = "instrumentation/opentelemetry-instrumentation-kafka-python" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, ] [package.optional-dependencies] instruments = [ { name = "kafka-python" }, ] [package.metadata] requires-dist = [ { name = "kafka-python", marker = "extra == 'instruments'", specifier = ">=2.0,<4.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-logging" source = { editable = "instrumentation/opentelemetry-instrumentation-logging" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-mysql" source = { editable = "instrumentation/opentelemetry-instrumentation-mysql" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi" }, ] [package.optional-dependencies] instruments = [ { name = "mysql-connector-python" }, ] [package.metadata] requires-dist = [ { name = "mysql-connector-python", marker = "extra == 'instruments'", specifier = ">=8.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi", editable = "instrumentation/opentelemetry-instrumentation-dbapi" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-mysqlclient" source = { editable = "instrumentation/opentelemetry-instrumentation-mysqlclient" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi" }, ] [package.optional-dependencies] instruments = [ { name = "mysqlclient" }, ] [package.metadata] requires-dist = [ { name = "mysqlclient", marker = "extra == 'instruments'", specifier = "<3" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi", editable = "instrumentation/opentelemetry-instrumentation-dbapi" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-openai-agents-v2" source = { editable = "instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-genai" }, { name = "typing-extensions" }, ] [package.optional-dependencies] instruments = [ { name = "openai-agents" }, ] [package.metadata] requires-dist = [ { name = "openai-agents", marker = "extra == 'instruments'", specifier = ">=0.3.3" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-util-genai", editable = "util/opentelemetry-util-genai" }, { name = "typing-extensions", specifier = ">=4.5.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-openai-v2" source = { editable = "instrumentation-genai/opentelemetry-instrumentation-openai-v2" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-genai" }, { name = "typing-extensions" }, ] [package.optional-dependencies] instruments = [ { name = "openai" }, ] [package.metadata] requires-dist = [ { name = "openai", marker = "extra == 'instruments'", specifier = ">=1.26.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-util-genai", editable = "util/opentelemetry-util-genai" }, { name = "typing-extensions", specifier = ">=4.5.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-pika" source = { editable = "instrumentation/opentelemetry-instrumentation-pika" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "packaging" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments = [ { name = "pika" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "packaging", specifier = ">=20.0" }, { name = "pika", marker = "extra == 'instruments'", specifier = ">=0.12.0" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-psycopg" source = { editable = "instrumentation/opentelemetry-instrumentation-psycopg" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi" }, ] [package.optional-dependencies] instruments = [ { name = "psycopg" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi", editable = "instrumentation/opentelemetry-instrumentation-dbapi" }, { name = "psycopg", marker = "extra == 'instruments'", specifier = ">=3.1.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-psycopg2" source = { editable = "instrumentation/opentelemetry-instrumentation-psycopg2" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi" }, ] [package.optional-dependencies] instruments-any = [ { name = "psycopg2" }, { name = "psycopg2-binary" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi", editable = "instrumentation/opentelemetry-instrumentation-dbapi" }, { name = "psycopg2", marker = "extra == 'instruments-any'", specifier = ">=2.7.3.1" }, { name = "psycopg2-binary", marker = "extra == 'instruments-any'", specifier = ">=2.7.3.1" }, ] provides-extras = ["instruments", "instruments-any"] [[package]] name = "opentelemetry-instrumentation-pymemcache" source = { editable = "instrumentation/opentelemetry-instrumentation-pymemcache" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments = [ { name = "pymemcache" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "pymemcache", marker = "extra == 'instruments'", specifier = ">=1.3.5,<5" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-pymongo" source = { editable = "instrumentation/opentelemetry-instrumentation-pymongo" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, ] [package.optional-dependencies] instruments = [ { name = "pymongo" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "pymongo", marker = "extra == 'instruments'", specifier = ">=3.1,<5.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-pymssql" source = { editable = "instrumentation/opentelemetry-instrumentation-pymssql" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi" }, ] [package.optional-dependencies] instruments = [ { name = "pymssql" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi", editable = "instrumentation/opentelemetry-instrumentation-dbapi" }, { name = "pymssql", marker = "extra == 'instruments'", specifier = ">=2.1.5,<3" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-pymysql" source = { editable = "instrumentation/opentelemetry-instrumentation-pymysql" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi" }, ] [package.optional-dependencies] instruments = [ { name = "pymysql" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi", editable = "instrumentation/opentelemetry-instrumentation-dbapi" }, { name = "pymysql", marker = "extra == 'instruments'", specifier = "<2" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-pyramid" source = { editable = "instrumentation/opentelemetry-instrumentation-pyramid" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-wsgi" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments = [ { name = "pyramid" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-wsgi", editable = "instrumentation/opentelemetry-instrumentation-wsgi" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-util-http", editable = "util/opentelemetry-util-http" }, { name = "pyramid", marker = "extra == 'instruments'", specifier = ">=1.7" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-redis" source = { editable = "instrumentation/opentelemetry-instrumentation-redis" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments = [ { name = "redis" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "redis", marker = "extra == 'instruments'", specifier = ">=2.6" }, { name = "wrapt", specifier = ">=1.12.1" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-remoulade" source = { editable = "instrumentation/opentelemetry-instrumentation-remoulade" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, ] [package.optional-dependencies] instruments = [ { name = "remoulade", version = "3.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "remoulade", version = "6.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "remoulade", marker = "extra == 'instruments'", specifier = ">=0.50" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-requests" source = { editable = "instrumentation/opentelemetry-instrumentation-requests" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] [package.optional-dependencies] instruments = [ { name = "requests" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-util-http", editable = "util/opentelemetry-util-http" }, { name = "requests", marker = "extra == 'instruments'", specifier = "~=2.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-sqlalchemy" source = { editable = "instrumentation/opentelemetry-instrumentation-sqlalchemy" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "packaging" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments = [ { name = "sqlalchemy" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "packaging", specifier = ">=21.0" }, { name = "sqlalchemy", marker = "extra == 'instruments'", specifier = ">=1.0.0,<2.1.0" }, { name = "wrapt", specifier = ">=1.11.2" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-sqlite3" source = { editable = "instrumentation/opentelemetry-instrumentation-sqlite3" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi", editable = "instrumentation/opentelemetry-instrumentation-dbapi" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-starlette" source = { editable = "instrumentation/opentelemetry-instrumentation-starlette" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-asgi" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] [package.optional-dependencies] instruments = [ { name = "starlette" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-asgi", editable = "instrumentation/opentelemetry-instrumentation-asgi" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-util-http", editable = "util/opentelemetry-util-http" }, { name = "starlette", marker = "extra == 'instruments'", specifier = ">=0.13" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-structlog" source = { editable = "instrumentation/opentelemetry-instrumentation-structlog" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, ] [package.optional-dependencies] instruments = [ { name = "structlog" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "structlog", marker = "extra == 'instruments'", specifier = ">=21.1" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-system-metrics" source = { editable = "instrumentation/opentelemetry-instrumentation-system-metrics" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "psutil" }, ] [package.optional-dependencies] instruments = [ { name = "psutil" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "psutil", specifier = ">=5.9.0,<8" }, { name = "psutil", marker = "extra == 'instruments'", specifier = ">=5" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-threading" source = { editable = "instrumentation/opentelemetry-instrumentation-threading" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "wrapt" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-tornado" source = { editable = "instrumentation/opentelemetry-instrumentation-tornado" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] [package.optional-dependencies] instruments = [ { name = "tornado" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-util-http", editable = "util/opentelemetry-util-http" }, { name = "tornado", marker = "extra == 'instruments'", specifier = ">=5.1.1" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-tortoiseorm" source = { editable = "instrumentation/opentelemetry-instrumentation-tortoiseorm" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, ] [package.optional-dependencies] instruments = [ { name = "pydantic" }, { name = "tortoise-orm" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "pydantic", marker = "extra == 'instruments'", specifier = ">=1.10.2" }, { name = "tortoise-orm", marker = "extra == 'instruments'", specifier = ">=0.17.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-urllib" source = { editable = "instrumentation/opentelemetry-instrumentation-urllib" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-util-http", editable = "util/opentelemetry-util-http" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-urllib3" source = { editable = "instrumentation/opentelemetry-instrumentation-urllib3" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments = [ { name = "urllib3" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-util-http", editable = "util/opentelemetry-util-http" }, { name = "urllib3", marker = "extra == 'instruments'", specifier = ">=1.0.0,<3.0.0" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-valkey-py" source = { editable = "instrumentation/opentelemetry-instrumentation-valkey-py" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] [package.optional-dependencies] instruments = [ { name = "valkey" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "valkey", marker = "extra == 'instruments'", specifier = "~=6.0" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-vertexai" source = { editable = "instrumentation-genai/opentelemetry-instrumentation-vertexai" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-genai" }, ] [package.optional-dependencies] instruments = [ { name = "google-cloud-aiplatform" }, ] [package.metadata] requires-dist = [ { name = "google-cloud-aiplatform", marker = "extra == 'instruments'", specifier = ">=1.64" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-util-genai", editable = "util/opentelemetry-util-genai" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-instrumentation-wsgi" source = { editable = "instrumentation/opentelemetry-instrumentation-wsgi" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-util-http", editable = "util/opentelemetry-util-http" }, ] provides-extras = ["instruments"] [[package]] name = "opentelemetry-propagator-aws-xray" source = { editable = "propagator/opentelemetry-propagator-aws-xray" } dependencies = [ { name = "opentelemetry-api" }, ] [package.metadata] requires-dist = [{ name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }] [[package]] name = "opentelemetry-propagator-ot-trace" source = { editable = "propagator/opentelemetry-propagator-ot-trace" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-sdk", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-sdk&branch=main" }, ] [[package]] name = "opentelemetry-proto" version = "1.44.0.dev0" source = { git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-proto&branch=main#9ffd585e2f5eb296e2c9e834887b382af0c18727" } dependencies = [ { name = "protobuf" }, ] [[package]] name = "opentelemetry-python-contrib" version = "0.0.0" source = { virtual = "." } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-distro" }, { name = "opentelemetry-exporter-prometheus-remote-write" }, { name = "opentelemetry-exporter-richconsole" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-aio-pika", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-aiohttp-client", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-aiohttp-server", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-aiokafka", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-aiopg", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-asgi", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-asyncio" }, { name = "opentelemetry-instrumentation-asyncpg", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-aws-lambda" }, { name = "opentelemetry-instrumentation-boto3sqs", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-botocore", extra = ["instruments-any"] }, { name = "opentelemetry-instrumentation-cassandra" }, { name = "opentelemetry-instrumentation-celery", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-click", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-confluent-kafka", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-dbapi" }, { name = "opentelemetry-instrumentation-django", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-falcon", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-fastapi", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-flask", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-grpc", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-httpx", extra = ["instruments-any"] }, { name = "opentelemetry-instrumentation-jinja2", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-kafka-python", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-logging" }, { name = "opentelemetry-instrumentation-mysql", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-mysqlclient", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-openai-v2", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-pika", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-psycopg", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-psycopg2", extra = ["instruments-any"] }, { name = "opentelemetry-instrumentation-pymemcache", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-pymongo", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-pymysql", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-pyramid", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-redis", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-remoulade", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-requests", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-sqlalchemy", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-sqlite3" }, { name = "opentelemetry-instrumentation-structlog", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-system-metrics" }, { name = "opentelemetry-instrumentation-threading" }, { name = "opentelemetry-instrumentation-tornado" }, { name = "opentelemetry-instrumentation-tortoiseorm", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-urllib" }, { name = "opentelemetry-instrumentation-urllib3", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-vertexai", extra = ["instruments"] }, { name = "opentelemetry-instrumentation-wsgi" }, { name = "opentelemetry-propagator-aws-xray" }, { name = "opentelemetry-propagator-ot-trace" }, { name = "opentelemetry-sdk" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-test-utils" }, { name = "opentelemetry-util-genai" }, { name = "opentelemetry-util-http" }, ] [package.dev-dependencies] dev = [ { name = "pre-commit" }, { name = "towncrier" }, { name = "tox" }, { name = "tox-uv" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-distro", editable = "opentelemetry-distro" }, { name = "opentelemetry-exporter-prometheus-remote-write", editable = "exporter/opentelemetry-exporter-prometheus-remote-write" }, { name = "opentelemetry-exporter-richconsole", editable = "exporter/opentelemetry-exporter-richconsole" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-aio-pika", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-aio-pika" }, { name = "opentelemetry-instrumentation-aiohttp-client", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-aiohttp-client" }, { name = "opentelemetry-instrumentation-aiohttp-server", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-aiohttp-server" }, { name = "opentelemetry-instrumentation-aiokafka", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-aiokafka" }, { name = "opentelemetry-instrumentation-aiopg", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-aiopg" }, { name = "opentelemetry-instrumentation-asgi", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-asgi" }, { name = "opentelemetry-instrumentation-asyncio", editable = "instrumentation/opentelemetry-instrumentation-asyncio" }, { name = "opentelemetry-instrumentation-asyncpg", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-asyncpg" }, { name = "opentelemetry-instrumentation-aws-lambda", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-aws-lambda" }, { name = "opentelemetry-instrumentation-boto3sqs", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-boto3sqs" }, { name = "opentelemetry-instrumentation-botocore", extras = ["instruments-any"], editable = "instrumentation/opentelemetry-instrumentation-botocore" }, { name = "opentelemetry-instrumentation-cassandra", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-cassandra" }, { name = "opentelemetry-instrumentation-celery", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-celery" }, { name = "opentelemetry-instrumentation-click", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-click" }, { name = "opentelemetry-instrumentation-confluent-kafka", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-confluent-kafka" }, { name = "opentelemetry-instrumentation-dbapi", editable = "instrumentation/opentelemetry-instrumentation-dbapi" }, { name = "opentelemetry-instrumentation-django", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-django" }, { name = "opentelemetry-instrumentation-falcon", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-falcon" }, { name = "opentelemetry-instrumentation-fastapi", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-fastapi" }, { name = "opentelemetry-instrumentation-flask", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-flask" }, { name = "opentelemetry-instrumentation-grpc", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-grpc" }, { name = "opentelemetry-instrumentation-httpx", extras = ["instruments-any"], editable = "instrumentation/opentelemetry-instrumentation-httpx" }, { name = "opentelemetry-instrumentation-jinja2", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-jinja2" }, { name = "opentelemetry-instrumentation-kafka-python", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-kafka-python" }, { name = "opentelemetry-instrumentation-logging", editable = "instrumentation/opentelemetry-instrumentation-logging" }, { name = "opentelemetry-instrumentation-mysql", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-mysql" }, { name = "opentelemetry-instrumentation-mysqlclient", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-mysqlclient" }, { name = "opentelemetry-instrumentation-openai-v2", extras = ["instruments"], editable = "instrumentation-genai/opentelemetry-instrumentation-openai-v2" }, { name = "opentelemetry-instrumentation-pika", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-pika" }, { name = "opentelemetry-instrumentation-psycopg", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-psycopg" }, { name = "opentelemetry-instrumentation-psycopg2", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-psycopg2" }, { name = "opentelemetry-instrumentation-psycopg2", extras = ["instruments-any"], editable = "instrumentation/opentelemetry-instrumentation-psycopg2" }, { name = "opentelemetry-instrumentation-pymemcache", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-pymemcache" }, { name = "opentelemetry-instrumentation-pymongo", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-pymongo" }, { name = "opentelemetry-instrumentation-pymysql", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-pymysql" }, { name = "opentelemetry-instrumentation-pyramid", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-pyramid" }, { name = "opentelemetry-instrumentation-redis", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-redis" }, { name = "opentelemetry-instrumentation-remoulade", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-remoulade" }, { name = "opentelemetry-instrumentation-requests", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-requests" }, { name = "opentelemetry-instrumentation-sqlalchemy", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-sqlalchemy" }, { name = "opentelemetry-instrumentation-sqlite3", editable = "instrumentation/opentelemetry-instrumentation-sqlite3" }, { name = "opentelemetry-instrumentation-structlog", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-structlog" }, { name = "opentelemetry-instrumentation-system-metrics", editable = "instrumentation/opentelemetry-instrumentation-system-metrics" }, { name = "opentelemetry-instrumentation-threading", editable = "instrumentation/opentelemetry-instrumentation-threading" }, { name = "opentelemetry-instrumentation-tornado", editable = "instrumentation/opentelemetry-instrumentation-tornado" }, { name = "opentelemetry-instrumentation-tortoiseorm", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-tortoiseorm" }, { name = "opentelemetry-instrumentation-urllib", editable = "instrumentation/opentelemetry-instrumentation-urllib" }, { name = "opentelemetry-instrumentation-urllib3", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-urllib3" }, { name = "opentelemetry-instrumentation-vertexai", extras = ["instruments"], editable = "instrumentation-genai/opentelemetry-instrumentation-vertexai" }, { name = "opentelemetry-instrumentation-wsgi", editable = "instrumentation/opentelemetry-instrumentation-wsgi" }, { name = "opentelemetry-propagator-aws-xray", editable = "propagator/opentelemetry-propagator-aws-xray" }, { name = "opentelemetry-propagator-ot-trace", editable = "propagator/opentelemetry-propagator-ot-trace" }, { name = "opentelemetry-sdk", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-sdk&branch=main" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "opentelemetry-test-utils", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=tests%2Fopentelemetry-test-utils&branch=main" }, { name = "opentelemetry-util-genai", editable = "util/opentelemetry-util-genai" }, { name = "opentelemetry-util-http", editable = "util/opentelemetry-util-http" }, ] [package.metadata.requires-dev] dev = [ { name = "pre-commit" }, { name = "towncrier" }, { name = "tox" }, { name = "tox-uv", specifier = ">=1" }, ] [[package]] name = "opentelemetry-sdk" version = "1.44.0.dev0" source = { git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-sdk&branch=main#9ffd585e2f5eb296e2c9e834887b382af0c18727" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] [[package]] name = "opentelemetry-semantic-conventions" version = "0.65b0.dev0" source = { git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main#9ffd585e2f5eb296e2c9e834887b382af0c18727" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] [[package]] name = "opentelemetry-test-utils" version = "0.65b0.dev0" source = { git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=tests%2Fopentelemetry-test-utils&branch=main#9ffd585e2f5eb296e2c9e834887b382af0c18727" } dependencies = [ { name = "asgiref" }, { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, { name = "requests" }, ] [[package]] name = "opentelemetry-util-genai" source = { editable = "util/opentelemetry-util-genai" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] [package.optional-dependencies] test = [ { name = "pytest" }, ] upload = [ { name = "fsspec" }, ] [package.metadata] requires-dist = [ { name = "fsspec", marker = "extra == 'upload'", specifier = ">=2025.9.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=7.0.0" }, { name = "wrapt", specifier = ">=1.0.0,<3.0.0" }, ] provides-extras = ["test", "upload"] [[package]] name = "opentelemetry-util-http" source = { editable = "util/opentelemetry-util-http" } [[package]] name = "packaging" version = "26.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] name = "pamqp" version = "3.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fb/62/35bbd3d3021e008606cd0a9532db7850c65741bbf69ac8a3a0d8cfeb7934/pamqp-3.3.0.tar.gz", hash = "sha256:40b8795bd4efcf2b0f8821c1de83d12ca16d5760f4507836267fd7a02b06763b", size = 30993, upload-time = "2024-01-12T20:37:25.085Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ac/8d/c1e93296e109a320e508e38118cf7d1fc2a4d1c2ec64de78565b3c445eb5/pamqp-3.3.0-py2.py3-none-any.whl", hash = "sha256:c901a684794157ae39b52cbf700db8c9aae7a470f13528b9d7b4e5f7202f8eb0", size = 33848, upload-time = "2024-01-12T20:37:21.359Z" }, ] [[package]] name = "pastedeploy" version = "3.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e3/97/0c4a613ec96a54d21daa7e089178263915554320402e89b4e319436a63cb/PasteDeploy-3.1.0.tar.gz", hash = "sha256:9ddbaf152f8095438a9fe81f82c78a6714b92ae8e066bed418b6a7ff6a095a95", size = 37841, upload-time = "2023-11-21T04:54:33.203Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/85/30/cdddd9a88969683a59222a6d61cd6dce923977f2e9f9ffba38e1324149cd/PasteDeploy-3.1.0-py3-none-any.whl", hash = "sha256:76388ad53a661448d436df28c798063108f70e994ddc749540d733cdbd1b38cf", size = 16943, upload-time = "2023-11-21T04:54:28.226Z" }, ] [[package]] name = "pika" version = "1.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f7/09/8c42dd00c4b3e09ebd174455d30823c261eefef9d2b7e94ae9ca779704c1/pika-1.4.1.tar.gz", hash = "sha256:e851f3e4992adfbf8eb64e9b86d94e3382f92ba0200055abedbb29676b8e713b", size = 154268, upload-time = "2026-05-22T18:01:24.855Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7f/7b/a09c0d378ee8604220902c58103104b2304ef99744c4b7fc45312d280484/pika-1.4.1-py3-none-any.whl", hash = "sha256:2daae7bd422a0fc4f4879fd48c9a1932ed74a0bc7172e1e5f9bde63a101ed074", size = 164962, upload-time = "2026-05-22T18:01:23.429Z" }, ] [[package]] name = "plaster" version = "1.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/26/93/66df0f87f1442d8afea8531ae8a4a9eca656006a54eac2b4489427e92c10/plaster-1.1.2.tar.gz", hash = "sha256:f8befc54bf8c1147c10ab40297ec84c2676fa2d4ea5d6f524d9436a80074ef98", size = 33232, upload-time = "2022-11-21T01:44:54.49Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e7/8b/3f98db1448e3b4d2d142716874a7e02f6101685fdaa0f55a8668e9ffa048/plaster-1.1.2-py2.py3-none-any.whl", hash = "sha256:42992ab1f4865f1278e2ad740e8ad145683bb4022e03534265528f0c23c0df2d", size = 11554, upload-time = "2022-11-21T01:44:52.984Z" }, ] [[package]] name = "plaster-pastedeploy" version = "1.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pastedeploy" }, { name = "plaster" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c7/af/01a22f73ce97c6375c88d7ceaf6f5f4f345e940da93c94f98833d898a449/plaster_pastedeploy-1.0.1.tar.gz", hash = "sha256:be262e6d2e41a7264875daa2fe2850cbb0615728bcdc92828fdc72736e381412", size = 20915, upload-time = "2022-11-07T01:23:05.348Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bd/30/2d4cf89035c22a89bf0e34dbc50fdc07c42c9bdc90fd972d495257ad2b6e/plaster_pastedeploy-1.0.1-py2.py3-none-any.whl", hash = "sha256:ad3550cc744648969ed3b810f33c9344f515ee8d8a8cec18e8f2c4a643c2181f", size = 7823, upload-time = "2022-11-07T01:23:03.23Z" }, ] [[package]] name = "platformdirs" version = "4.10.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pre-commit" version = "4.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, { name = "identify" }, { name = "nodeenv" }, { name = "pyyaml" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, ] [[package]] name = "prometheus-client" version = "0.25.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" }, ] [[package]] name = "prompt-toolkit" version = "3.0.52" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] [[package]] name = "propcache" version = "0.5.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5b/56/030b7b4719d53085722893e0009dffb9236aa10bca1b12121bdc5626ef16/propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b", size = 93417, upload-time = "2026-05-08T20:59:15.597Z" }, { url = "https://files.pythonhosted.org/packages/1a/55/1140a8e067b8ec093a18a4ae7bb0045d9db65da38a08618ddc5e2f1994aa/propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c", size = 53847, upload-time = "2026-05-08T20:59:17.096Z" }, { url = "https://files.pythonhosted.org/packages/20/42/0e7443c90310498561addf346e7d57fe3c6ba1914e1ba938b5464c7bbfd2/propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb", size = 53512, upload-time = "2026-05-08T20:59:18.64Z" }, { url = "https://files.pythonhosted.org/packages/b7/db/cf51a71bab2009517d1a7f0ee07657e3bd446c4d69f67e6966cf17bcf956/propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e", size = 58068, upload-time = "2026-05-08T20:59:20.683Z" }, { url = "https://files.pythonhosted.org/packages/b7/43/39b6bdee9699fa1e1641c519feeb64a67e2a9f93bb465c70776b37a7333f/propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e", size = 61020, upload-time = "2026-05-08T20:59:22.112Z" }, { url = "https://files.pythonhosted.org/packages/26/0b/843726fbb0a29a8c5684fdb25971823638399f31e52e9d1f06a02dc9aa6b/propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b", size = 62732, upload-time = "2026-05-08T20:59:23.805Z" }, { url = "https://files.pythonhosted.org/packages/39/6e/899fed76dc1942b8a64193a4f059d7f1a2c7ef65085e8a9366ed8ec0d199/propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d", size = 60140, upload-time = "2026-05-08T20:59:25.389Z" }, { url = "https://files.pythonhosted.org/packages/ab/09/3da4be9b5b879219ad234aa535b3dd4a080ed1ad48d3a73ca07a9e798f22/propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d", size = 60400, upload-time = "2026-05-08T20:59:27.238Z" }, { url = "https://files.pythonhosted.org/packages/60/2f/09b72b874a9aa0044faf52a69807a6ed618e267ceaa9ec4a63195fa5b504/propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0", size = 58155, upload-time = "2026-05-08T20:59:28.48Z" }, { url = "https://files.pythonhosted.org/packages/8a/37/97489848c54c95578045473954f10956d619ce6a09e7ac137b71cdcb698b/propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b", size = 57037, upload-time = "2026-05-08T20:59:30.146Z" }, { url = "https://files.pythonhosted.org/packages/22/db/6c695285ccfc49012743ee9c98212b8c5dd0aed7b63cfd816d4a0f7a1601/propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf", size = 61103, upload-time = "2026-05-08T20:59:31.626Z" }, { url = "https://files.pythonhosted.org/packages/98/a9/1e500401ca593b0bdb6bf75a70bc2d723835fd53360edff6af70692c7546/propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf", size = 60394, upload-time = "2026-05-08T20:59:32.829Z" }, { url = "https://files.pythonhosted.org/packages/1f/87/f638b6e375eae0f30a1a2325d8b34fd85fdc785bb9960cf805f3bf1ec69a/propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e", size = 63084, upload-time = "2026-05-08T20:59:35.964Z" }, { url = "https://files.pythonhosted.org/packages/f6/18/884573f5d97b6d9eba68de759a82c901b7e39d7904d30f7b8d58d42d2a12/propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274", size = 60999, upload-time = "2026-05-08T20:59:38.481Z" }, { url = "https://files.pythonhosted.org/packages/8f/1a/c3915eb059ceec9e758a56e4cfd955292bc0f201be2176a46b76d94b303a/propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe", size = 39036, upload-time = "2026-05-08T20:59:40.323Z" }, { url = "https://files.pythonhosted.org/packages/5b/02/1dfd5607501a602d19c1c449d2d193b7d1c611f9246b4059026a1189a80e/propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d", size = 42190, upload-time = "2026-05-08T20:59:42.232Z" }, { url = "https://files.pythonhosted.org/packages/57/93/f71588ad08b3e6f4b555b5ef215808a3c02b042d0151ad82fa6f15be677a/propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5", size = 38545, upload-time = "2026-05-08T20:59:44.087Z" }, { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" }, { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033, upload-time = "2026-05-08T20:59:47.408Z" }, { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" }, { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573, upload-time = "2026-05-08T20:59:50.778Z" }, { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645, upload-time = "2026-05-08T20:59:52.227Z" }, { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563, upload-time = "2026-05-08T20:59:53.866Z" }, { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888, upload-time = "2026-05-08T20:59:55.457Z" }, { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253, upload-time = "2026-05-08T20:59:57.075Z" }, { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558, upload-time = "2026-05-08T20:59:58.602Z" }, { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007, upload-time = "2026-05-08T20:59:59.837Z" }, { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355, upload-time = "2026-05-08T21:00:01.144Z" }, { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057, upload-time = "2026-05-08T21:00:02.401Z" }, { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938, upload-time = "2026-05-08T21:00:03.638Z" }, { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731, upload-time = "2026-05-08T21:00:04.881Z" }, { url = "https://files.pythonhosted.org/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0", size = 38966, upload-time = "2026-05-08T21:00:06.511Z" }, { url = "https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82", size = 42135, upload-time = "2026-05-08T21:00:08.088Z" }, { url = "https://files.pythonhosted.org/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab", size = 38381, upload-time = "2026-05-08T21:00:09.692Z" }, { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] [[package]] name = "proto-plus" version = "1.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c9/56/e647b0c675392d2da368da7b6f158f7368b18542fd6f7d7400a2f39de000/proto_plus-1.28.0.tar.gz", hash = "sha256:38e5696342835b08fc116f30a25665b29531cda9d5d5643e9b81fc312385abd9", size = 57221, upload-time = "2026-05-07T08:04:50.811Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7c/20/b122d4626976acb81132036d2ad1bb35a1a8775fceb837ec30964622516a/proto_plus-1.28.0-py3-none-any.whl", hash = "sha256:a630604310899e73c59ec302e5765c058d412b2f090b9c79c8822589f14955b8", size = 50410, upload-time = "2026-05-07T08:03:31.962Z" }, ] [[package]] name = "protobuf" version = "6.33.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] [[package]] name = "psutil" version = "7.2.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] [[package]] name = "psycopg" version = "3.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" }, ] [[package]] name = "psycopg2" version = "2.9.12" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c7/bc/f66df707ed1aec949fbf24e4460e4f4277a7ba23cdadb3965bb1f634ddb9/psycopg2-2.9.12.tar.gz", hash = "sha256:1dedb1c7a1d8552c4a6044c6b1c41a52e6a8e2d144af83eccac758076b1b7c15", size = 379683, upload-time = "2026-04-20T23:36:11.013Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/14/7e/940ade9113c6cdd8b4c0b27f820006bdf91c30170a5a95e8332104d5d3ed/psycopg2-2.9.12-cp310-cp310-win_amd64.whl", hash = "sha256:d5fbe092315fb007c03544704e6d1e678a6c0378139d01cea433dc59edf041b4", size = 2756868, upload-time = "2026-04-20T23:33:13.049Z" }, { url = "https://files.pythonhosted.org/packages/cc/a1/9cc069f8efc92383d3823ef8805c932f5fda723b3f07d2a8ee019ae2919f/psycopg2-2.9.12-cp311-cp311-win_amd64.whl", hash = "sha256:2532c0cdc6ad18c9c35cd935cc3159712e14f05276a6d29a6435c52d24b840c1", size = 2756862, upload-time = "2026-04-20T23:33:17.206Z" }, { url = "https://files.pythonhosted.org/packages/7e/b2/7319dc488444b1dfe9ce89c4440df1d5425e2579e6ce9a63e2b70f648491/psycopg2-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:83d48e66e18c301d832e93c984a7bcbc0f4ac3bb79e2137e3bc335978c756dc0", size = 2757068, upload-time = "2026-04-20T23:33:20.664Z" }, { url = "https://files.pythonhosted.org/packages/91/ab/03403779a251ca14a7f1b07a41c7aa735fd451f0aebe4c4f901a231167a4/psycopg2-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:3d23e684927d37b95cee9a943f6927b04ae2fdcd056fd0e2a30929ee89fee5a9", size = 2757176, upload-time = "2026-04-20T23:33:23.577Z" }, { url = "https://files.pythonhosted.org/packages/73/4a/a3566f77501c21a6c2a1fc234dbe5dff74a86e3f150ef070e4ffb835e7f9/psycopg2-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:a73d5513bfe929c56555006c7a9cc7ae6e4276aa99dd2b1e2544eb8bb54f8b23", size = 2848588, upload-time = "2026-04-20T23:33:25.983Z" }, ] [[package]] name = "psycopg2-binary" version = "2.9.12" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/80/49bacf9e51617d8309f6f0123e29edc793f6f5f6700c7d1f1b20782fbb37/psycopg2_binary-2.9.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b818ceff717f98851a64bffd4c5eb5b3059ae280276dcecc52ac658dcf006a4", size = 3712314, upload-time = "2026-04-20T23:33:31.363Z" }, { url = "https://files.pythonhosted.org/packages/d3/f2/98eeac7d60c43df9338287834edf9b3e69be68a2db78a57b1b81d705e735/psycopg2_binary-2.9.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2fa0d7caca8635c56e373055094eeda3208d901d55dd0ff5abc1d4e47f82b56", size = 3822389, upload-time = "2026-04-20T23:33:34.178Z" }, { url = "https://files.pythonhosted.org/packages/9f/7c/30575e75f14d5351a56a1971bb43fe7f8bf7edf1b654fb1bec65c42a8812/psycopg2_binary-2.9.12-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:864c261b3690e1207d14bbfe0a61e27567981b80c47a778561e49f676f7ce433", size = 4578448, upload-time = "2026-04-20T23:33:37.073Z" }, { url = "https://files.pythonhosted.org/packages/65/9b/4df366d89f28c527dc39d0b6c98a5ca74e30d37ac097b73f3352147568ae/psycopg2_binary-2.9.12-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c5ee5213445dd45312459029b8c4c0a695461eb517b753d2582315bd07995f5e", size = 4273705, upload-time = "2026-04-20T23:33:39.291Z" }, { url = "https://files.pythonhosted.org/packages/4e/8a/c566803818eb03161ba869b6ba612bf7ad56816d98b9e5121e0a22ad6b0b/psycopg2_binary-2.9.12-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f9cae1f848779b5b01f417e762c40d026ea93eb0648249a604728cda991dde3", size = 5893784, upload-time = "2026-04-20T23:33:41.658Z" }, { url = "https://files.pythonhosted.org/packages/63/fe/0dfa5797e0b229e0567bc378695224caf14d547f73b05be0c80549089772/psycopg2_binary-2.9.12-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:63a3ebbd543d3d1eda088ac99164e8c5bac15293ee91f20281fd17d050aee1c4", size = 4109306, upload-time = "2026-04-20T23:33:43.953Z" }, { url = "https://files.pythonhosted.org/packages/3c/89/28063adf17a4ba501eedd9890feab0c649ee4d8bd0a97df0ff1e9584feab/psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d6fcbba8c9fed08a73b8ac61ea79e4821e45b1e92bb466230c5e746bbf3d5256", size = 3654400, upload-time = "2026-04-20T23:33:46.115Z" }, { url = "https://files.pythonhosted.org/packages/84/94/5a01de0aa4ead0b8d8d1aa4ec18cec0bd36d03fa714eaa5bb8a0b1b50020/psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:36512911ebb2b60a0c3e44d0bb5048c1980aced91235d133b7874f3d1d93487c", size = 3299215, upload-time = "2026-04-20T23:33:48.202Z" }, { url = "https://files.pythonhosted.org/packages/7a/85/723bb085a61c6ac2dc0a0043f375f2fe7365363e27b073bad56ca5bda979/psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:8ffdb59fe88f99589e34354a130217aa1fd2d615612402d6edc8b3dbc7a44463", size = 3047724, upload-time = "2026-04-20T23:33:50.74Z" }, { url = "https://files.pythonhosted.org/packages/b4/67/4d8b1e0d2fc4166677380eac0edf9cdff91013aca2546e8ef7bc04b56158/psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a46fe069b65255df410f856d842bc235f90e22ffdf532dda625fd4213d3fd9b1", size = 3349183, upload-time = "2026-04-20T23:33:59.635Z" }, { url = "https://files.pythonhosted.org/packages/73/99/21af7a5498637ea4dc91a17c281a53bc1d632fbafe00f6689fbfb32a9fed/psycopg2_binary-2.9.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab29414b25dcb698bf26bf213e3348abdcd07bbd5de032a5bec15bd75b298b03", size = 2757036, upload-time = "2026-04-20T23:34:01.606Z" }, { url = "https://files.pythonhosted.org/packages/d5/19/d4ce60954f3bb9d8e3bc5e5c4d1f2487de2d3851bf2391d54954c9df12a6/psycopg2_binary-2.9.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5c8ce6c61bd1b1f6b9c24ee32211599f6166af2c55abb19456090a21fd16554b", size = 3712338, upload-time = "2026-04-20T23:34:03.961Z" }, { url = "https://files.pythonhosted.org/packages/53/71/c85409ee0d78890f0660eff262e815e7dd2bb741a17611d82e9e8cd9dc5e/psycopg2_binary-2.9.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326", size = 3822407, upload-time = "2026-04-20T23:34:05.977Z" }, { url = "https://files.pythonhosted.org/packages/3c/ed/60486c2c7f0d4d1ede2bfb1ed27e2498477ce646bc7f6b2759906303117e/psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06", size = 4578425, upload-time = "2026-04-20T23:34:08.246Z" }, { url = "https://files.pythonhosted.org/packages/0b/b9/656cb03fad9f4f49f2145c334b1126ee75189929ca4e6187d485a2d59951/psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8", size = 4273709, upload-time = "2026-04-20T23:34:10.974Z" }, { url = "https://files.pythonhosted.org/packages/99/66/08cf0da0e25cc6fb142c89be45fc8418792858f0c4cbff5e24530ff02cd6/psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3", size = 5893779, upload-time = "2026-04-20T23:34:13.905Z" }, { url = "https://files.pythonhosted.org/packages/17/d7/eecd9ce8e146d3721115d82d3836efdbb712187e4590325df549989d18f4/psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39", size = 4109308, upload-time = "2026-04-20T23:34:16.761Z" }, { url = "https://files.pythonhosted.org/packages/b6/2e/b1dc289b362cc8d45697b57eefbd673186f49a4ea0906928988e3affcc98/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c", size = 3654405, upload-time = "2026-04-20T23:34:19.303Z" }, { url = "https://files.pythonhosted.org/packages/eb/e4/4c4aea6473214dbdbd0fbba11aa4691e76dc01722c55724c5951719865ff/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a", size = 3299187, upload-time = "2026-04-20T23:34:21.206Z" }, { url = "https://files.pythonhosted.org/packages/ba/5d/b03b99986446a4f57b170ed9a2579fb7ff9783ca0fa5226b19db99737fee/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c", size = 3047716, upload-time = "2026-04-20T23:34:23.077Z" }, { url = "https://files.pythonhosted.org/packages/14/86/382ee4afbd1d97500c9d2862b20c2fdeddf4b7335e984df3fb4309f64108/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be", size = 3349237, upload-time = "2026-04-20T23:34:25.211Z" }, { url = "https://files.pythonhosted.org/packages/a8/16/9a57c75ba1eda7165c017342f526810d5f5a12647dde749c99ae9a7141d7/psycopg2_binary-2.9.12-cp311-cp311-win_amd64.whl", hash = "sha256:cb4a1dacdd48077150dc762a9e5ddbf32c256d66cb46f80839391aa458774936", size = 2757036, upload-time = "2026-04-20T23:34:27.77Z" }, { url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" }, { url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" }, { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" }, { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" }, { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" }, { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" }, { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" }, { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" }, { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" }, { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" }, { url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" }, { url = "https://files.pythonhosted.org/packages/91/bb/4608c96f970f6e0c56572e87027ef4404f709382a3503e9934526d7ba051/psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", size = 3712419, upload-time = "2026-04-20T23:34:58.754Z" }, { url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990, upload-time = "2026-04-20T23:35:00.821Z" }, { url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696, upload-time = "2026-04-20T23:35:03.409Z" }, { url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982, upload-time = "2026-04-20T23:35:05.583Z" }, { url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867, upload-time = "2026-04-20T23:35:07.902Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578, upload-time = "2026-04-20T23:35:10.149Z" }, { url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816, upload-time = "2026-04-20T23:35:12.56Z" }, { url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307, upload-time = "2026-04-20T23:35:15.029Z" }, { url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968, upload-time = "2026-04-20T23:35:16.757Z" }, { url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369, upload-time = "2026-04-20T23:35:18.933Z" }, { url = "https://files.pythonhosted.org/packages/42/ee/dee8dcaad07f735824de3d6563bc67119fa6c28257b17977a8d624f02fab/psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", size = 2757347, upload-time = "2026-04-20T23:35:21.283Z" }, { url = "https://files.pythonhosted.org/packages/13/1b/708c0dca874acfad6d65314271859899a79007686f3a1f74e82a2ed4b645/psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", size = 3712428, upload-time = "2026-04-20T23:35:23.453Z" }, { url = "https://files.pythonhosted.org/packages/d6/39/ddbea9d4b4de6aca9431b6ed253f530f8a02d3b8f9bcfd0dbfe2b3de6fe4/psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", size = 3823184, upload-time = "2026-04-20T23:35:25.92Z" }, { url = "https://files.pythonhosted.org/packages/bf/a0/bc2fef74b106fa345567122a0659e6d94512ed7dc0131ec44c9e5aba3725/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", size = 4579157, upload-time = "2026-04-20T23:35:28.542Z" }, { url = "https://files.pythonhosted.org/packages/57/d7/d4e3b2005d3de607ca4fbb0e8742e248056e52184a6b94ebda3c1c2c329b/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", size = 4274970, upload-time = "2026-04-20T23:35:30.418Z" }, { url = "https://files.pythonhosted.org/packages/2e/42/c9853f8db3967fe08bcde11f53d53b85d351750cae726ce001cb68afa9c1/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", size = 5895175, upload-time = "2026-04-20T23:35:33.584Z" }, { url = "https://files.pythonhosted.org/packages/eb/fd/b82b5601a97630308bef079f545ffec481bbbc795c2ba5ec416a01d03f60/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e", size = 4110658, upload-time = "2026-04-20T23:35:35.638Z" }, { url = "https://files.pythonhosted.org/packages/62/8c/32ca69b0389ef25dd22937bf9e8fbe2ce27aea20b05ded48c4ce4cb42475/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", size = 3656251, upload-time = "2026-04-20T23:35:37.854Z" }, { url = "https://files.pythonhosted.org/packages/c4/29/96992a2b59e3b9d730fcf9612d0a387305025dc867a9fc490a9e496e074e/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", size = 3301810, upload-time = "2026-04-20T23:35:39.927Z" }, { url = "https://files.pythonhosted.org/packages/56/ad/44b06659949b243ae10112cd3b20a197f9bf3e81d5651379b9eb889bfaad/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", size = 3048977, upload-time = "2026-04-20T23:35:41.806Z" }, { url = "https://files.pythonhosted.org/packages/1d/f2/10a1bcebadb6aa55e280e1f58975c36a7b560ea525184c7aa4064c466633/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", size = 3351466, upload-time = "2026-04-20T23:35:43.993Z" }, { url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762, upload-time = "2026-04-20T23:35:46.421Z" }, ] [[package]] name = "pyasn1" version = "0.6.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, ] [[package]] name = "pyasn1-modules" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, ] [[package]] name = "pycparser" version = "3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] name = "pydantic" version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [[package]] name = "pydantic-core" version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, ] [[package]] name = "pydantic-settings" version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, ] [[package]] name = "pygments" version = "2.20.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pyjwt" version = "2.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, ] [package.optional-dependencies] crypto = [ { name = "cryptography" }, ] [[package]] name = "pymemcache" version = "4.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d9/b6/4541b664aeaad025dfb8e851dcddf8e25ab22607e674dd2b562ea3e3586f/pymemcache-4.0.0.tar.gz", hash = "sha256:27bf9bd1bbc1e20f83633208620d56de50f14185055e49504f4f5e94e94aff94", size = 70176, upload-time = "2022-10-17T16:53:07.726Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/41/ba/2f7b22d8135b51c4fefb041461f8431e1908778e6539ff5af6eeaaee367a/pymemcache-4.0.0-py2.py3-none-any.whl", hash = "sha256:f507bc20e0dc8d562f8df9d872107a278df049fa496805c1431b926f3ddd0eab", size = 60772, upload-time = "2022-10-17T16:53:04.388Z" }, ] [[package]] name = "pymongo" version = "4.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dnspython" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ca/64/50be6fbac9c79fe2e4c17401a467da2d8764d82833d83cec325afe5cab32/pymongo-4.17.0.tar.gz", hash = "sha256:70ffa08ba641468cc068cf46c06b34f01a8ce3489f6411309fcb5ceabe6b2fc0", size = 2523370, upload-time = "2026-04-20T16:39:53.524Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c9/77/28ebbf69772a4341d530831c7a006cdb06877ac23075cb53b0a227df4fe1/pymongo-4.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:47b021363cd923ace5edc7a1d63c0ff8a6d9d43859b8a1ba23645f5afae63221", size = 819234, upload-time = "2026-04-20T16:37:20.888Z" }, { url = "https://files.pythonhosted.org/packages/88/cf/5a70cee503ff9a2fea20607607f14d189f4d975960ac0945ec306ee7b695/pymongo-4.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:422fa50d7d7f5c22ea0953554396c9ef95684a2d775f860bd75a7b510538dfca", size = 819969, upload-time = "2026-04-20T16:37:24.187Z" }, { url = "https://files.pythonhosted.org/packages/23/d5/07b7e27e662c58d872efd104a0e8055eb6569aa1b6d4da436f3fdee7f897/pymongo-4.17.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:addd0498ebbdc6354227f6ed457ed9fce442d48a3bb30d5b5bad33e104996561", size = 1244510, upload-time = "2026-04-20T16:37:26.069Z" }, { url = "https://files.pythonhosted.org/packages/fb/be/7cac5b1e89bd5a8e395067648241390321593a7c29243e36f91343c02a90/pymongo-4.17.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5c8e180cb2cabe37300e1e36c60aa4f2ff956cc579f0142135a5d2cba252243", size = 1263245, upload-time = "2026-04-20T16:37:28.003Z" }, { url = "https://files.pythonhosted.org/packages/2e/20/40e8e99824c1fda18261411e65ce3b0cd3d9a6ed3c056cdd0a569adc870b/pymongo-4.17.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bd835cdb37a1adec359dd072c24f8bb14809e2644fde86fab4ee2fc9719b9483", size = 1304113, upload-time = "2026-04-20T16:37:30.048Z" }, { url = "https://files.pythonhosted.org/packages/3a/94/fb7e25441dd66f2069a9b172380849b0eaa5881c18b3db217bf64a6d393c/pymongo-4.17.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c4979e7e8887862bbb44d203f00cc8263a3f27237876fa691b6beba23e40e6d8", size = 1297046, upload-time = "2026-04-20T16:37:32.054Z" }, { url = "https://files.pythonhosted.org/packages/4f/c9/7352e0c20fe772541556e4d283c05e07ec48f8b0d2737ad930ac4a1b6655/pymongo-4.17.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:77aa4bc164b4de60d5db193b322f0f5b6ead716e831031bfdef8e8bd92205556", size = 1265708, upload-time = "2026-04-20T16:37:33.934Z" }, { url = "https://files.pythonhosted.org/packages/8d/e4/3df15494c2015ed297958517f0e4f6493e21b00990748068a973e66d45e0/pymongo-4.17.0-cp310-cp310-win32.whl", hash = "sha256:48bbc576677b50af043df870d84ded67cc3a9b4aa7553201beef4da5dc050a0a", size = 805533, upload-time = "2026-04-20T16:37:35.744Z" }, { url = "https://files.pythonhosted.org/packages/22/fa/b4e71bb8cb82ad7d21bb4e8c476f2d573ba68b20368aac36ef06e4a196b4/pymongo-4.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46767f28dea610e02edf6c5d956ce615c3c7790ea396660b9b1efd5c5ead2e0", size = 815677, upload-time = "2026-04-20T16:37:37.808Z" }, { url = "https://files.pythonhosted.org/packages/22/e2/0a4bba644f1cda3970ea1012149eeae3594ebfeed3f81fdaf32b61d90c95/pymongo-4.17.0-cp310-cp310-win_arm64.whl", hash = "sha256:757f2a4c0c2c46cab87df0333681ce69e86c9d5b45bc5203ceba5410b3489e59", size = 807293, upload-time = "2026-04-20T16:37:39.707Z" }, { url = "https://files.pythonhosted.org/packages/c4/e2/336d86f221cf1b56b2ed9330d4a3b98f9f38f0b37829ae9a9184617d5419/pymongo-4.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4141e6c6a339789b2974efa00ecd9409101672d77a0e3ee2cc3839eedf8ec4df", size = 874668, upload-time = "2026-04-20T16:37:41.39Z" }, { url = "https://files.pythonhosted.org/packages/34/8e/75d3c6c935d187ab59c61e9c15d9aab3f274b563eaf1706e8cae5f508dec/pymongo-4.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e68c76b84e0c132d9dbf9307f12ff8185702328187a87b9aca8c941303873433", size = 875294, upload-time = "2026-04-20T16:37:43.432Z" }, { url = "https://files.pythonhosted.org/packages/5f/ec/62e855744489dbcd54fd778aae4d80fa4c4819e8fb228ca0cf6f21a03997/pymongo-4.17.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba2195d4f386f839a52a23ea1cfd60ffaaba78a3d7841db51b7e433001139918", size = 1496233, upload-time = "2026-04-20T16:37:45.518Z" }, { url = "https://files.pythonhosted.org/packages/82/e8/93e4e5e5ce8fdf8929dabeefe24aafa5ce046028eed0dfa8eeb936e72c49/pymongo-4.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8446ff4bfcb6ec2a2e50998c860986a1e992136f998b7f53e7a717fb8aa5a0b9", size = 1522927, upload-time = "2026-04-20T16:37:47.492Z" }, { url = "https://files.pythonhosted.org/packages/f7/ca/425dc1d21e0f17bdea0072fc463f662f7fa06d2852af52975c9eced3c07c/pymongo-4.17.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2a0d5ac205728c86e0a02192f1aa5f865b0d7d51f8df6101c01a69a7fc620d72", size = 1583468, upload-time = "2026-04-20T16:37:49.221Z" }, { url = "https://files.pythonhosted.org/packages/b3/9d/f08b07eeffda1a43c1759f0fa625e88ae12360996eb56d42aad832fa7dff/pymongo-4.17.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:485c8a8eaa4c739f00a331fc73757898ee7c092c214a79e63866ff76aaf282ff", size = 1572787, upload-time = "2026-04-20T16:37:51.061Z" }, { url = "https://files.pythonhosted.org/packages/e9/c2/6855a07aafa7b894929af23675b6fb9634800ce43122b76a62f6eeb8da2a/pymongo-4.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b2dfcc795f5b9fedbe179a11fdf6051581479d196582a3fe819a92a00e9b9969", size = 1526184, upload-time = "2026-04-20T16:37:53.358Z" }, { url = "https://files.pythonhosted.org/packages/4e/05/c952bac7db71c1942ea3559fcd308b49754cc5004b455935fb4000d1f37b/pymongo-4.17.0-cp311-cp311-win32.whl", hash = "sha256:c2292144505fb12156b981bd440f3dc994a883da06ac726c0c8692ccdbc1c510", size = 852621, upload-time = "2026-04-20T16:37:55.28Z" }, { url = "https://files.pythonhosted.org/packages/11/c0/c04da9f4c0c6252404598f4e394b862a58a9e866822a70ae261c8a018fdf/pymongo-4.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:2e190827834fce70ecdf9d46796c6dbc0ce08ea87dc2ff5bc6f3f5579b605cb9", size = 867852, upload-time = "2026-04-20T16:37:57.233Z" }, { url = "https://files.pythonhosted.org/packages/1d/b2/c7b4870fbeef471e947d3e014676f5910d02e0197074d692ebcf24ec049a/pymongo-4.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:a8f9c40a09bb7d4b9fc8b1da65ecf6efa79bda5cb2756f39d9b6940fac1d19ae", size = 855019, upload-time = "2026-04-20T16:37:58.983Z" }, { url = "https://files.pythonhosted.org/packages/98/90/60bcb508840135d5ee46b51b1a950f548338aa8145a8366dbe6639ae51ac/pymongo-4.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53ffa94b2340dbf6b055e09a0090618c60482c158ecfc9565642fc996bf0944", size = 930529, upload-time = "2026-04-20T16:38:00.936Z" }, { url = "https://files.pythonhosted.org/packages/a6/e9/313840f1e52c6dfac47f704428cbfbce59956ebe7633bffc92b03f74f0ad/pymongo-4.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6fe0de9d0f6791abce3471230b32b4817bf89d27b1182b6a550e1ec0fa72aa9a", size = 930665, upload-time = "2026-04-20T16:38:02.915Z" }, { url = "https://files.pythonhosted.org/packages/78/35/9d3565ea45b1606f635c1e2cd2563c28d66caafdc50f7ad7d979fcd1b363/pymongo-4.17.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e537e95514dae1aaa718f481ec03151a0f0394bcd05f1322896d8fc1330cb729", size = 1762369, upload-time = "2026-04-20T16:38:05.375Z" }, { url = "https://files.pythonhosted.org/packages/95/ee/149b0d4b1a11c38bff6f14c23d5814c9b0843fd6dc38ad40596bdb1a62d2/pymongo-4.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37a8385c29881b43eab31f584100fa0eaddedd5607adf010147ba1810118be90", size = 1798044, upload-time = "2026-04-20T16:38:07.195Z" }, { url = "https://files.pythonhosted.org/packages/7b/d4/4cee4a7b8d8f6f0550ef6cd2fea42455c5ed619a220cb6ba4fb40d6a5bc8/pymongo-4.17.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f3ee3d241ed77a4fc99ce3cff3b289c3ebce37f61fdd7349d3592c23b82c8784", size = 1878567, upload-time = "2026-04-20T16:38:09.121Z" }, { url = "https://files.pythonhosted.org/packages/45/ef/7fe366c84952619ee2f69973566c214775e083dd4df465751912153e4b72/pymongo-4.17.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9eb5d63a3c518cb0804ed678f5e2b875af032d89a7cf57a57360322cf6a4d222", size = 1864881, upload-time = "2026-04-20T16:38:10.896Z" }, { url = "https://files.pythonhosted.org/packages/2f/35/b577d82c6d1be7aee7ac7e249bc86f7847998345042e5f8360de238e177b/pymongo-4.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e97e03fa13327c87e3fdc5656acd01e71817f0c1dc3221cd8f30de136bf4ec3", size = 1800349, upload-time = "2026-04-20T16:38:13.589Z" }, { url = "https://files.pythonhosted.org/packages/b8/69/dafcf04f66e130ddd91aeb92e7a692480eda46dcd04ec1dbe82c06619e10/pymongo-4.17.0-cp312-cp312-win32.whl", hash = "sha256:6877214bff5f06f6884a9fc8d9016a4a7a5f51f537f5c51ac3a576f93e7dfb32", size = 900518, upload-time = "2026-04-20T16:38:15.541Z" }, { url = "https://files.pythonhosted.org/packages/11/35/5c9262a459f988b4eb2605f70815240b77a0d4131136c4326d18f1822b89/pymongo-4.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:9828485f72f63c7d802e0ec41f71906f633c2692621ab3af55ca990186b091b1", size = 920335, upload-time = "2026-04-20T16:38:17.665Z" }, { url = "https://files.pythonhosted.org/packages/8d/da/e9c7265ee176faccf4e52c4797837e794d93569a1046f6b19a4acc36e5ad/pymongo-4.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:1195370a77baf003b59b10e91ecc4706297197f0dd9d29c840cc556dc08f7cee", size = 903289, upload-time = "2026-04-20T16:38:19.33Z" }, { url = "https://files.pythonhosted.org/packages/2a/6b/c1206879708b94e82fcd8b9653440ec271f79a3674d122192df383047f5a/pymongo-4.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:809ec74de3b9148ae43fa8df9faf53470f511c8d384f13b99d6f671f2a379f15", size = 985829, upload-time = "2026-04-20T16:38:21.031Z" }, { url = "https://files.pythonhosted.org/packages/cb/cf/bb044ed85160e5c40f568c7c4f4e8ea16f40764ff5d302e5befbe8f6f814/pymongo-4.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a431b737816bf4cddd4fa0fcef04e424ad36b7692734a64150f872fb8f3208be", size = 985899, upload-time = "2026-04-20T16:38:23.409Z" }, { url = "https://files.pythonhosted.org/packages/74/0a/f6dfd5ea3901e5d6888da8de8ba728971a1d447debab681cfc56f90d1208/pymongo-4.17.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e4fab10f8403169ce92f3cea921609d9ee81107306caae06c08f592d4b8ad2b5", size = 2028569, upload-time = "2026-04-20T16:38:25.343Z" }, { url = "https://files.pythonhosted.org/packages/4a/c5/081f59a1c02ae8c0dc73ae58e563838c44eec81aeafa7d0b93a637841c9b/pymongo-4.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20323b0b1c1d33770ad1fc68d429c757734ce9ad3594421c3d6618f10572b1b9", size = 2072916, upload-time = "2026-04-20T16:38:27.291Z" }, { url = "https://files.pythonhosted.org/packages/31/42/6e41d434297ffe8b30d9c3717916591a4a7be9075a0dcc2fafdfaaaa62ed/pymongo-4.17.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5a5de048e6da5c18e27cc2437e8c15b3b0cdc8385c15b41178b0caa3322a09c2", size = 2173234, upload-time = "2026-04-20T16:38:29.474Z" }, { url = "https://files.pythonhosted.org/packages/3d/cf/1e4a7db352ef9485831c7268dfe8402f0117b32a9ad54b16e810699e3617/pymongo-4.17.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dff3de1294fbbc1db0ba6b511f77b8e540601d092538a31312e99c8a91a78b1e", size = 2156784, upload-time = "2026-04-20T16:38:32.134Z" }, { url = "https://files.pythonhosted.org/packages/12/10/6195be29962a61ebb5f4bd9e4c7519890b172f7968a0a0d880398c6ddb02/pymongo-4.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faf03e4c2aafd6de626dbd30ba246d369ae33f47f10629d1bbe40f72115027a6", size = 2074446, upload-time = "2026-04-20T16:38:34.004Z" }, { url = "https://files.pythonhosted.org/packages/37/48/33410b8819837ed370c738587306bdf060b59cef11823be212f4a07703c5/pymongo-4.17.0-cp313-cp313-win32.whl", hash = "sha256:c9786665926a09630c5d420c79762cfadbff35a9438bcbc4c81a9fb5ab9228b7", size = 948435, upload-time = "2026-04-20T16:38:35.922Z" }, { url = "https://files.pythonhosted.org/packages/6f/77/c0ed522f798a286b99acaa7914ed8d9c80ab091f97f57c59ffed72906e5e/pymongo-4.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:5960519b4d7168f1ecdd3ea10c81b2aedeb9423651aca953cfbc8e76705d3b38", size = 972847, upload-time = "2026-04-20T16:38:37.888Z" }, { url = "https://files.pythonhosted.org/packages/97/f0/c39480a2db385fde23861d0c8acda41cdaf1d43e46579db72c5c013a2e81/pymongo-4.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:0ff6bd2f735ab5356541e3e57d5b7dbfbc3f2ee1ccb10b6b0f82d58af69d1d8e", size = 951575, upload-time = "2026-04-20T16:38:40.544Z" }, { url = "https://files.pythonhosted.org/packages/da/49/2b0250762a89737ed6f9cea238331baca061b89a8ddd10dd17fee52c3970/pymongo-4.17.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff5aa3f1c7e3f08eb0e7a016c91ba468b1850ccfd63d9b1f12f56350f4974cef", size = 1040945, upload-time = "2026-04-20T16:38:42.783Z" }, { url = "https://files.pythonhosted.org/packages/89/1c/7a9b5447a08be20e84b6e5b17330917e8d6d9507daa3cd099a9309f11ad7/pymongo-4.17.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e816db649ba5d7de0568cf3a9f287a9dc9aad21cf0ca667ab156a7ef47fca0b0", size = 1041187, upload-time = "2026-04-20T16:38:45.358Z" }, { url = "https://files.pythonhosted.org/packages/78/a1/71704f61632dfc90407a5834fe5f6132854937c4a3648f6c05c351d85a45/pymongo-4.17.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c4fded3a9f1d6a687e36ebd384ac6d00b9b00de1969aa74048e7051ec2a713", size = 2294806, upload-time = "2026-04-20T16:38:47.734Z" }, { url = "https://files.pythonhosted.org/packages/ad/b9/aff42be75108b96c2469b1d9329b912c15108f3e7ef32fdc86da8423c330/pymongo-4.17.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2db66aa8dd253a0fc1fad3b0d23d5b3993f7ebde02fbbd7727128debf2853675", size = 2348231, upload-time = "2026-04-20T16:38:50.371Z" }, { url = "https://files.pythonhosted.org/packages/f2/30/44c115b8ba1479942c15fd9480eb29a7da0ba68acd56983423ba0deb4a94/pymongo-4.17.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3987e96e7c7be4083d42e8ac2cc6c0d5b78db9973c90fce42ae800b616ca6b20", size = 2467614, upload-time = "2026-04-20T16:38:52.665Z" }, { url = "https://files.pythonhosted.org/packages/d2/84/21ee95c8bf0ca7acae7ec7eb365d740bf8fc0156c194baf2c3bdfcb85ec0/pymongo-4.17.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cee36b3c0d0354f880fa7a7fdcdaf2bb5e542c2281e25c1bfadf8cfe21eba7d2", size = 2445970, upload-time = "2026-04-20T16:38:55.175Z" }, { url = "https://files.pythonhosted.org/packages/06/89/081d7f1809d5ca09d1e47e49f2111b245f5694de3a7af32cd3a353a6f43f/pymongo-4.17.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:320b34457b20bbcc79997801f95d25ce00472915ca5241167242b42c4359e027", size = 2348605, upload-time = "2026-04-20T16:38:57.557Z" }, { url = "https://files.pythonhosted.org/packages/ea/c3/0d949f9d3f2a341c1f635c398c16615e96f89f51ff424ed81e914cf1a4de/pymongo-4.17.0-cp314-cp314-win32.whl", hash = "sha256:df4a644af9ae132d4bfdb2e9516ea51a615fd881caddfbfbd071cf1354844479", size = 1004119, upload-time = "2026-04-20T16:39:00.309Z" }, { url = "https://files.pythonhosted.org/packages/f7/55/5c3a3db1048054c695c75c5964cc8bedc2247fdb5a75ef6fab4ec8bb013e/pymongo-4.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:c797f8a80957134f6dd9690367a0f8f5906d672119af2c6aa55f0c527b656bed", size = 1032314, upload-time = "2026-04-20T16:39:02.665Z" }, { url = "https://files.pythonhosted.org/packages/e0/19/e235f39906134cb0ffd5574c5a59c355ef5380f0499644ab94994afbb109/pymongo-4.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:68fca71e05ee5da23a8d73cee8379dfb3d26e609a377cae731d742771ed96946", size = 1007627, upload-time = "2026-04-20T16:39:04.678Z" }, { url = "https://files.pythonhosted.org/packages/1e/e0/c4c1a86791415b14c684fa0908f9da96de91594a3fd1fa1b8dc689fbb800/pymongo-4.17.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b4384700cffc3f1dd98e088bc0072dedf6d7d68a230bb4b972665cf69c071c1e", size = 1099151, upload-time = "2026-04-20T16:39:06.969Z" }, { url = "https://files.pythonhosted.org/packages/81/4b/69c67f3e23fd9b23b9bedc7ebd23754881cc9d5c5d5b2a9811e96b07f475/pymongo-4.17.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:93641192644fa1ee0f34030e774fd31022a27ad11ba22cb1716142231524f8bd", size = 1099346, upload-time = "2026-04-20T16:39:08.996Z" }, { url = "https://files.pythonhosted.org/packages/a2/19/a5208f62f9508a26d73acc69bd3821b8c8adae253679a3c26d2f9652f0d5/pymongo-4.17.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75bc3aa5b94fdb7138d357ec6ca61cd97e0c79f4f7f0bd3efe9639b15cc50942", size = 2619034, upload-time = "2026-04-20T16:39:11.049Z" }, { url = "https://files.pythonhosted.org/packages/77/27/426cba1ec5973082a56d4150798529bfdf4151c31391ed1fbbecb23ef2ac/pymongo-4.17.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e8f8e23c6df7c6d6929f5e734980b227706e73ee847517c9ba5af90f7fc466", size = 2689939, upload-time = "2026-04-20T16:39:13.617Z" }, { url = "https://files.pythonhosted.org/packages/ef/2e/f70993d1255e33f6ee59a4ec4371cc65bff7a7e3fda7d55c3386f25287e8/pymongo-4.17.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:15d3f3d732aecac1f8d481bde4029755615639bd3076f258a2147210aec8515a", size = 2824994, upload-time = "2026-04-20T16:39:16.057Z" }, { url = "https://files.pythonhosted.org/packages/b3/eb/87b0e988ba889e1fcc3430c2cfc166b251872c813e92b43174298bee17ff/pymongo-4.17.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5f62862d0f87be481fa1fe8cb811994486773c94a2b61e509285e3f2890763", size = 2801745, upload-time = "2026-04-20T16:39:18.476Z" }, { url = "https://files.pythonhosted.org/packages/67/4c/3f83412d086f682d4d468761d66ddc49cf161e786ea74073045eb4491c60/pymongo-4.17.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64837adbbd72073301af51bb0fc80e3d7707fe5527cea1033ba0320f0b2f881b", size = 2684636, upload-time = "2026-04-20T16:39:20.878Z" }, { url = "https://files.pythonhosted.org/packages/9e/d8/b75f6f4ab6c8beb50b0270a4f1e2530b5774f5e116563440e1677ca1820f/pymongo-4.17.0-cp314-cp314t-win32.whl", hash = "sha256:b93b22eedc62598cf5ee9d8c8007a8e9121c50fd88137012d8985500e9dc3151", size = 1056356, upload-time = "2026-04-20T16:39:22.996Z" }, { url = "https://files.pythonhosted.org/packages/e4/5e/648c8a238eef18a25ed8a169ea6542d4a860bbec3e95b3d9badac2935c71/pymongo-4.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3689ea34f6b647c7d1e7bdc60fcfb214b2789ed1359a7fb96569c69f50e5f18f", size = 1090964, upload-time = "2026-04-20T16:39:24.989Z" }, { url = "https://files.pythonhosted.org/packages/dc/cb/d9780b66939c4fc1f024bcc7be23a2abcfe06a9745ca8fa76dc73395482e/pymongo-4.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9543d8f84c2e5608565c08ac679774811e6730770d8a645439b073422a4276fb", size = 1058526, upload-time = "2026-04-20T16:39:27.924Z" }, ] [[package]] name = "pymssql" version = "2.3.13" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7a/cc/843c044b7f71ee329436b7327c578383e2f2499313899f88ad267cdf1f33/pymssql-2.3.13.tar.gz", hash = "sha256:2137e904b1a65546be4ccb96730a391fcd5a85aab8a0632721feb5d7e39cfbce", size = 203153, upload-time = "2026-02-14T05:00:36.865Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0f/b5/77d2af3cab1a129891f75a4223a145664ec0443a340ee737518b499b4edb/pymssql-2.3.13-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:476f6f06b2ae5dfbfa0b169a6ecdd0d9ddfedb07f2d6dc97d2dd630ff2d6789a", size = 3173361, upload-time = "2026-02-14T04:59:16.582Z" }, { url = "https://files.pythonhosted.org/packages/93/e4/d7833727a19cd1aca2e6de4d59b9ecd3c7c15c1b9c9f0fef8c6a38176f76/pymssql-2.3.13-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:17942dc9474693ab2229a8a6013e5b9cb1312a5251207552141bb85fcce8c131", size = 2976106, upload-time = "2026-02-14T04:59:19.833Z" }, { url = "https://files.pythonhosted.org/packages/e9/16/121a3d77530ccfdec3cfdf30342d4ca67283441a0adc4f4daaf963f57454/pymssql-2.3.13-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d87237500def5f743a52e415cd369d632907212154fcc7b4e13f264b4e30021", size = 3043228, upload-time = "2026-02-14T04:59:21.603Z" }, { url = "https://files.pythonhosted.org/packages/85/34/178a4293329c9cf61d438262a52553a92b4421a40419a9ef58f7783ce444/pymssql-2.3.13-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:612ac062027d2118879f11a5986e9d9d82d07ca3545bb98c93200b68826ea687", size = 3175449, upload-time = "2026-02-14T04:59:24.047Z" }, { url = "https://files.pythonhosted.org/packages/00/aa/bcfc4b7488a19026bac62a819dd6ee389bc2e706f72d7c021d7c6fa8d08e/pymssql-2.3.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f1897c1b767cc143e77d285123ae5fd4fa7379a1bfec5c515d38826caf084eb6", size = 3689919, upload-time = "2026-02-14T04:59:26.345Z" }, { url = "https://files.pythonhosted.org/packages/8d/58/036bdbb7f036d97a3d502bf285f3530a9e800b07922ab6227ef69ce15da7/pymssql-2.3.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:48631c7b9fd14a1bd5675c521b6082590bf700b7961c65638d237817b3fde735", size = 3436776, upload-time = "2026-02-14T04:59:28.942Z" }, { url = "https://files.pythonhosted.org/packages/83/45/92ef7f032c9ce46c5adcf1fa1513e99a9b533ca673bbf01748574ca0f2cc/pymssql-2.3.13-cp310-cp310-win_amd64.whl", hash = "sha256:79c759db6e991eeae473b000c2e0a7fb8da799b2da469fe5a10d30916315e0b5", size = 2009253, upload-time = "2026-02-14T04:59:31.034Z" }, { url = "https://files.pythonhosted.org/packages/23/20/3d270f2bcfe7cf73b6d17719998316856550ca719791ac00be07e5be7a47/pymssql-2.3.13-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:152be40c0d7f5e4b1323f7728b0a01f3ee0082190cfbadf84b2c2e930d57e00e", size = 3171876, upload-time = "2026-02-14T04:59:33.247Z" }, { url = "https://files.pythonhosted.org/packages/9c/b0/b4f1c7879a4fdac688f227cc55f31a5cfc8c36dc63117a10af472a399fc1/pymssql-2.3.13-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:d94da3a55545c5b6926cb4d1c6469396f0ae32ad5d6932c513f7a0bf569b4799", size = 2973968, upload-time = "2026-02-14T04:59:35.462Z" }, { url = "https://files.pythonhosted.org/packages/3c/68/45157f1bb9b8499e4abe7b64195f44aaa2d6bf6aae305d4e8cf5df522424/pymssql-2.3.13-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51e42c5defc3667f0803c7ade85db0e6f24b9a1c5a18fcdfa2d09c36bff9b065", size = 3035519, upload-time = "2026-02-14T04:59:37.138Z" }, { url = "https://files.pythonhosted.org/packages/57/f3/3aeffc2b3683c135d75e0536657090ddb5f07114eb5e528303e1f4880393/pymssql-2.3.13-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aa18944a121f996178e26cadc598abdbf73759f03dc3cd74263fdab1b28cd96", size = 3162703, upload-time = "2026-02-14T04:59:39.571Z" }, { url = "https://files.pythonhosted.org/packages/e5/c0/bd35090a223961d9190dfb884be14529358d561cad1b4211dc351b20dcfd/pymssql-2.3.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:910404e0ec85c4cc7c633ec3df9b04a35f23bb74a844dd377a387026ae635e3a", size = 3681162, upload-time = "2026-02-14T04:59:42.181Z" }, { url = "https://files.pythonhosted.org/packages/c0/d4/d6a5d74c9942d1554538087cfd6ff489d3645bce484c53339f25c4cf6077/pymssql-2.3.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4b834c34e7600369eee7bc877948b53eb0fe6f3689f0888d005ae47dd53c0a66", size = 3424100, upload-time = "2026-02-14T04:59:43.795Z" }, { url = "https://files.pythonhosted.org/packages/27/38/7f1eff7dbbd286a16e341b813fd55c88258e07268a3ccebbfb0e9c46ca74/pymssql-2.3.13-cp311-cp311-win_amd64.whl", hash = "sha256:5c2e55b6513f9c5a2f58543233ed40baaa7f91c79e64a5f961ea3fc57a700b80", size = 2010201, upload-time = "2026-02-14T04:59:45.404Z" }, { url = "https://files.pythonhosted.org/packages/ba/60/a2e8a8a38f7be21d54402e2b3365cd56f1761ce9f2706c97f864e8aa8300/pymssql-2.3.13-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cf4f32b4a05b66f02cb7d55a0f3bcb0574a6f8cf0bee4bea6f7b104038364733", size = 3158689, upload-time = "2026-02-14T04:59:46.982Z" }, { url = "https://files.pythonhosted.org/packages/43/9e/0cf0ffb9e2f73238baf766d8e31d7237b5bee3cc1bb29a376b404610994a/pymssql-2.3.13-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:2b056eb175955f7fb715b60dc1c0c624969f4d24dbdcf804b41ab1e640a2b131", size = 2960018, upload-time = "2026-02-14T04:59:48.668Z" }, { url = "https://files.pythonhosted.org/packages/93/ea/bc27354feaca717faa4626911f6b19bb62985c87dda28957c63de4de5895/pymssql-2.3.13-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:319810b89aa64b99d9c5c01518752c813938df230496fa2c4c6dda0603f04c4c", size = 3065719, upload-time = "2026-02-14T04:59:50.369Z" }, { url = "https://files.pythonhosted.org/packages/1e/7a/8028681c96241fb5fc850b87c8959402c353e4b83c6e049a99ffa67ded54/pymssql-2.3.13-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0ea72641cb0f8bce7ad8565dbdbda4a7437aa58bce045f2a3a788d71af2e4be", size = 3190567, upload-time = "2026-02-14T04:59:52.202Z" }, { url = "https://files.pythonhosted.org/packages/aa/f1/ab5b76adbbd6db9ce746d448db34b044683522e7e7b95053f9dd0165297b/pymssql-2.3.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1493f63d213607f708a5722aa230776ada726ccdb94097fab090a1717a2534e0", size = 3710481, upload-time = "2026-02-14T04:59:54.01Z" }, { url = "https://files.pythonhosted.org/packages/59/aa/2fa0951475cd0a1829e0b8bfbe334d04ece4bce11546a556b005c4100689/pymssql-2.3.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb3275985c23479e952d6462ae6c8b2b6993ab6b99a92805a9c17942cf3d5b3d", size = 3453789, upload-time = "2026-02-14T04:59:56.841Z" }, { url = "https://files.pythonhosted.org/packages/78/08/8cd2af9003f9fc03912b658a64f5a4919dcd68f0dd3bbc822b49a3d14fd9/pymssql-2.3.13-cp312-cp312-win_amd64.whl", hash = "sha256:a930adda87bdd8351a5637cf73d6491936f34e525a5e513068a6eac742f69cdb", size = 1994709, upload-time = "2026-02-14T04:59:58.972Z" }, { url = "https://files.pythonhosted.org/packages/d4/4f/ee15b1f6b11e7c3accdc7da7840a019b63f12ba09eaa008acc601182f516/pymssql-2.3.13-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:30918bb044242865c01838909777ef5e0f1b9ecd7f5882346aefa57f4414b29c", size = 3156333, upload-time = "2026-02-14T05:00:01.21Z" }, { url = "https://files.pythonhosted.org/packages/79/03/aea5c77bad4a52649a1d9f786a1d9ce1c83d50f1a75df288e292737b6d80/pymssql-2.3.13-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:1c6d0b2d7961f159a07e4f0d8cc81f70ceab83f5e7fd1e832a2d069e1d67ee4e", size = 2957990, upload-time = "2026-02-14T05:00:03.11Z" }, { url = "https://files.pythonhosted.org/packages/5f/f8/30ac16fba32ff066b05f12c392d7b812fe11f06cb62d1d86ca5177c50a8b/pymssql-2.3.13-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16c5957a3c9e51a03276bfd76a22431e2bc4c565e2e95f2cbb3559312edda230", size = 3065264, upload-time = "2026-02-14T05:00:05.377Z" }, { url = "https://files.pythonhosted.org/packages/a9/98/7568447bf85921d21453fd56e19b6c9591d595fde0546c5a569f3ae937a8/pymssql-2.3.13-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0fddd24efe9d18bbf174fab7c6745b0927773718387f5517cf8082241f721a68", size = 3190039, upload-time = "2026-02-14T05:00:06.925Z" }, { url = "https://files.pythonhosted.org/packages/35/f1/4d9d275ebaac42cdd49d40d504ccb648f27710660c8b60cc427752438c09/pymssql-2.3.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:123c55ee41bc7a82c76db12e2eb189b50d0d7a11222b4f8789206d1cda3b33b9", size = 3710151, upload-time = "2026-02-14T05:00:08.424Z" }, { url = "https://files.pythonhosted.org/packages/6f/bd/a5cc6244fd27d3ea0cc82f12a7d38a24d7fd90b0022afd250014e8bfba15/pymssql-2.3.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e053b443e842f9e1698fcb2b23a4bff1ff3d410894d880064e754ad823d541e5", size = 3453156, upload-time = "2026-02-14T05:00:09.978Z" }, { url = "https://files.pythonhosted.org/packages/26/d0/c20ff0bbffd18db528bcc7b0c68b25c12ad563ed67c56ceca87c58f7399e/pymssql-2.3.13-cp313-cp313-win_amd64.whl", hash = "sha256:5c045c0f1977a679cc30d5acd9da3f8aeb2dc6e744895b26444b4a2f20dad9a0", size = 1995236, upload-time = "2026-02-14T05:00:11.495Z" }, { url = "https://files.pythonhosted.org/packages/ec/5f/6b64f78181d680f655ab40ba7b34cb68c045a2f4e04a10a70d768cd383b7/pymssql-2.3.13-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:fc5482969c813b0a45ce51c41844ae5bfa8044ad5ef8b4820ef6de7d4545b7f2", size = 3158377, upload-time = "2026-02-14T05:00:13.581Z" }, { url = "https://files.pythonhosted.org/packages/ff/24/155dbb0992c431496d440f47fb9d587cd0059ee20baf65e3d891794d862a/pymssql-2.3.13-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:ff5be7ab1d643dbce2ee3424d2ef9ae8e4146cf75bd20946bc7a6108e3ad1e47", size = 2959039, upload-time = "2026-02-14T05:00:15.883Z" }, { url = "https://files.pythonhosted.org/packages/c9/89/b453dd1b1188779621fb974ac715ab2e738f4a0b69f7291ab014298bd80d/pymssql-2.3.13-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d66ce0a249d2e3b57369048d71e1f00d08dfb90a758d134da0250ae7bc739c1", size = 3063862, upload-time = "2026-02-14T05:00:17.537Z" }, { url = "https://files.pythonhosted.org/packages/02/e5/96f57c78162013678ecc3f3f7e5fb52c83ee07beef26906d0870770c3ef6/pymssql-2.3.13-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d663c908414a6a032f04d17628138b1782af916afc0df9fefac4751fa394c3ac", size = 3188155, upload-time = "2026-02-14T05:00:19.011Z" }, { url = "https://files.pythonhosted.org/packages/cd/a2/4bee9484734ae0c55d10a2f6ff82dd4e416f52420755161b8760c817ad64/pymssql-2.3.13-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aa5e07eff7e6e8bd4ba22c30e4cb8dd073e138cd272090603609a15cc5dbc75b", size = 3709344, upload-time = "2026-02-14T05:00:21.139Z" }, { url = "https://files.pythonhosted.org/packages/37/cf/3520d96afa213c88db4f4a1988199db476d869a62afdd5d9c4635c184631/pymssql-2.3.13-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:db77da1a3fc9b5b5c5400639d79d7658ba7ad620957100c5b025be608b562193", size = 3451799, upload-time = "2026-02-14T05:00:22.504Z" }, { url = "https://files.pythonhosted.org/packages/25/50/4be9bd9cf4b43208a7175117a533ece200cfe4131a39f9909bdc7560ddeb/pymssql-2.3.13-cp314-cp314-win_amd64.whl", hash = "sha256:7d7037d2b5b907acc7906d0479924db2935a70c720450c41339146a4ada2b93d", size = 2049139, upload-time = "2026-02-14T05:00:23.951Z" }, ] [[package]] name = "pymysql" version = "1.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c9/bc/1c6a92f385940f727daeecf3bacaf186e03875dff57197801046c583bcf0/pymysql-1.2.0.tar.gz", hash = "sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33", size = 49021, upload-time = "2026-05-19T08:26:22.302Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c4/bd/2534e130295c8cfd4f0a2e31623baab7502278f1e97bcfe61db75656a77f/pymysql-1.2.0-py3-none-any.whl", hash = "sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0", size = 45716, upload-time = "2026-05-19T08:26:20.974Z" }, ] [[package]] name = "pyopenssl" version = "26.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3f/e8/7325d258199b159eb2c03fe32107533e2832e70e63f4fb88a6aa00023201/pyopenssl-26.4.0.tar.gz", hash = "sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7", size = 182046, upload-time = "2026-08-01T19:50:50.512Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/51/ad/2cf6d3fa2fae5c79e1ed9960c0d42badd0f94d81dd12b50604cdc839e648/pyopenssl-26.4.0-py3-none-any.whl", hash = "sha256:f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c", size = 56026, upload-time = "2026-08-01T19:50:48.94Z" }, ] [[package]] name = "pypika-tortoise" version = "0.6.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c2/06/89fe5fff93c5a01dbdeb9f3d843a7e997dc6e3a87222a260a164ff91fb81/pypika_tortoise-0.6.5.tar.gz", hash = "sha256:64d96c9b88450f6360ad22a7063933b6a90961a7317f04b2b63c98fd5d705506", size = 81468, upload-time = "2026-03-13T20:44:54.439Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6c/b8/502910eb8b315f719d8f6a6509f13a38b6c4c05378f14ac151ff347bff0a/pypika_tortoise-0.6.5-py3-none-any.whl", hash = "sha256:9194ac6ce6ac9bdfc6e959c831c5788ef05ee1371e82ba281b0eb75f4a2bd4f1", size = 47936, upload-time = "2026-03-13T20:44:53.541Z" }, ] [[package]] name = "pyproject-api" version = "1.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/62/62/0fe346fe380b1aafaf819c8cb195d3241bb4f355f908e6339814131a830b/pyproject_api-1.10.1.tar.gz", hash = "sha256:c2b2726bd7aa9217b6c50b621fef5b2ae5def4d55b779c9e0694c15e0a8517ba", size = 23477, upload-time = "2026-05-28T14:22:14.049Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/d7/29e1e5e882f79133631f7bcace42d23db493f616463c157a1ab614bf69dd/pyproject_api-1.10.1-py3-none-any.whl", hash = "sha256:fa9e6f66c35b5017e909825d8f2b5d5482ea699d7be809d21c03bd1f7317f36a", size = 12992, upload-time = "2026-05-28T14:22:12.711Z" }, ] [[package]] name = "pyramid" version = "2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "hupper" }, { name = "plaster" }, { name = "plaster-pastedeploy" }, { name = "setuptools" }, { name = "translationstring" }, { name = "venusian" }, { name = "webob" }, { name = "zope-deprecation" }, { name = "zope-interface" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7e/f3/05b49d8bbc6f4592f8820d9e306e20b0b4c266171f3455c737c49b4a7338/pyramid-2.1.tar.gz", hash = "sha256:5a24bc1b5f9e0f0d1c0b6ddd1111e84c15dac44a07e3c1d24e74dd9bb4fc22eb", size = 2628991, upload-time = "2026-03-11T01:23:59.642Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1f/d4/03a8f9b60b8f2a9198c962d08fd9b657c1c0797cbbde3793babbad200845/pyramid-2.1-py3-none-any.whl", hash = "sha256:0aa62604221f6a8ac6e77a0faa02a5b7a294d6675f15581d8d416dffe2ac98e7", size = 243407, upload-time = "2026-03-11T01:23:56.896Z" }, ] [[package]] name = "pytest" version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "python-discovery" version = "1.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/1a/cbbaf13b730abb0a16b964d984e19f2fe520c21a4dc664051359a3f5a9e7/python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690", size = 70277, upload-time = "2026-06-11T16:10:42.383Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", size = 33886, upload-time = "2026-06-11T16:10:41.192Z" }, ] [[package]] name = "python-dotenv" version = "1.2.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] name = "python-multipart" version = "0.0.32" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, ] [[package]] name = "python-snappy" version = "0.7.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cramjam" }, ] sdist = { url = "https://files.pythonhosted.org/packages/39/66/9185fbb6605ba92716d9f77fbb13c97eb671cd13c3ad56bd154016fbf08b/python_snappy-0.7.3.tar.gz", hash = "sha256:40216c1badfb2d38ac781ecb162a1d0ec40f8ee9747e610bcfefdfa79486cee3", size = 9337, upload-time = "2024-08-29T13:16:05.705Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/86/c1/0ee413ddd639aebf22c85d6db39f136ccc10e6a4b4dd275a92b5c839de8d/python_snappy-0.7.3-py3-none-any.whl", hash = "sha256:074c0636cfcd97e7251330f428064050ac81a52c62ed884fc2ddebbb60ed7f50", size = 9155, upload-time = "2024-08-29T13:16:04.773Z" }, ] [[package]] name = "pytz" version = "2026.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, ] [[package]] name = "pywin32" version = "312" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/fe/1b/9cfdeac80ee45bebbbcb31f1b7b99a0d81a1c72de48d837be984e0e88b1d/pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e", size = 6361387, upload-time = "2026-06-04T07:49:14.329Z" }, { url = "https://files.pythonhosted.org/packages/33/b1/7afc96d041d982c27bc2df6f853d43f01fd273e3d39d04be3647ddeb533d/pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db", size = 6926780, upload-time = "2026-06-04T07:49:16.881Z" }, { url = "https://files.pythonhosted.org/packages/ce/3a/4140da9ad54108e517f4a16b2d83da3033e08662144623e1239587cb7db6/pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd", size = 4307203, upload-time = "2026-06-04T07:49:18.993Z" }, { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" }, { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" }, { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" }, { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, ] [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] name = "redis" version = "8.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cc/c3/928b290c2c0ca99ab96eea5b4ff8f30be8112b075301a7d3ba214a3c8c12/redis-8.0.1.tar.gz", hash = "sha256:afc5a7a2f5a084f5b1880dec548dd45be17db7e43c82a30d84f952aefb05cfb0", size = 5114170, upload-time = "2026-06-23T14:52:37.728Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fd/0a/c2345ebf1ebe70840ce3f6c6ee612f8fa749cfbd1b03069c53bf0c62aaad/redis-8.0.1-py3-none-any.whl", hash = "sha256:47daa35a058c23468d6437f17a8c76882cb316b838ef763036af99b96cedd743", size = 502406, upload-time = "2026-06-23T14:52:36.137Z" }, ] [[package]] name = "referencing" version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] [[package]] name = "remoulade" version = "3.5.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.11.*'", "python_full_version < '3.11'", ] dependencies = [ { name = "attrs" }, { name = "prometheus-client" }, { name = "python-dateutil" }, { name = "pytz" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/de/685788c7701777699d6851adbf8d22268de61ee3cda9374596d09c8d985c/remoulade-3.5.2.tar.gz", hash = "sha256:0db01d16a358b02da9ddc2e5c4b852a4e1616622a54127388e46ae4010afaaf9", size = 116754, upload-time = "2025-10-10T08:15:17.516Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/59/7c/7438a4fe2d2fd7af3f3136d2b04b189c30f01494841fc98dfedf64d1dd98/remoulade-3.5.2-py3-none-any.whl", hash = "sha256:a791fe32d01df421d2c3bd95829bf0298a316bd9ac3a5cc0a845ac0ffd460993", size = 141611, upload-time = "2025-10-10T08:15:15.537Z" }, ] [[package]] name = "remoulade" version = "6.2.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", "python_full_version == '3.12.*'", ] dependencies = [ { name = "attrs" }, { name = "prometheus-client" }, { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/04/27/a72b57736b93ebe1b929cb7c0ece9f3e4e74715b773754cb418aaf26b72e/remoulade-6.2.0.tar.gz", hash = "sha256:f45985fbbc30deab7c197c5deeecaac08dfeee21381d38ef58e3b8da80a02e79", size = 99784, upload-time = "2026-05-18T12:55:33.967Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a2/d7/c17d18e0663a54cffcc9b802798a1907b38620f5d3ef3a8f86cd54a9e471/remoulade-6.2.0-py3-none-any.whl", hash = "sha256:944d1c2b3583a7d6df29f3100a67ebab4dfca31c92161c36eff8f91e45006fbd", size = 152725, upload-time = "2026-05-18T12:55:31.299Z" }, ] [[package]] name = "requests" version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] name = "rich" version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] name = "rpds-py" version = "0.30.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11'", ] sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, ] [[package]] name = "rpds-py" version = "2026.6.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, ] [[package]] name = "s3transfer" version = "0.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] sdist = { url = "https://files.pythonhosted.org/packages/11/b3/bcdc2f58fa92592db511beda154c2c08d28f21f6c4637f06a42a24b10c21/s3transfer-0.17.1.tar.gz", hash = "sha256:042dd5e3b1b512355e35a23f0223e426b7042e80b97830ea2680ddce327fc45e", size = 159439, upload-time = "2026-05-26T19:45:01.714Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/85/dd/904873250a6554fbae40cddbf9198e3cc37a2f1319d5e1a5ce82fe269c17/s3transfer-0.17.1-py3-none-any.whl", hash = "sha256:5b9827d1044159bbb01b86ef8902760ea39281927f5de31de75e1d657177bf4c", size = 88264, upload-time = "2026-05-26T19:45:00.452Z" }, ] [[package]] name = "scylla-driver" version = "3.29.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "geomet" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c1/ca/5080651593e74f72769e20754350c57519a33a75e7699d26ff657c8fc237/scylla_driver-3.29.11.tar.gz", hash = "sha256:a8f644d73713be556e99d6355cd2de41503ee50bff990865d71887e1f9f18b46", size = 316559, upload-time = "2026-06-15T11:52:41.896Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/3c/9cdec4c9ae643769401e11da9e6535e327257b53e29b8cfec417b77ad99b/scylla_driver-3.29.11-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e7d899f1b5bc6d9441db01b05bd3c317b99be23525e6baae789ee59d759187fc", size = 3220705, upload-time = "2026-06-15T11:51:46.302Z" }, { url = "https://files.pythonhosted.org/packages/d2/10/d273b4f94714b5422f26fa008bba22d7a70ec9c605fe0cf8b5518935b23f/scylla_driver-3.29.11-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:cdd43ebb03db2441b920033d032a824612aa7f4daa58e88563e0a2036d6bd860", size = 3603254, upload-time = "2026-06-15T11:51:48.179Z" }, { url = "https://files.pythonhosted.org/packages/10/53/a4479aec9f8dd883feb7b99ed89ee6a729834e254754d82bf2e3b578e951/scylla_driver-3.29.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57e8fffaf0803c0a406cc0e331a738c5d305fb28c9f5834d29c077fa134a8820", size = 4118325, upload-time = "2026-06-15T11:51:50.223Z" }, { url = "https://files.pythonhosted.org/packages/4f/a8/665162acf8dd9b1fa12e9dd3a9ebee8f1a322c0fe25345ab78f86b10b975/scylla_driver-3.29.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74668f6d4bca9f05f0826b8b464f363e7c1c7ac1cbbf373bae638bdc68780b8e", size = 4394374, upload-time = "2026-06-15T11:51:52.058Z" }, { url = "https://files.pythonhosted.org/packages/3a/bc/f0797da3690a5c8f8011d35710552da4ef6c78d8b695b546af3221bfccdc/scylla_driver-3.29.11-cp310-cp310-win32.whl", hash = "sha256:ce1cc363d2b9c9e5a0def22b85c13c1bde6f26d1abca427b408bd18f0fd3828c", size = 2462716, upload-time = "2026-06-15T11:51:53.876Z" }, { url = "https://files.pythonhosted.org/packages/83/61/3a43c957833f29b93f7c057056aa4d2d8fa1f5901e798d0fa84ceb87dd96/scylla_driver-3.29.11-cp310-cp310-win_amd64.whl", hash = "sha256:f3e2d06685fb1e6dad8905b0efa9423155852cf1b2a0d2b1aad3687cd488e4a4", size = 2988754, upload-time = "2026-06-15T11:51:55.49Z" }, { url = "https://files.pythonhosted.org/packages/28/9f/0a43d3d3cba395d6f26d90b963bb8c05286b2dd94bdbc073d6887e595404/scylla_driver-3.29.11-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:241bffc347888dbeae48d842e58a7d20915ee518beb46467d9b7cd5a0ad501ed", size = 3183084, upload-time = "2026-06-15T11:51:57.104Z" }, { url = "https://files.pythonhosted.org/packages/53/a0/04940b9fcf135b24f22539a3cf0684f37c7bb5b0b2cc6f5d5aebfe04595c/scylla_driver-3.29.11-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:39f48ebe02d17e7cc6f3ba34f9748c971932eaa11d6557ab7d5fae1631987a55", size = 3580336, upload-time = "2026-06-15T11:51:58.732Z" }, { url = "https://files.pythonhosted.org/packages/b8/03/c565ddcc8f8c75531b9025992a07b75fa9be37d9e44b61c6eb5770bc25d9/scylla_driver-3.29.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acb38ed6acae2185bdef6834569bb95acd858ec91a8269c555e912127946e998", size = 4000995, upload-time = "2026-06-15T11:52:00.72Z" }, { url = "https://files.pythonhosted.org/packages/37/0f/74db8872a61e5325ece30c1f86561122836c7937fed537780ad2481031b9/scylla_driver-3.29.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17b19807a8690a4df2e52bb23327a746715e7076f5f28b45a6ce4556b2a00dd7", size = 4254458, upload-time = "2026-06-15T11:52:02.291Z" }, { url = "https://files.pythonhosted.org/packages/cc/93/7d629e113e1c612c956e70cafcbfc68d41dd2f81b07699b8f3b51699d95d/scylla_driver-3.29.11-cp311-cp311-win32.whl", hash = "sha256:70dda8384ae23472cfdf6c798d20e59639aa1e4796937417088151adb4acf4a9", size = 2453984, upload-time = "2026-06-15T11:52:03.862Z" }, { url = "https://files.pythonhosted.org/packages/d5/21/cadd8063c9b8c1e377a95d9efef18b1e7e9e78e86c0745273940aa11d536/scylla_driver-3.29.11-cp311-cp311-win_amd64.whl", hash = "sha256:2546a6646eead8225c8a0440c744c50ca85147fe61a712fb68e08be11ab21560", size = 2998783, upload-time = "2026-06-15T11:52:05.443Z" }, { url = "https://files.pythonhosted.org/packages/59/2e/006b35c81fdb8cc916b0965347631d7580888716e1b6b41a02121d213c35/scylla_driver-3.29.11-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6ee63f2503590f683e4b6c39c0070f567cc825fabd20f74cfe053d926a930ffc", size = 3229770, upload-time = "2026-06-15T11:52:06.937Z" }, { url = "https://files.pythonhosted.org/packages/b6/ee/994d10943da376a501d4dc369aa2ba5b37b5c3a696376b89d43f9b3a6b3c/scylla_driver-3.29.11-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:ee188f93fc931d9ce6231b550260988312445ecd2ea336445ec98b225ace47fb", size = 3641001, upload-time = "2026-06-15T11:52:08.608Z" }, { url = "https://files.pythonhosted.org/packages/8c/c1/50c2cbfe2cd10d968384a4443b1503514f3604486903f00df63dd04919d4/scylla_driver-3.29.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ed1b2ec3634d3b25609a3a587de420a41b435f497e963434a8b5b42fa32c88e", size = 3767130, upload-time = "2026-06-15T11:52:10.205Z" }, { url = "https://files.pythonhosted.org/packages/c2/0e/5d8e30fbb318484e998772d35664cc9dc3c8e870736a4c5c1e01f9c34100/scylla_driver-3.29.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd0d872882a4c098eacc83b832a042ea0b5165e247790d378e215c2a9f205d44", size = 4060468, upload-time = "2026-06-15T11:52:11.943Z" }, { url = "https://files.pythonhosted.org/packages/25/e3/30e6e853e1e828a299dcaeeae885e07a01b9b3cb33d41a174f5ba2aeae71/scylla_driver-3.29.11-cp312-cp312-win32.whl", hash = "sha256:45050fd2028f07e236b7b3a555d4d17a1d88ea8ba8b5609ae1d14730a65b1e4f", size = 2339489, upload-time = "2026-06-15T11:52:13.309Z" }, { url = "https://files.pythonhosted.org/packages/33/12/6444f51145a7605b1c47e53c81960ce7a76c708f2d395dd3bf5b172fc092/scylla_driver-3.29.11-cp312-cp312-win_amd64.whl", hash = "sha256:1ea1fd011d8cf74b706c1b6ae982437597abf545e6d53f3a79905c97831db16a", size = 2822126, upload-time = "2026-06-15T11:52:15.07Z" }, { url = "https://files.pythonhosted.org/packages/7b/8a/bb78e727e13d5ee58dfc694956f2b29952ade25f7afc2a1eadacdae68f66/scylla_driver-3.29.11-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:0a93dd4af8995ac08335e581aca393a2bca64c3d4748ec359b9400797f2a7449", size = 3316808, upload-time = "2026-06-15T11:52:16.47Z" }, { url = "https://files.pythonhosted.org/packages/e6/f4/d1b68f8053b8a95d920f001a64504726e5e5ee7485050881387b3d26fa80/scylla_driver-3.29.11-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:abb5f98b4efbcb6257d1b97eea9c2bb95ea509e8983c2b8e021810ef460cb390", size = 3724512, upload-time = "2026-06-15T11:52:17.938Z" }, { url = "https://files.pythonhosted.org/packages/2c/d9/b76a407c8d61e01ab91ce894a77e88d96f8b6aa01aec7fdc5734c191a099/scylla_driver-3.29.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5a4851afb2a6d8410f55e7344b0589f71289338ff35c6901135c1a683381149", size = 3723429, upload-time = "2026-06-15T11:52:19.538Z" }, { url = "https://files.pythonhosted.org/packages/ae/53/4bb1ad1c948a4df65f317a24f18af8544e287f7fa67c28be364e02659311/scylla_driver-3.29.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fb9bc8ae114434c3418ca8ba0bca00fb7afd4943a52694b5bde3f8681a0eea9", size = 4011545, upload-time = "2026-06-15T11:52:21.341Z" }, { url = "https://files.pythonhosted.org/packages/26/30/c565e65b9f874a27d397902ae65a86947ac40cc2e6083feba65bd52d28f3/scylla_driver-3.29.11-cp313-cp313-win32.whl", hash = "sha256:dda6b9f7fe48c6a0623e629a6bc0497dff132ca8a2a8897cd1f4281b5b12ecfe", size = 2331085, upload-time = "2026-06-15T11:52:22.784Z" }, { url = "https://files.pythonhosted.org/packages/94/4b/92d66d8c342f53aebf5f09f64045d7f4c1264de827915bd47663a60f90f0/scylla_driver-3.29.11-cp313-cp313-win_amd64.whl", hash = "sha256:677b5b0312275a958c80dbd3d1686e4a60c805ddae290a4b56551245c0a07a95", size = 2826380, upload-time = "2026-06-15T11:52:24.212Z" }, { url = "https://files.pythonhosted.org/packages/b0/01/9c85bfd5aad430450e99a63ab5a9ca3d0c4b2e01582c90f8dfe01c997b8f/scylla_driver-3.29.11-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f222606faeb11f7d41197f16b97cfc3c9578608bd5c2813d51d30c4dc42e675b", size = 3348247, upload-time = "2026-06-15T11:52:25.643Z" }, { url = "https://files.pythonhosted.org/packages/57/ba/af50578a9055cf1645e7c164eeb61184e8359cf04f9b301a886052a78458/scylla_driver-3.29.11-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:844e61a13edf0d12f95a64f6dd8a9c0fd0b0919570e10631e33c640acff59db7", size = 3729362, upload-time = "2026-06-15T11:52:27.287Z" }, { url = "https://files.pythonhosted.org/packages/3d/ad/f7d6de9f357a78ba641be9280f52fa881d7e1ef7a5b5c12da598b3030740/scylla_driver-3.29.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e74576dba8992b4dac6ea3fe4c8cfd92f5318aa08b20142aff6423f20e56da7d", size = 3785189, upload-time = "2026-06-15T11:52:28.91Z" }, { url = "https://files.pythonhosted.org/packages/d5/fa/23a0d73a0e9ea471c92d750692670843b929e83ca0b02c52eb947ab194c9/scylla_driver-3.29.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf33948e181c469f63108c01f319078051819c147f6446ea56b71975656efc1e", size = 4035669, upload-time = "2026-06-15T11:52:31.082Z" }, { url = "https://files.pythonhosted.org/packages/cc/87/4f4af09ff3c3ab7ad844f85c5d436d07144ca86aee78c48c7e58ca6e6c53/scylla_driver-3.29.11-cp314-cp314-win32.whl", hash = "sha256:cf98016b6ce61dc30d80ac5bc491e0a84aa9da09e326fead5bba888d55f56126", size = 2370153, upload-time = "2026-06-15T11:52:32.725Z" }, { url = "https://files.pythonhosted.org/packages/54/bc/810ec23f1f116391c23e0d4f1c60a9aa48dfef1e9e6678e306a788d90d5c/scylla_driver-3.29.11-cp314-cp314-win_amd64.whl", hash = "sha256:ae36b731481c9ded343811132bcce4f68c9f2de8af8a51248fcea4d9c3b36d52", size = 2882915, upload-time = "2026-06-15T11:52:34.442Z" }, { url = "https://files.pythonhosted.org/packages/87/1c/2f2d138e6876ee1b4fb394ca08222c9a551e37b9154a42ba6625f86faa19/scylla_driver-3.29.11-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:3094c90a49c172b1c09bbf44b46ea0c2c284e35cfd0222bc0062492de05131c0", size = 365538, upload-time = "2026-06-15T11:52:36.064Z" }, { url = "https://files.pythonhosted.org/packages/d2/59/ef44ea990f5b68413828632662a36ca32c5a9004b138e1a12d409fdab106/scylla_driver-3.29.11-pp311-pypy311_pp73-macosx_15_0_x86_64.whl", hash = "sha256:617a8c459b30403002d38d7f99e3ed160da7671c7e62b0647e5fbd89539c6aff", size = 365046, upload-time = "2026-06-15T11:52:37.542Z" }, { url = "https://files.pythonhosted.org/packages/85/f2/f259f62ce06f42992dec6fb3e13825685a480f2739ede5f4873bd0868c07/scylla_driver-3.29.11-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8072842efb2ff7c9761563485156fb0b6a11c68b028e32b6a807a70d226dbbbd", size = 366285, upload-time = "2026-06-15T11:52:39.041Z" }, { url = "https://files.pythonhosted.org/packages/ca/53/11af78d26a31ffdc9f2bb6119686a6e688e5104152c2a7c94ad911869aa5/scylla_driver-3.29.11-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2a4116f2da833f76db6e59b4c0534b72291cac478ca4b5944abbdd52b1635d94", size = 369417, upload-time = "2026-06-15T11:52:40.394Z" }, ] [[package]] name = "setuptools" version = "81.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] [[package]] name = "sqlalchemy" version = "2.0.51" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/71/76/b3ea1d8842e7b62c718a88d302809003d65ed82011460ca48907dde658c4/sqlalchemy-2.0.51-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0", size = 2162087, upload-time = "2026-06-15T16:05:15.795Z" }, { url = "https://files.pythonhosted.org/packages/6c/22/f19552eb7876774d50cfd025337ef5d67acc10cd8f29adab7716cf47c352/sqlalchemy-2.0.51-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652", size = 3244579, upload-time = "2026-06-15T16:10:36.165Z" }, { url = "https://files.pythonhosted.org/packages/fc/97/e4a2eb5a8ec5cd3c2a0615a2f15f0afca89ac039229599b9ed0c0ed28e5e/sqlalchemy-2.0.51-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d", size = 3243515, upload-time = "2026-06-15T16:12:22.627Z" }, { url = "https://files.pythonhosted.org/packages/74/c6/5900ec624fab3360aa2ec59b99bb2046dd79799e310bb78a0514eaa4038e/sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84", size = 3195492, upload-time = "2026-06-15T16:10:38.097Z" }, { url = "https://files.pythonhosted.org/packages/8f/41/2ee3c4e1ac4fd22309349823fe13f33febeab1a71db1d7e9d60293a07dcb/sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080", size = 3215782, upload-time = "2026-06-15T16:12:24.051Z" }, { url = "https://files.pythonhosted.org/packages/ce/1c/3bd72c341f1cb5faed5a7457ea840228a46be51cfbaf31a9db72fc963f11/sqlalchemy-2.0.51-cp310-cp310-win32.whl", hash = "sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1", size = 2122119, upload-time = "2026-06-15T16:13:26.915Z" }, { url = "https://files.pythonhosted.org/packages/2a/63/b6dfdd646abf91c3bedb13727226a5e765e5f8365e898d43818e6672fa46/sqlalchemy-2.0.51-cp310-cp310-win_amd64.whl", hash = "sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a", size = 2145158, upload-time = "2026-06-15T16:13:28.386Z" }, { url = "https://files.pythonhosted.org/packages/3a/69/a67c69e5f28fc9c99d6f7bd60bd50e91f2fed2423e3b30fb228fa00e51f3/sqlalchemy-2.0.51-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba", size = 2161838, upload-time = "2026-06-15T16:05:17.144Z" }, { url = "https://files.pythonhosted.org/packages/9a/a4/c8c22b8438bddc0a030157c6ec0f6ef97b3c38effa444bdab2a27af04090/sqlalchemy-2.0.51-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604", size = 3319402, upload-time = "2026-06-15T16:10:40.002Z" }, { url = "https://files.pythonhosted.org/packages/90/54/44012d32fd77d991256d2ff793ba3807c51d40cb27a85b4796224f6744df/sqlalchemy-2.0.51-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd", size = 3319675, upload-time = "2026-06-15T16:12:25.658Z" }, { url = "https://files.pythonhosted.org/packages/29/a5/de0592acaf5906cd7430874392d6f7e8b4a7c8437610953ee2d1501c0b44/sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260", size = 3270777, upload-time = "2026-06-15T16:10:42.125Z" }, { url = "https://files.pythonhosted.org/packages/cb/14/a44c90739c780b362238e4ac3cb19dd0ca40d13e6ddc5daa112166ddab4f/sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265", size = 3293940, upload-time = "2026-06-15T16:12:27.156Z" }, { url = "https://files.pythonhosted.org/packages/65/eb/fbd0f206a330e66f8c602a99c37c4e731f107faed62954b41b01f16dd9d9/sqlalchemy-2.0.51-cp311-cp311-win32.whl", hash = "sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86", size = 2121183, upload-time = "2026-06-15T16:13:29.905Z" }, { url = "https://files.pythonhosted.org/packages/ad/fd/005bf80f3cf6e5c62b5dd68616280f51cd012c60840fa74781b3ed7b1623/sqlalchemy-2.0.51-cp311-cp311-win_amd64.whl", hash = "sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc", size = 2145796, upload-time = "2026-06-15T16:13:31.283Z" }, { url = "https://files.pythonhosted.org/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" }, { url = "https://files.pythonhosted.org/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" }, { url = "https://files.pythonhosted.org/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" }, { url = "https://files.pythonhosted.org/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" }, { url = "https://files.pythonhosted.org/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" }, { url = "https://files.pythonhosted.org/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" }, { url = "https://files.pythonhosted.org/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" }, { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" }, { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" }, { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" }, { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" }, { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" }, { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" }, { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" }, { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" }, { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" }, { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" }, { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" }, { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" }, { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" }, { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" }, { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" }, { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" }, { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" }, { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" }, { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" }, { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" }, { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" }, { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, ] [[package]] name = "sqlparse" version = "0.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5f/d3/3f06a1006f2261d1342aefb3c71eed02f5d4ca5bdbecd86ebc12ad38306e/sqlparse-0.6.0.tar.gz", hash = "sha256:113c35c75365ab9cc9c7231d68c6428fb11c085fc8e9eb1ad659b7ddbf6cd2b9", size = 178477, upload-time = "2026-08-13T19:16:06.396Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d9/50/f00935da0ec7cbf325f8dc4f772ae46fbc7b672dd62876e73f0a94adda57/sqlparse-0.6.0-py3-none-any.whl", hash = "sha256:b861c0288ce2fa56209a9a6412d2e066ac664b3873b89c26c9d8415e8e32996f", size = 50070, upload-time = "2026-08-13T19:16:04.062Z" }, ] [[package]] name = "sse-starlette" version = "3.4.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "starlette" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d2/1b/bc9e3e7a72dcdad7dc7888758f5d00f56f8909ed5cfdff822bd72bb4c520/sse_starlette-3.4.5.tar.gz", hash = "sha256:83072538bc211a2f68b7b0422226c4af3e9b62e106e07034664b832ca019842a", size = 35249, upload-time = "2026-06-20T17:36:58.322Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/75/c88d3f5dafd59c791da1ce27650d30bf5b70cbf1cbf01cd00e5f9e360915/sse_starlette-3.4.5-py3-none-any.whl", hash = "sha256:e71bad53323f65573c3864a6c3bd0c1eb6e5f092b2e48082b0c35927d19ca296", size = 16518, upload-time = "2026-06-20T17:36:56.729Z" }, ] [[package]] name = "starlette" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] [[package]] name = "structlog" version = "21.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ea/77/e38019e698b0c0134f903ab40e87f0975813ca7f74dad287272788134f03/structlog-21.5.0.tar.gz", hash = "sha256:68c4c29c003714fe86834f347cb107452847ba52414390a7ee583472bde00fc9", size = 473089, upload-time = "2021-12-16T12:48:11.976Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4b/a2/7c5d78fa0b6fc4f249d0a732811b2cef979d16677e21f10637bbedcd0e6a/structlog-21.5.0-py3-none-any.whl", hash = "sha256:fd7922e195262b337da85c2a91c84be94ccab1f8fd1957bd6986f6904e3761c8", size = 53394, upload-time = "2021-12-16T12:48:09.837Z" }, ] [[package]] name = "tenacity" version = "9.1.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, ] [[package]] name = "tomli" version = "2.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] [[package]] name = "tomli-w" version = "1.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, ] [[package]] name = "tomlkit" version = "0.15.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/51/db/03eaf4331631ef6b27d6e3c9b68c54dc6f0d63d87201fed600cc409307fd/tomlkit-0.15.0.tar.gz", hash = "sha256:7d1a9ecba3086638211b13814ea79c90dd54dd11993564376f3aa92271f5c7a3", size = 161875, upload-time = "2026-05-10T07:38:22.245Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/43/8bd850ee71a191bf072e31302c73a66be413fecdd98fdcd111ecbcce13ca/tomlkit-0.15.0-py3-none-any.whl", hash = "sha256:4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738", size = 41328, upload-time = "2026-05-10T07:38:23.517Z" }, ] [[package]] name = "tornado" version = "6.5.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/64/24/95ec527ad67b76d59299e5465b3935d05e4294b7e0290a3924b7487df30b/tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2", size = 519252, upload-time = "2026-06-08T17:34:51.232Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163", size = 448543, upload-time = "2026-06-08T17:34:38.052Z" }, { url = "https://files.pythonhosted.org/packages/92/4f/090b1431e5a43df696feceffc268c5383cc079ecb5f08ce58f917109aafe/tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100", size = 446707, upload-time = "2026-06-08T17:34:39.594Z" }, { url = "https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972", size = 449774, upload-time = "2026-06-08T17:34:41.204Z" }, { url = "https://files.pythonhosted.org/packages/35/37/d434c73f4c6e014b745b9b37085f34f40c022f007efff3d7fe65991899f3/tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b", size = 450745, upload-time = "2026-06-08T17:34:42.531Z" }, { url = "https://files.pythonhosted.org/packages/b6/2b/56b9aff361d7f1ab728a805ec7d7ea835f8807afa9f5cc690ea0e630efb9/tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92", size = 450578, upload-time = "2026-06-08T17:34:43.787Z" }, { url = "https://files.pythonhosted.org/packages/02/30/a7444fb23aa76860a14198fab96ac79f1866b0a6e19e26c4381b0938e50f/tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5", size = 449985, upload-time = "2026-06-08T17:34:45.326Z" }, { url = "https://files.pythonhosted.org/packages/5c/42/5f0e56c01e8d9d36f4e23f367b85ae6cae0c1ecddd5e6977d8388ad27488/tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4", size = 451047, upload-time = "2026-06-08T17:34:46.784Z" }, { url = "https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4", size = 451485, upload-time = "2026-06-08T17:34:48.248Z" }, { url = "https://files.pythonhosted.org/packages/71/2e/7b1c769803121b809112cf9a00681c472eae1d80e32d7ec0e0bd61d0d0e1/tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796", size = 450506, upload-time = "2026-06-08T17:34:49.702Z" }, ] [[package]] name = "tortoise-orm" version = "1.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiosqlite" }, { name = "anyio" }, { name = "iso8601", marker = "python_full_version < '4'" }, { name = "pypika-tortoise" }, { name = "tomlkit", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1d/55/e75d3ae0dd2c96cf961bf068f465fb62ec481d802beb65f406620bfd40a0/tortoise_orm-1.1.7.tar.gz", hash = "sha256:a0f0f27f9c92547739f0a3f0862c257ab64d8baadbc0e548cbcfa2d70bcbe275", size = 387489, upload-time = "2026-03-21T20:03:29.087Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bf/9b/e7e5b4f4e9433b9d9c389afc62ea0ed2597626842dc0f76dd7b54eb32cdd/tortoise_orm-1.1.7-py3-none-any.whl", hash = "sha256:b640b786905891cc120aa16d1a7af18cd54b111a812f9b1c1d1c7b294d62949d", size = 272437, upload-time = "2026-03-21T20:03:27.539Z" }, ] [[package]] name = "towncrier" version = "25.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "jinja2" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/eb/5bf25a34123698d3bbab39c5bc5375f8f8bcbcc5a136964ade66935b8b9d/towncrier-25.8.0.tar.gz", hash = "sha256:eef16d29f831ad57abb3ae32a0565739866219f1ebfbdd297d32894eb9940eb1", size = 76322, upload-time = "2025-08-30T11:41:55.393Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/42/06/8ba22ec32c74ac1be3baa26116e3c28bc0e76a5387476921d20b6fdade11/towncrier-25.8.0-py3-none-any.whl", hash = "sha256:b953d133d98f9aeae9084b56a3563fd2519dfc6ec33f61c9cd2c61ff243fb513", size = 65101, upload-time = "2025-08-30T11:41:53.644Z" }, ] [[package]] name = "tox" version = "4.56.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "colorama" }, { name = "filelock" }, { name = "packaging" }, { name = "platformdirs" }, { name = "pluggy" }, { name = "pyproject-api" }, { name = "python-discovery" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tomli-w" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/60/6a/d20d405fc6661902ff803a9fa048d8aae27597c3b5dc750369ded82d08f7/tox-4.56.1.tar.gz", hash = "sha256:db1c2610802553189cf40de251661d066a635ee0ed9bf2a60093b5f1a7f36ef8", size = 283155, upload-time = "2026-06-25T06:18:36.802Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d8/97/560a5dfde154619d9643b1e208119dddc29bbb35a38a4ce4d095c16cf8f0/tox-4.56.1-py3-none-any.whl", hash = "sha256:4d06b925c4dd67872099b39c5a46fba79a2169c5f6e32060f95a8b1181f0ef55", size = 216469, upload-time = "2026-06-25T06:18:35.229Z" }, ] [[package]] name = "tox-uv" version = "1.35.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tox-uv-bare" }, { name = "uv" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ca/dc/6e9994c799bdbb309f829dd6b8d98764dd0757302f3433c380438a3a127b/tox_uv-1.35.2-py3-none-any.whl", hash = "sha256:2d99b0e3c782ba49e7cbe521c8d344758595961b17a3633738d67096641c1bde", size = 6565, upload-time = "2026-05-05T01:34:16.07Z" }, ] [[package]] name = "tox-uv-bare" version = "1.35.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tox" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0a/cb/168dc1ccf24e4065a9a0a33df55709ed2b5eb73bd2b13ddd53187e5dffb8/tox_uv_bare-1.35.2.tar.gz", hash = "sha256:49e28a804c97f23ea17e25859960c0fa78f35bccb7e14344cfd840e89a9aade9", size = 32333, upload-time = "2026-05-05T01:34:18.916Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5f/53/4a33dc81da39db7b31e5622333df361e8fe055b7ec636bd5fea762c9182d/tox_uv_bare-1.35.2-py3-none-any.whl", hash = "sha256:c0d590a41d1054a1ad0874e9e5943ff52402786e3d4599d8f8d37a65b566ef53", size = 22307, upload-time = "2026-05-05T01:34:17.681Z" }, ] [[package]] name = "tqdm" version = "4.68.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" }, ] [[package]] name = "translationstring" version = "1.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/14/39/32325add93da9439775d7fe4b4887eb7986dbc1d5675b0431f4531f560e5/translationstring-1.4.tar.gz", hash = "sha256:bf947538d76e69ba12ab17283b10355a9ecfbc078e6123443f43f2107f6376f3", size = 24199, upload-time = "2020-07-09T11:58:32.041Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3b/98/36187601a15e3d37e9bfcf0e0e1055532b39d044353b06861c3a519737a9/translationstring-1.4-py2.py3-none-any.whl", hash = "sha256:5f4dc4d939573db851c8d840551e1a0fb27b946afe3b95aafc22577eed2d6262", size = 15028, upload-time = "2020-07-09T11:58:30.995Z" }, ] [[package]] name = "truststore" version = "0.10.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] name = "tzdata" version = "2026.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, ] [[package]] name = "tzlocal" version = "5.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/81/5b/879b2f932adfa7a053c360d50bc896c977fa6426109185f7c12ebdd0cb9d/tzlocal-5.4.4.tar.gz", hash = "sha256:8dbb8660838688a7b6ba4fed31d18dedf842afb4d47ca050d6d891c2c15f3be4", size = 31170, upload-time = "2026-06-29T08:03:40.026Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/a4/017a7a6cbe387d961a688ec31364ae60a5c4e22c96ae9921b79a947c855d/tzlocal-5.4.4-py3-none-any.whl", hash = "sha256:aae09f0126a8a86fa736be266eb4a471380d26a0de3bc14844e7821fee3e2a15", size = 18115, upload-time = "2026-06-29T08:03:38.666Z" }, ] [[package]] name = "urllib3" version = "2.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] name = "uv" version = "0.11.26" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d0/cb/5efc713948ddb10b00abfb51bfd429221c720175557f9c7965fea2448fe4/uv-0.11.26.tar.gz", hash = "sha256:2a433ece2ace088dd572d8abb0e6bd9a4ecb0e10bc9856447bbb37545f384f29", size = 4331220, upload-time = "2026-06-30T14:52:03.77Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/71/86dbffac9e26df28a16639c426cf4ba572aaf43d9231463e0dca337895b2/uv-0.11.26-py3-none-linux_armv6l.whl", hash = "sha256:fb97bf04512dfe16d86084e75d8129701fc8da9fb40de8746b73c3aa617c5897", size = 25197324, upload-time = "2026-06-30T14:50:51.75Z" }, { url = "https://files.pythonhosted.org/packages/ec/80/525b73c8188e7052343e7109466a08fcd5195055aff4b0346ce3622e48cb/uv-0.11.26-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a58a06e5a4b0035538d3ab4160ad74c716076ea7148eb3317171c6276ac020b4", size = 24179172, upload-time = "2026-06-30T14:50:56.52Z" }, { url = "https://files.pythonhosted.org/packages/7b/5e/cf7b94ed3b1932c2a62573dcd388ad6c1da5c52111cd71ab7f20faa4a0aa/uv-0.11.26-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b6d078d2ce83897884c2330c0676f27be4bf3d223fb2a409460f579fb5f0a98", size = 22949576, upload-time = "2026-06-30T14:51:00.538Z" }, { url = "https://files.pythonhosted.org/packages/bf/fd/71fa021f6909c4139d8354bea623b5e0ef0ce4a08da250da1a1645528da2/uv-0.11.26-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:1cd9ba4951681ce17f1703106266fcbe27aaa7d37f07d53cce8b5686d68a8755", size = 24936673, upload-time = "2026-06-30T14:51:04.496Z" }, { url = "https://files.pythonhosted.org/packages/7d/5e/273425e58a8812423e3d1f6c5da1015e636fbf13a83d104317ca37e16304/uv-0.11.26-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:e4f4c3268e69ac96f01972274a62f5f930c03cbc680adba6f21e63237ba3a639", size = 24719617, upload-time = "2026-06-30T14:51:08.419Z" }, { url = "https://files.pythonhosted.org/packages/81/f8/1601e2acc7c54963814b4831eab996d8599e690712722c5acec5114860be/uv-0.11.26-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:efcbe0e187846f5ddba23bcaed17e4f9cd2463da5c45bdb5869616f686d713ff", size = 24734176, upload-time = "2026-06-30T14:51:12.685Z" }, { url = "https://files.pythonhosted.org/packages/88/d2/a8a422e54c08cf4b8d51bedb9dbdd3cc233aa290ad8b3ee0438c0c02a3a5/uv-0.11.26-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:120ab2de93164d08cf5950f7fe18cbebe3ff670865ae41a292452bab2346477f", size = 26158780, upload-time = "2026-06-30T14:51:16.514Z" }, { url = "https://files.pythonhosted.org/packages/db/e6/647fe5fdc888a3d27f79977877ce4e88052fe9be5398371e51bb134fc262/uv-0.11.26-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9052bf27c7ee426901f35a48715fa9288ce631c1878b91c9a6c950288f4b8633", size = 27009550, upload-time = "2026-06-30T14:51:20.659Z" }, { url = "https://files.pythonhosted.org/packages/72/c2/85d8e762ad83b0f14fae2255b0578c4fd7dc915746f81b64ed786342627a/uv-0.11.26-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:efdddfcc9b1b790c5f7985c5c183c851682ced165b44ffa914f4947f5cad1fbf", size = 26183777, upload-time = "2026-06-30T14:51:24.715Z" }, { url = "https://files.pythonhosted.org/packages/d3/00/478c3a870dcac690b8c337ee950a60a952e817f574945e85155c3cc0ab34/uv-0.11.26-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dcf4e0b5b5cbdc242dcb002f1f8d99e7cf8c043609869228a9ce15e095c0b18", size = 26260589, upload-time = "2026-06-30T14:51:28.809Z" }, { url = "https://files.pythonhosted.org/packages/a7/51/e4e43e106fb8cdc026b97491ea4600f4194a9c4da0b4e4e30c2a7dceb268/uv-0.11.26-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:866ae8d28f7381c15de0906a284c1e97916424c635bf40f7960b3fc889cd725e", size = 25073850, upload-time = "2026-06-30T14:51:32.717Z" }, { url = "https://files.pythonhosted.org/packages/f2/c2/e772b7e6c8a835e8bf6739a391cdfc8e8e244c5c496d9b40625068b59ff4/uv-0.11.26-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:22f6d62e794b252ff3a1e2dfe5010cc76208f90b2c906e54971a0223ad6f16bc", size = 25682609, upload-time = "2026-06-30T14:51:36.888Z" }, { url = "https://files.pythonhosted.org/packages/1a/69/ea77209a224a23a399cb7f6414f77ef032bd9e083e01199a0ebebf0d3ff2/uv-0.11.26-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:edd0c12b75141a6d830d138a91e366ad66e630f1c1dcaf83b8325b80cbacfcbb", size = 25800556, upload-time = "2026-06-30T14:51:40.937Z" }, { url = "https://files.pythonhosted.org/packages/77/60/b6c0c03d2538a016b6624fa251960012e564ea02f841e958c7d60e974685/uv-0.11.26-py3-none-musllinux_1_1_i686.whl", hash = "sha256:af6a45b11a569cc4d2437e89a25a53dcf753f2a02a8f2de96be09b9b942cb3ec", size = 25385658, upload-time = "2026-06-30T14:51:45.103Z" }, { url = "https://files.pythonhosted.org/packages/8d/e7/46881ff9164aa2e7c649901837d58eee3c57beb3b0fcc0fea6a4e40cf8f3/uv-0.11.26-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:c28822517d03aebbe9549aaaecc88ad580e4b2b6a927abffe5774a74d6ba09f6", size = 26551013, upload-time = "2026-06-30T14:51:49.062Z" }, { url = "https://files.pythonhosted.org/packages/d6/94/380dad6c2bbe12417025aacd12cfc08322ed4c9dd8f760bff7035b86f22d/uv-0.11.26-py3-none-win32.whl", hash = "sha256:79e5c1b3410047e1962290c3b7b8f512d2c1bb95200c60b016f7729287cf34c0", size = 23947180, upload-time = "2026-06-30T14:51:53.065Z" }, { url = "https://files.pythonhosted.org/packages/d0/13/9c588226d5b478328d739e654944430719f3ffe8999d6a24d425ec9664ab/uv-0.11.26-py3-none-win_amd64.whl", hash = "sha256:d95567e9470dc48ff03265f420c3c6973f6437f18a79d5e00b6eb4b2d9379907", size = 26909320, upload-time = "2026-06-30T14:51:57.235Z" }, { url = "https://files.pythonhosted.org/packages/21/1d/ea66b12813878797126e2b3aca124b1c9c5ef53120702d1c00172f90a21d/uv-0.11.26-py3-none-win_arm64.whl", hash = "sha256:7e69d1569afbb936e7bf4e4ab2f72d606405f4a68f380f088a0b2233e84e056a", size = 25176820, upload-time = "2026-06-30T14:52:01.05Z" }, ] [[package]] name = "uvicorn" version = "0.49.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" }, ] [[package]] name = "valkey" version = "6.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c3/ee/7fd930fc712275084722ddd464a0ea296abdb997d2da396320507968daeb/valkey-6.1.1.tar.gz", hash = "sha256:5880792990c6c2b5eb604a5ed5f98f300880b6dd92d123819b66ed54bb259731", size = 4601372, upload-time = "2025-08-11T06:41:10.63Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d0/a2/252afa4da08c714460f49e943070f86a02931f99f886182765194002fe33/valkey-6.1.1-py3-none-any.whl", hash = "sha256:e2691541c6e1503b53c714ad9a35551ac9b7c0bbac93865f063dbc859a46de92", size = 259474, upload-time = "2025-08-11T06:41:08.769Z" }, ] [[package]] name = "venusian" version = "3.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/70/4c/eefa68085c555dc11e6744b9c6fbe5966b1c9378c47267776a448923e9a5/venusian-3.1.1.tar.gz", hash = "sha256:534fb3b355669283eb3954581931e5d1d071fce61d029d58f3219a5e3a6f0c41", size = 39269, upload-time = "2024-12-02T02:35:04.033Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5a/4b/34d926eba40db81b204066a60b4efdc5d8867a8efcbfe44d69b634b1c907/venusian-3.1.1-py3-none-any.whl", hash = "sha256:0845808a985976acbceaa1fbb871c7fac4fb28ae75453232970e9c2c2866dbf4", size = 14026, upload-time = "2024-12-02T02:35:03.056Z" }, ] [[package]] name = "vine" version = "5.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" }, ] [[package]] name = "virtualenv" version = "21.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, { name = "python-discovery" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f1/a5/81f987504738e6defeed61ec1c47e2aefab3c35d8eeb87e1b3f38cf28254/virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8", size = 4578798, upload-time = "2026-06-16T16:23:58.603Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" }, ] [[package]] name = "wcwidth" version = "0.8.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, ] [[package]] name = "webob" version = "1.8.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "legacy-cgi", marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/f9/974eafebfd0bd442b8848899fe7d30675c93f750c313e1a6fe61acbde1e3/webob-1.8.10.tar.gz", hash = "sha256:1c963a11f307bc3f624fbab9dde737701eae255f32981b7a5486a88db1767c2b", size = 280796, upload-time = "2026-06-02T19:56:47.268Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/83/21/fce134877fb6fc6ad3c464e4a07ede0ee9219f705d26a981ae58ea36ca13/webob-1.8.10-py2.py3-none-any.whl", hash = "sha256:e68ad87fda378191081965ab02a185391c26e4e926adec855c3b0286a8369d49", size = 115825, upload-time = "2026-06-02T19:56:44.765Z" }, ] [[package]] name = "websockets" version = "15.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] [[package]] name = "werkzeug" version = "3.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, ] [[package]] name = "wrapt" version = "1.17.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] [[package]] name = "yarl" version = "1.24.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3f/df/f1c7a3de0831cd83194f1a85c5bb431b13f81e6b45079314c86d1c4ef3f2/yarl-1.24.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5249a113065c2b7a958bc699759e359cd61cfc81e3069662208f48f191b7ed12", size = 129057, upload-time = "2026-05-19T21:27:47.564Z" }, { url = "https://files.pythonhosted.org/packages/48/41/7daafb32dd7562bf45b1ce56562e7e1a9146f6479b6456873eb8a3413c40/yarl-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4425fa244fbf530b006d0c5f79ce920114cfff5b4f5f6056e669f8e160fdc0", size = 91545, upload-time = "2026-05-19T21:27:50.089Z" }, { url = "https://files.pythonhosted.org/packages/a8/8f/7b3ec212f1ea0683f55f978e3246bc313c38818664edfc97a9f349a4901e/yarl-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15c0b5e49d3c44e2a0b93e6a49476c5edad0a7686b92c395765a7ea775572a75", size = 91380, upload-time = "2026-05-19T21:27:51.953Z" }, { url = "https://files.pythonhosted.org/packages/8a/1b/8bafab7db23b0567ae9db749099b329d91e3b82bc6028b2050ba583e116c/yarl-1.24.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:246d32a53a947c8f0189f5d699cbd4c7036de45d9359e13ba238d1239678c727", size = 105957, upload-time = "2026-05-19T21:27:53.98Z" }, { url = "https://files.pythonhosted.org/packages/7f/77/21030c2f8d21d21559719beafc772ada2014be933418ed1eaed9cc800e42/yarl-1.24.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:64480fb3e4d4ed9ed71c48a91a477384fc342a50ca30071d2f8a88d51d9c9413", size = 97242, upload-time = "2026-05-19T21:27:55.981Z" }, { url = "https://files.pythonhosted.org/packages/50/d8/f9ea63d1b6aa910a866e089d871fff6cbd49caab29b86b35221a62dfa0d5/yarl-1.24.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:349de4701dc3760b6e876628423a8f147ef4f5599d10aba1e10702075d424ed9", size = 114719, upload-time = "2026-05-19T21:27:58.037Z" }, { url = "https://files.pythonhosted.org/packages/e9/a3/04e0ee98ac58a249ea7ed75223f5f901ba81a834f0b4921b58e5cec11757/yarl-1.24.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d162677af8d5d3d6ebab8394b021f4d041ac107a4b705873148a77a49dc9e1b2", size = 112140, upload-time = "2026-05-19T21:27:59.618Z" }, { url = "https://files.pythonhosted.org/packages/02/ad/0b9cc9f38a7324a7eb1d80f834eaa5283d17e9271bbda3186e598dddaeac/yarl-1.24.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5f5c6ec23a9043f2d139cc072f53dd23168d202a334b9b2fda8de4c3e890d90", size = 106721, upload-time = "2026-05-19T21:28:02.586Z" }, { url = "https://files.pythonhosted.org/packages/65/e7/a52478ebfc66ec989e085c6ae038b9f1bfa4190baa193b133b669c709e2f/yarl-1.24.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:60de6742447fbbf697f16f070b8a443f1b5fe6ca3826fbef9fe70ecd5328e643", size = 106478, upload-time = "2026-05-19T21:28:04.523Z" }, { url = "https://files.pythonhosted.org/packages/04/d8/5508530fea8472542de00013ae280765fc938ee196fc4030c43a498afb36/yarl-1.24.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acf93187c3710e422368eb768aee98db551ec7c85adc250207a95c16548ab7ac", size = 105423, upload-time = "2026-05-19T21:28:06.515Z" }, { url = "https://files.pythonhosted.org/packages/84/f1/ece28505e9628e8b756e11bb4f28864a17cc33b6b44db4d2aaf0622bf630/yarl-1.24.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f4b0352fd41fd34b6651934606268816afd6914d09626f9bcbbf018edb0afb3f", size = 99878, upload-time = "2026-05-19T21:28:08.637Z" }, { url = "https://files.pythonhosted.org/packages/3f/52/fb5d34529b46dd84013afcfb30b8d2bc2832ed03d412736f577d604fa393/yarl-1.24.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6b208bb939099b4b297438da4e9b25357f0b1c791888669b963e45b203ea9f36", size = 114025, upload-time = "2026-05-19T21:28:10.64Z" }, { url = "https://files.pythonhosted.org/packages/43/f0/ff9d31aaab024f7a251c0ed308a98ae29bf9f7dc344e78f28b1322431ca2/yarl-1.24.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4b85b8825e631295ff4bc8943f7471d54c533a9360bbe15ebb38e018b555bb8a", size = 105613, upload-time = "2026-05-19T21:28:12.784Z" }, { url = "https://files.pythonhosted.org/packages/31/7d/3296fb3f3ecd52bf9ae6c16b0895c1cda7e9170a2083861552b683f70264/yarl-1.24.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e26acf20c26cb4fefc631fdb75aca2a6b8fa8b7b5d7f204fb6a8f1e63c706f53", size = 111665, upload-time = "2026-05-19T21:28:14.393Z" }, { url = "https://files.pythonhosted.org/packages/1a/74/77aa6ddaca4fbf42e45e675a465c43956dd40702281049975a2aa04eae59/yarl-1.24.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:819ca24f8eafcfb683c1bd5f44f2f488cea1274eb8944731ffd2e1f10f619342", size = 106914, upload-time = "2026-05-19T21:28:15.893Z" }, { url = "https://files.pythonhosted.org/packages/d8/02/7611f22cd1d4ed7373eb7f9ee21fde1046edba2e7c0e514880d760352f48/yarl-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:5cb0f995a901c36be096ccbf4c673591c2faabbe96279598ffaec8c030f85bf4", size = 92658, upload-time = "2026-05-19T21:28:17.471Z" }, { url = "https://files.pythonhosted.org/packages/91/00/671d0add79938127292839ae44506ce2f7fe8909c72d5a931864f128fd0b/yarl-1.24.2-cp310-cp310-win_arm64.whl", hash = "sha256:f408eace7e22a68b467a0562e0d27d322f91fe3eaaa6f466b962c6cfaea9fa39", size = 87887, upload-time = "2026-05-19T21:28:19.021Z" }, { url = "https://files.pythonhosted.org/packages/c5/c5/1ce244152ff2839645e7cae92f90e7bafcb2c52bea7ff586ac714f14f5df/yarl-1.24.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:36348bebb147b83818b9d7e673ea4debc75970afc6ffdc7e3975ad05ce5a58c1", size = 128971, upload-time = "2026-05-19T21:28:20.543Z" }, { url = "https://files.pythonhosted.org/packages/87/5a/00f36967203ed89cb3acd2c8ed526cc3fed9418eb70ce128160a911c8499/yarl-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a97e42c8a2233f2f279ecadd9e4a037bcb5d813b78435e8eedd4db5a9e9708c", size = 91507, upload-time = "2026-05-19T21:28:22.556Z" }, { url = "https://files.pythonhosted.org/packages/31/d0/1fb0c1cd27288f39f6974da4318c32768d72c9890984541fdf1e2e32a51d/yarl-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d027d56f1035e339d1001ac33eceab5b2ec8e42e449787bb75e289fb9a5cd1d", size = 91343, upload-time = "2026-05-19T21:28:24.092Z" }, { url = "https://files.pythonhosted.org/packages/03/ce/d4a646508bed2f8dec6435b40166fe9308dd191262033d3f307b2bbcaecd/yarl-1.24.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a6377060e7927187a42b7eb202090cbe2b34933a4eeaf90e3bd9e33432e5cae", size = 105704, upload-time = "2026-05-19T21:28:25.872Z" }, { url = "https://files.pythonhosted.org/packages/4b/07/b3278e82d8bc41485bcf6d856cd0433262593de615b1d3dc43bd3f5bead4/yarl-1.24.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:17076578bce0049a5ce57d14ad1bded391b68a3b213e9b81b0097b090244999a", size = 97281, upload-time = "2026-05-19T21:28:27.352Z" }, { url = "https://files.pythonhosted.org/packages/17/5b/4cee6e7c92e487bebe7afc797da0aa54a248ab4e776a68fe369ec29665a5/yarl-1.24.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:50713f1d4d6be6375bb178bb43d140ee1acb8abe589cd723320b7925a275be1e", size = 114020, upload-time = "2026-05-19T21:28:29.458Z" }, { url = "https://files.pythonhosted.org/packages/5c/82/111076571545a7d4f9cca3fbd5c6f40615af58642be09f12328f48022468/yarl-1.24.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:34263e2fa8fb5bb63a0d97706cda38edbad62fddb58c7f12d6acbc092812aa50", size = 111450, upload-time = "2026-05-19T21:28:31.262Z" }, { url = "https://files.pythonhosted.org/packages/b6/ec/08f671f69a444d704aeecebf92af659b67b97a869942411d0a578b08c334/yarl-1.24.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49016d82f032b1bd1e10b01078a7d29ae71bf468eeae0ea22df8bab691e60003", size = 106384, upload-time = "2026-05-19T21:28:32.856Z" }, { url = "https://files.pythonhosted.org/packages/e5/86/ce41e7a7a199340b2330d52b60f25c4074b6636dd0e60b1a80d31a9db042/yarl-1.24.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3f6d2c216318f8f32038ca3f72501ba08536f0fd18a36e858836b121b2deed9f", size = 106153, upload-time = "2026-05-19T21:28:35.222Z" }, { url = "https://files.pythonhosted.org/packages/c4/5d/31be8a729531ab3e55ac3e7e5c800be8c89ea98947f418b2f6ea259fb6ee/yarl-1.24.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:08d3a33218e0c64393e7610284e770409a9c31c429b078bcb24096ed0a783b8f", size = 105322, upload-time = "2026-05-19T21:28:36.642Z" }, { url = "https://files.pythonhosted.org/packages/47/9b/b57afb22b386ae87ac9940f09878b98d8c333f89113e6fc96fcf4ca9eb64/yarl-1.24.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5d699376c4ca3cba49bbfae3a05b5b70ded572937171ce1e0b8d87118e2ba294", size = 99057, upload-time = "2026-05-19T21:28:38.386Z" }, { url = "https://files.pythonhosted.org/packages/a3/4f/06348c27c8389256c313e8a57d796808fc0264c915dd5e7cfd3c0e314dc7/yarl-1.24.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a1cab588b4fa14bea2e55ebea27478adfb05372f47573738e1acc4a36c0b05d2", size = 113502, upload-time = "2026-05-19T21:28:40.091Z" }, { url = "https://files.pythonhosted.org/packages/5f/1c/284f307b298e4a17b7943b07d9d7ecc4151537f8d137ba51f3bb6c31ca20/yarl-1.24.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ec87ccc31bd21db7ad009d8572c127c1000f268517618a4cc09adba3c2a7f21c", size = 105253, upload-time = "2026-05-19T21:28:41.987Z" }, { url = "https://files.pythonhosted.org/packages/c8/bf/0de123bec8619e45c80cbded9085f61b5b4a9eddb8abe6d25d28ee1ec866/yarl-1.24.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d1dd47a22843b212baa8d74f37796815d43bd046b42a0f41e9da433386c3136b", size = 111345, upload-time = "2026-05-19T21:28:43.93Z" }, { url = "https://files.pythonhosted.org/packages/90/af/0248eb065e51129d2a9b2436cd1b5c772c19a6b04e5b6a186955671e3319/yarl-1.24.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7b54b9c67c2b06bd7b9a77253d242124b9c95d2c02def5a1144001ee547dd9d5", size = 106558, upload-time = "2026-05-19T21:28:45.806Z" }, { url = "https://files.pythonhosted.org/packages/21/3c/f960d7a65ef97d8ba9b424fb5128796a4bc710fc6df2ddbbd7dfdc3bbd20/yarl-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:f8fdbcff8b2c7c9284e60c196f693588598ddcee31e11c18e14949ce44519d45", size = 92808, upload-time = "2026-05-19T21:28:48.465Z" }, { url = "https://files.pythonhosted.org/packages/03/1a/49fb03750e4de4d2284cd5b885a383133c34eef45bd59631b2bb8b7e81e8/yarl-1.24.2-cp311-cp311-win_arm64.whl", hash = "sha256:b32c37a7a337e90822c45797bf3d79d60875cfcccd3ecc80e9f453d87026c122", size = 87610, upload-time = "2026-05-19T21:28:50.07Z" }, { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" }, { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" }, { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" }, { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" }, { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" }, { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" }, { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" }, { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" }, { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" }, { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" }, { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" }, { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" }, { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" }, { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" }, { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" }, { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" }, { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" }, { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" }, { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" }, { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" }, { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" }, { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" }, { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" }, { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" }, { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" }, { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" }, { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" }, { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" }, { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" }, { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" }, { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" }, { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" }, { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" }, { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" }, { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" }, { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" }, { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" }, { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" }, { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" }, { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" }, { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" }, { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" }, { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" }, { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" }, { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" }, { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" }, { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" }, { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" }, { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" }, { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" }, { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" }, { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" }, { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" }, { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" }, { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" }, { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" }, { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" }, { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" }, { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" }, { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" }, { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" }, { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" }, { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" }, { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" }, { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" }, { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" }, { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" }, { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, ] [[package]] name = "zope-deprecation" version = "6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "setuptools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/56/05/8eaa04876e1fde656f1bfd8f62a4140d7ac23bcf16db67dd4036698dae1f/zope_deprecation-6.0.tar.gz", hash = "sha256:18727ebda8e63a6d4bd28a290e8b46852e9f14473debb5cc40a0a2dccfadf15f", size = 24480, upload-time = "2025-09-12T07:06:06.779Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e9/ed/da7f8b1c73caf989a0ff7096cc73c59e6c36f35a8f967c51104098602e2d/zope_deprecation-6.0-py3-none-any.whl", hash = "sha256:ff72d51c88b516b9ddf2cfb826381cc49f99a6a89b7d35c97faca7bee3b46da6", size = 9512, upload-time = "2025-09-12T07:06:05.847Z" }, ] [[package]] name = "zope-interface" version = "8.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/08/dc/50550cfcbb2ea3cbca5f1d7ed05c8aa840f831a0f2d63aec0a953f7c590e/zope_interface-8.5.tar.gz", hash = "sha256:7a3ba1c5877f0f3e3906b02ddf793abed2becc2948116414ce0e1dd820b68d6d", size = 257957, upload-time = "2026-05-26T06:50:14.574Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b6/43/9cd98bee951d23848de690ba2809f87e3b22c67c370987acc960da15ad37/zope_interface-8.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0c8aa2bf8f3911ef37b87deb1bbe225a310e6eb6522a16d77f5d8330c4f6fbe", size = 210951, upload-time = "2026-05-26T06:49:00.178Z" }, { url = "https://files.pythonhosted.org/packages/17/0f/8f1a29966bcf863e3a2121edcafb81c55715de7886bcc9544749cc79e7da/zope_interface-8.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:efe234a0fafb4b6b1602e9be9245b97c2bf06d67c07af5a4bc3c0438978b555c", size = 211309, upload-time = "2026-05-26T06:49:02.732Z" }, { url = "https://files.pythonhosted.org/packages/9f/9f/37e564eaaf85e3abc1ada40a79fa43f2ab45bdb67431b0ec0fe29e4763e2/zope_interface-8.5-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:dabeb6fe1228d411994f300811edc6866fff0cdcbc9cef98a78f05ea0da42e37", size = 254881, upload-time = "2026-05-26T06:49:04.303Z" }, { url = "https://files.pythonhosted.org/packages/06/61/e6501d8ea7a2cac3217e03f404e1f98c1df7191d83cfe86b1895fbba5dac/zope_interface-8.5-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:147a9442dcc2b7339ecdb1be2b3cdb098e90462e39425054053ebfb50d99125a", size = 259811, upload-time = "2026-05-26T06:49:06.373Z" }, { url = "https://files.pythonhosted.org/packages/91/15/bfa25ef480b02af6e9452c478483fec75e87c9e2b60c407fd0b1f6054b9c/zope_interface-8.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a17e681224267880707c9ec9e730ad9a1ad2d65c371256843efba6cf48711b58", size = 260358, upload-time = "2026-05-26T06:49:08.317Z" }, { url = "https://files.pythonhosted.org/packages/64/51/2b518072fea76242da64451d501c69b7b5ccdef9b57fead584ccf1c180d5/zope_interface-8.5-cp310-cp310-win_amd64.whl", hash = "sha256:d178968a1a611df30549a717d1624cb38ca810347339e3e37b7baa6f6781a170", size = 214822, upload-time = "2026-05-26T06:49:10.441Z" }, { url = "https://files.pythonhosted.org/packages/ce/f1/83ad110fb847413affe71609bb50e59e1aa082e1236030122227c7c283d3/zope_interface-8.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:afc66ccaef2a3c0bef6ca02aad40d29a39276389dad16a8eac36f9f385e4d057", size = 211426, upload-time = "2026-05-26T06:49:12.595Z" }, { url = "https://files.pythonhosted.org/packages/bb/a7/6b6e0c31ac240cb9fc015ae9ed45ca54be886c18fcf7bfa2377a4d7a8785/zope_interface-8.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c28044972187245d7a309e4699319bfdbd2ffcbf7176d1d4ddf5adffb2dea80f", size = 211850, upload-time = "2026-05-26T06:49:14.474Z" }, { url = "https://files.pythonhosted.org/packages/37/36/7599ecabcf80ce4fef2e1ef3c5ac0d4696b61f03f724cc44022f4d226af9/zope_interface-8.5-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:03bbecc7982af713d7499d4084bc03916413d17ffd45f89009348cc0c1d9e376", size = 260711, upload-time = "2026-05-26T06:49:16.568Z" }, { url = "https://files.pythonhosted.org/packages/03/3e/1774b0ee46ccbb5498ee3c33ece40315b6ef58bc71957be94bd345340bc1/zope_interface-8.5-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf917009a4a7457c7290225a019f4a0aa706d96accd2cfdba2418d3bc1fcde2f", size = 265277, upload-time = "2026-05-26T06:49:18.656Z" }, { url = "https://files.pythonhosted.org/packages/b6/09/e533b2ffabaae4e5d5730d6768a591cf335defe8e37bec2ad905d09be656/zope_interface-8.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:31cff25b2aaedb5267e6e77b1e9be6b0ec4f622032de8a069202b8ffacda7dc2", size = 266369, upload-time = "2026-05-26T06:49:20.174Z" }, { url = "https://files.pythonhosted.org/packages/49/4a/3ebe6a4c122b2d5340db45cbe7e490663d3228b172710ec71060cd5d541e/zope_interface-8.5-cp311-cp311-win_amd64.whl", hash = "sha256:17a3114bbdddb5e75e5784cdf318944636190cbbc72d357ef9fb1a8b0351f955", size = 215161, upload-time = "2026-05-26T06:49:21.799Z" }, { url = "https://files.pythonhosted.org/packages/d2/59/056ad97af5b16db1975ee98ec7ab03d2ce3f3355efad904ced1dbce0e39f/zope_interface-8.5-cp311-cp311-win_arm64.whl", hash = "sha256:aab6bb5bee10f38ea688b95ba054396b67f613552d2c8378be7fcb2d2fba7646", size = 213481, upload-time = "2026-05-26T06:49:25.085Z" }, { url = "https://files.pythonhosted.org/packages/97/cc/b84123a948f3162a34623e188922827cd845244fdd043ed20f8d02228caa/zope_interface-8.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8e6ee90c2e6de7c37058d5fa41f123c8b13a312db8d1e0fb5840d7f4bcdff9c9", size = 212165, upload-time = "2026-05-26T06:49:26.566Z" }, { url = "https://files.pythonhosted.org/packages/4e/78/cbceec44f1b27208a76c1a688c131302685852406a23df5aab68324109cc/zope_interface-8.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1adc90d3576b3b4c4de4953e6002c37bef28b78d7fa54c1bbfd0c50f022fe7c", size = 212341, upload-time = "2026-05-26T06:49:28.182Z" }, { url = "https://files.pythonhosted.org/packages/e1/c3/005032195ff3b210c139b7c560ed5c534e844b0907d8e44d2b3d8919305e/zope_interface-8.5-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:e6347b8d8d12c5eca6502450a92be30079b7acfade2c4f693efa0deb8871b06e", size = 265296, upload-time = "2026-05-26T06:49:29.741Z" }, { url = "https://files.pythonhosted.org/packages/c5/66/1036543d6a66bc04c19df3cf650f3ad938a002ab0a443c24e23e8de5e8b9/zope_interface-8.5-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5e970dabea777a24b0b0bbf9dae3ab75ce8b2d8e948edf4875627034b21f3560", size = 270689, upload-time = "2026-05-26T06:49:31.767Z" }, { url = "https://files.pythonhosted.org/packages/30/4c/8b56259558cace4414e753ca6740396a1f59d4a95ddb55b4658600408670/zope_interface-8.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f0b48ccadaa9839e09ff81e969703cecb3f402c813bfe8b958652e699bea69f5", size = 270280, upload-time = "2026-05-26T06:49:33.489Z" }, { url = "https://files.pythonhosted.org/packages/f9/ea/649908c83aa8fdb7faf2ddca4d3cf6fb8f2157121267dc56e8f72681e26c/zope_interface-8.5-cp312-cp312-win_amd64.whl", hash = "sha256:e0e311f1277468c08fd59a2b41f71b43d25dff639789d364747acd1705c0df6e", size = 215019, upload-time = "2026-05-26T06:49:35.607Z" }, { url = "https://files.pythonhosted.org/packages/9f/97/da13037b4c563e4df32eedbc819f8c00b754af494f68211e3dffd48d52da/zope_interface-8.5-cp312-cp312-win_arm64.whl", hash = "sha256:652b73107a04159ec6c020db6c1543d4f1e8f4d069bd2aac88a947820923517b", size = 213569, upload-time = "2026-05-26T06:49:37.317Z" }, { url = "https://files.pythonhosted.org/packages/f4/8c/4c15755d701f2ec0e80d64a18e1ebaf5be2c584c0ec153fd516f5d13eada/zope_interface-8.5-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:28e80457c134d1fa57a7d758004dece348654e1b1467ac22dcdc20fc1d127c52", size = 212512, upload-time = "2026-05-26T06:49:38.996Z" }, { url = "https://files.pythonhosted.org/packages/9a/2e/4360c54c465db042cc8fbeeec92abac28b4cedbf6ba63c1f092fd08a190f/zope_interface-8.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:09495ce9d559c06b70f2d4855b3e4f48a822a9ddc8be1d30c5b4e5be14ae1ace", size = 212541, upload-time = "2026-05-26T06:49:41.186Z" }, { url = "https://files.pythonhosted.org/packages/aa/a5/692a2b8d70f78e848793231d5fae5fecbf8d0cccd73430fdc34802a6d3c1/zope_interface-8.5-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:7849ad8fa90763cc1087f4dda78ca3a233e950b3e08fac7079297c9cafbbd7bb", size = 265191, upload-time = "2026-05-26T06:49:43.449Z" }, { url = "https://files.pythonhosted.org/packages/70/8d/454a9cfc7a050c394ab4f11b3371f7897828b7415e096afff724637e65e0/zope_interface-8.5-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5578c9421ca409a1f39f153d6f7803e4cde01da592ec75a9ac5e1b777d18d33b", size = 270626, upload-time = "2026-05-26T06:49:45.425Z" }, { url = "https://files.pythonhosted.org/packages/51/8c/db8409cfa3575b8e9b4800babd7d49f8228433cd1f0c56814bd0ada49c33/zope_interface-8.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e1bd7d96b4ca5fa311f54c9eac16dce4886b428c1531dbe06067763ccdf123b4", size = 270444, upload-time = "2026-05-26T06:49:47.025Z" }, { url = "https://files.pythonhosted.org/packages/4a/df/a386940e41469ef615e100a216d8b386521e9e598817147f87932ca203c4/zope_interface-8.5-cp313-cp313-win_amd64.whl", hash = "sha256:0c8123d2a4dfde2a613c7cb772605477724782c20bc2e0ad1d9435376a6a44a3", size = 215021, upload-time = "2026-05-26T06:49:48.478Z" }, { url = "https://files.pythonhosted.org/packages/89/75/477eb5669b6b2a7a843decd1a075e9b1971a8720017654143a7183abd3d9/zope_interface-8.5-cp313-cp313-win_arm64.whl", hash = "sha256:6d02be14f3173c6c7288bc2fdf530090c01c3cf8764ad46c68024686f364278e", size = 213610, upload-time = "2026-05-26T06:49:50.01Z" }, { url = "https://files.pythonhosted.org/packages/d4/19/5032e954827fdf02db2d2f49737ac4378bb9cfc2cd95a8f2e2a5ae2ec01a/zope_interface-8.5-cp314-cp314-macosx_10_9_x86_64.whl", hash = "sha256:ffaecf013251a89d0de6feb49a46eba48ad8cbbf8a40aeb6045e459e7bec6784", size = 212597, upload-time = "2026-05-26T06:49:51.63Z" }, { url = "https://files.pythonhosted.org/packages/f1/53/3ef644012cf8a6a234a2d6134aab5a5c65ac5467c86296865501d4fbc406/zope_interface-8.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:126fa9d1c52295ae076d4cf968634f0a1826afa408a20808b57ff72877b8f69f", size = 212626, upload-time = "2026-05-26T06:49:53.236Z" }, { url = "https://files.pythonhosted.org/packages/32/67/bc8b4f465d388039255003e230c284a175cedf1203c692f23cb7bff64efe/zope_interface-8.5-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:3090e3a663d20194756a59a272e0c8508b889341e31d5894223331fe6b4f9b21", size = 266827, upload-time = "2026-05-26T06:49:54.873Z" }, { url = "https://files.pythonhosted.org/packages/a7/eb/37d05b935ede53d79690fecc8d201440084418e590bcfc05f384451c7593/zope_interface-8.5-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9342fb74e2afefdb081bf1df727d209ea56995c6e13f5a0540e6d7aff4beafb8", size = 270139, upload-time = "2026-05-26T06:49:57.116Z" }, { url = "https://files.pythonhosted.org/packages/8b/0b/fd0c54579e2ce8dc6cf1a757903f3374bc6fbda929a46af9e0f53cb0e5f0/zope_interface-8.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6c54725d818f1b57a7efb8b16528326e1f3c257b602b32393fd255c45af8799d", size = 270338, upload-time = "2026-05-26T06:49:58.698Z" }, { url = "https://files.pythonhosted.org/packages/c1/1d/c420dcd777bb761067ea92879ac766694a5ca78608185f1aecea64cbfc11/zope_interface-8.5-cp314-cp314-win_amd64.whl", hash = "sha256:29d74febbae1afeb6834c4ccbf42e242a673c860060f09e53142825270456140", size = 215789, upload-time = "2026-05-26T06:50:00.405Z" }, { url = "https://files.pythonhosted.org/packages/62/94/50b5eb8f94e527edceac14f9955e58917424ea79bb572ddc18548561cbc2/zope_interface-8.5-cp314-cp314-win_arm64.whl", hash = "sha256:633c8c49396f38df030340797c533e9fe460d1b5d1e42d88e55e938e525f548c", size = 213757, upload-time = "2026-05-26T06:50:01.973Z" }, { url = "https://files.pythonhosted.org/packages/17/6f/5d5f32c4dfcdb16ce2ec5363da686840f13c13e1a1214cb70b49e1cd6d9f/zope_interface-8.5-cp314-cp314t-macosx_10_9_x86_64.whl", hash = "sha256:133999820fdbae513c36c03d6f29ef87317aaa3edef39112222b155083664714", size = 213591, upload-time = "2026-05-26T06:50:03.529Z" }, { url = "https://files.pythonhosted.org/packages/f3/55/de0c3459ff717fce3342f9a29464c281fdeb0d36c3171ee88d119d5f0650/zope_interface-8.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8bd75c96966e573232f0599deaff717564828031c7f05563ccc1ac35c5ee0304", size = 213733, upload-time = "2026-05-26T06:50:05.101Z" }, { url = "https://files.pythonhosted.org/packages/c2/95/d97430abd5ae9677e8b9295b58720c0064a5b557dbb6b8bf5928484cf0d8/zope_interface-8.5-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:14b0e9799351d4c34fe99afd67f0cdd76e55ba15c66a98699d5fc22ea8241e08", size = 294905, upload-time = "2026-05-26T06:50:07.384Z" }, { url = "https://files.pythonhosted.org/packages/41/ec/a0f8f3dad6e74992f4654bdd94802be0929eabca7b871cac3b6fbb5e961b/zope_interface-8.5-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0cd6a732ac84b94eb1ef9222a117347a27efd294ee16810ffdf7ecd307677ed5", size = 300885, upload-time = "2026-05-26T06:50:08.997Z" }, { url = "https://files.pythonhosted.org/packages/0f/da/6881b48803a0ee8d23eb5efa30fce3ed218a2bd9de5758ce489d224fee81/zope_interface-8.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:798b7c87d0e59a7d5d086d642208d0d8700ff0d55c4029134b3c479c3bfb110f", size = 304672, upload-time = "2026-05-26T06:50:10.563Z" }, { url = "https://files.pythonhosted.org/packages/2e/0e/b4c01320859ff1d585438bc231fd60bd258d096359bccf6654fecdf0cffb/zope_interface-8.5-cp314-cp314t-win_amd64.whl", hash = "sha256:0fc3a9d45f114d27eaa1e53beeb144533689edca8a9f66505b1e8e8b3f075e42", size = 217241, upload-time = "2026-05-26T06:50:12.171Z" }, ]